1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.datetime.timezone; 18 19 import android.app.Activity; 20 import android.app.settings.SettingsEnums; 21 import android.app.timezonedetector.ManualTimeZoneSuggestion; 22 import android.app.timezonedetector.TimeZoneDetector; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.SharedPreferences; 26 import android.icu.util.TimeZone; 27 import android.os.Bundle; 28 import android.provider.Settings; 29 import android.util.Log; 30 import android.view.Menu; 31 import android.view.MenuInflater; 32 import android.view.MenuItem; 33 34 import androidx.annotation.VisibleForTesting; 35 import androidx.preference.PreferenceCategory; 36 37 import com.android.settings.R; 38 import com.android.settings.core.SubSettingLauncher; 39 import com.android.settings.dashboard.DashboardFragment; 40 import com.android.settings.datetime.timezone.model.FilteredCountryTimeZones; 41 import com.android.settings.datetime.timezone.model.TimeZoneData; 42 import com.android.settings.datetime.timezone.model.TimeZoneDataLoader; 43 import com.android.settings.search.BaseSearchIndexProvider; 44 import com.android.settingslib.core.AbstractPreferenceController; 45 import com.android.settingslib.search.SearchIndexable; 46 47 import java.util.ArrayList; 48 import java.util.Date; 49 import java.util.List; 50 import java.util.Locale; 51 import java.util.Objects; 52 import java.util.Set; 53 54 /** 55 * The class displays a time zone picker either by regions or fixed offset time zones. 56 */ 57 @SearchIndexable 58 public class TimeZoneSettings extends DashboardFragment { 59 60 private static final String TAG = "TimeZoneSettings"; 61 62 private static final int MENU_BY_REGION = Menu.FIRST; 63 private static final int MENU_BY_OFFSET = Menu.FIRST + 1; 64 65 private static final int REQUEST_CODE_REGION_PICKER = 1; 66 private static final int REQUEST_CODE_ZONE_PICKER = 2; 67 private static final int REQUEST_CODE_FIXED_OFFSET_ZONE_PICKER = 3; 68 69 private static final String PREF_KEY_REGION = "time_zone_region"; 70 private static final String PREF_KEY_REGION_CATEGORY = "time_zone_region_preference_category"; 71 private static final String PREF_KEY_FIXED_OFFSET_CATEGORY = 72 "time_zone_fixed_offset_preference_category"; 73 74 private Locale mLocale; 75 private boolean mSelectByRegion; 76 private TimeZoneData mTimeZoneData; 77 private Intent mPendingZonePickerRequestResult; 78 79 private String mSelectedTimeZoneId; 80 private TimeZoneInfo.Formatter mTimeZoneInfoFormatter; 81 82 @Override getMetricsCategory()83 public int getMetricsCategory() { 84 return SettingsEnums.ZONE_PICKER; 85 } 86 87 @Override getPreferenceScreenResId()88 protected int getPreferenceScreenResId() { 89 return R.xml.time_zone_prefs; 90 } 91 92 @Override getLogTag()93 protected String getLogTag() { 94 return TAG; 95 } 96 97 /** 98 * Called during onAttach 99 */ 100 @VisibleForTesting 101 @Override createPreferenceControllers(Context context)102 public List<AbstractPreferenceController> createPreferenceControllers(Context context) { 103 mLocale = context.getResources().getConfiguration().getLocales().get(0); 104 mTimeZoneInfoFormatter = new TimeZoneInfo.Formatter(mLocale, new Date()); 105 final List<AbstractPreferenceController> controllers = new ArrayList<>(); 106 RegionPreferenceController regionPreferenceController = 107 new RegionPreferenceController(context); 108 regionPreferenceController.setOnClickListener(this::startRegionPicker); 109 RegionZonePreferenceController regionZonePreferenceController = 110 new RegionZonePreferenceController(context); 111 regionZonePreferenceController.setOnClickListener(this::onRegionZonePreferenceClicked); 112 FixedOffsetPreferenceController fixedOffsetPreferenceController = 113 new FixedOffsetPreferenceController(context); 114 fixedOffsetPreferenceController.setOnClickListener(this::startFixedOffsetPicker); 115 116 controllers.add(regionPreferenceController); 117 controllers.add(regionZonePreferenceController); 118 controllers.add(fixedOffsetPreferenceController); 119 return controllers; 120 } 121 122 @Override onCreate(Bundle icicle)123 public void onCreate(Bundle icicle) { 124 super.onCreate(icicle); 125 // Hide all interactive preferences 126 setPreferenceCategoryVisible((PreferenceCategory) findPreference( 127 PREF_KEY_REGION_CATEGORY), false); 128 setPreferenceCategoryVisible((PreferenceCategory) findPreference( 129 PREF_KEY_FIXED_OFFSET_CATEGORY), false); 130 131 // Start loading TimeZoneData 132 getLoaderManager().initLoader(0, null, new TimeZoneDataLoader.LoaderCreator( 133 getContext(), this::onTimeZoneDataReady)); 134 } 135 136 @Override onActivityResult(int requestCode, int resultCode, Intent data)137 public void onActivityResult(int requestCode, int resultCode, Intent data) { 138 if (resultCode != Activity.RESULT_OK || data == null) { 139 return; 140 } 141 142 switch (requestCode) { 143 case REQUEST_CODE_REGION_PICKER: 144 case REQUEST_CODE_ZONE_PICKER: { 145 if (mTimeZoneData == null) { 146 mPendingZonePickerRequestResult = data; 147 } else { 148 onZonePickerRequestResult(mTimeZoneData, data); 149 } 150 break; 151 } 152 case REQUEST_CODE_FIXED_OFFSET_ZONE_PICKER: { 153 String tzId = data.getStringExtra(FixedOffsetPicker.EXTRA_RESULT_TIME_ZONE_ID); 154 // Ignore the result if user didn't change the time zone. 155 if (tzId != null && !tzId.equals(mSelectedTimeZoneId)) { 156 onFixedOffsetZoneChanged(tzId); 157 } 158 break; 159 } 160 } 161 } 162 163 @VisibleForTesting setTimeZoneData(TimeZoneData timeZoneData)164 void setTimeZoneData(TimeZoneData timeZoneData) { 165 mTimeZoneData = timeZoneData; 166 } 167 onTimeZoneDataReady(TimeZoneData timeZoneData)168 private void onTimeZoneDataReady(TimeZoneData timeZoneData) { 169 if (mTimeZoneData == null && timeZoneData != null) { 170 mTimeZoneData = timeZoneData; 171 setupForCurrentTimeZone(); 172 getActivity().invalidateOptionsMenu(); 173 if (mPendingZonePickerRequestResult != null) { 174 onZonePickerRequestResult(timeZoneData, mPendingZonePickerRequestResult); 175 mPendingZonePickerRequestResult = null; 176 } 177 } 178 } 179 startRegionPicker()180 private void startRegionPicker() { 181 startPickerFragment(RegionSearchPicker.class, new Bundle(), REQUEST_CODE_REGION_PICKER); 182 } 183 onRegionZonePreferenceClicked()184 private void onRegionZonePreferenceClicked() { 185 final Bundle args = new Bundle(); 186 args.putString(RegionZonePicker.EXTRA_REGION_ID, 187 use(RegionPreferenceController.class).getRegionId()); 188 startPickerFragment(RegionZonePicker.class, args, REQUEST_CODE_ZONE_PICKER); 189 } 190 startFixedOffsetPicker()191 private void startFixedOffsetPicker() { 192 startPickerFragment(FixedOffsetPicker.class, new Bundle(), 193 REQUEST_CODE_FIXED_OFFSET_ZONE_PICKER); 194 } 195 startPickerFragment(Class<? extends BaseTimeZonePicker> fragmentClass, Bundle args, int resultRequestCode)196 private void startPickerFragment(Class<? extends BaseTimeZonePicker> fragmentClass, Bundle args, 197 int resultRequestCode) { 198 new SubSettingLauncher(getContext()) 199 .setDestination(fragmentClass.getCanonicalName()) 200 .setArguments(args) 201 .setSourceMetricsCategory(getMetricsCategory()) 202 .setResultListener(this, resultRequestCode) 203 .launch(); 204 } 205 setDisplayedRegion(String regionId)206 private void setDisplayedRegion(String regionId) { 207 use(RegionPreferenceController.class).setRegionId(regionId); 208 updatePreferenceStates(); 209 } 210 setDisplayedTimeZoneInfo(String regionId, String tzId)211 private void setDisplayedTimeZoneInfo(String regionId, String tzId) { 212 final TimeZoneInfo tzInfo = tzId == null ? null : mTimeZoneInfoFormatter.format(tzId); 213 final FilteredCountryTimeZones countryTimeZones = 214 mTimeZoneData.lookupCountryTimeZones(regionId); 215 216 use(RegionZonePreferenceController.class).setTimeZoneInfo(tzInfo); 217 // Only clickable when the region has more than 1 time zones or no time zone is selected. 218 219 use(RegionZonePreferenceController.class).setClickable(tzInfo == null || 220 (countryTimeZones != null && countryTimeZones.getTimeZoneIds().size() > 1)); 221 use(TimeZoneInfoPreferenceController.class).setTimeZoneInfo(tzInfo); 222 223 updatePreferenceStates(); 224 } 225 setDisplayedFixedOffsetTimeZoneInfo(String tzId)226 private void setDisplayedFixedOffsetTimeZoneInfo(String tzId) { 227 if (isFixedOffset(tzId)) { 228 use(FixedOffsetPreferenceController.class).setTimeZoneInfo( 229 mTimeZoneInfoFormatter.format(tzId)); 230 } else { 231 use(FixedOffsetPreferenceController.class).setTimeZoneInfo(null); 232 } 233 updatePreferenceStates(); 234 } 235 onZonePickerRequestResult(TimeZoneData timeZoneData, Intent data)236 private void onZonePickerRequestResult(TimeZoneData timeZoneData, Intent data) { 237 String regionId = data.getStringExtra(RegionSearchPicker.EXTRA_RESULT_REGION_ID); 238 String tzId = data.getStringExtra(RegionZonePicker.EXTRA_RESULT_TIME_ZONE_ID); 239 // Ignore the result if user didn't change the region or time zone. 240 if (Objects.equals(regionId, use(RegionPreferenceController.class).getRegionId()) 241 && Objects.equals(tzId, mSelectedTimeZoneId)) { 242 return; 243 } 244 245 FilteredCountryTimeZones countryTimeZones = 246 timeZoneData.lookupCountryTimeZones(regionId); 247 if (countryTimeZones == null || !countryTimeZones.getTimeZoneIds().contains(tzId)) { 248 Log.e(TAG, "Unknown time zone id is selected: " + tzId); 249 return; 250 } 251 252 mSelectedTimeZoneId = tzId; 253 setDisplayedRegion(regionId); 254 setDisplayedTimeZoneInfo(regionId, mSelectedTimeZoneId); 255 saveTimeZone(regionId, mSelectedTimeZoneId); 256 257 // Switch to the region mode if the user switching from the fixed offset 258 setSelectByRegion(true); 259 } 260 onFixedOffsetZoneChanged(String tzId)261 private void onFixedOffsetZoneChanged(String tzId) { 262 mSelectedTimeZoneId = tzId; 263 setDisplayedFixedOffsetTimeZoneInfo(tzId); 264 saveTimeZone(null, mSelectedTimeZoneId); 265 266 // Switch to the fixed offset mode if the user switching from the region mode 267 setSelectByRegion(false); 268 } 269 saveTimeZone(String regionId, String tzId)270 private void saveTimeZone(String regionId, String tzId) { 271 SharedPreferences.Editor editor = getPreferenceManager().getSharedPreferences().edit(); 272 if (regionId == null) { 273 editor.remove(PREF_KEY_REGION); 274 } else { 275 editor.putString(PREF_KEY_REGION, regionId); 276 } 277 editor.apply(); 278 ManualTimeZoneSuggestion manualTimeZoneSuggestion = 279 TimeZoneDetector.createManualTimeZoneSuggestion(tzId, "Settings: Set time zone"); 280 TimeZoneDetector timeZoneDetector = getActivity().getSystemService(TimeZoneDetector.class); 281 timeZoneDetector.suggestManualTimeZone(manualTimeZoneSuggestion); 282 } 283 284 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)285 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 286 menu.add(0, MENU_BY_REGION, 0, R.string.zone_menu_by_region); 287 menu.add(0, MENU_BY_OFFSET, 0, R.string.zone_menu_by_offset); 288 super.onCreateOptionsMenu(menu, inflater); 289 } 290 291 @Override onPrepareOptionsMenu(Menu menu)292 public void onPrepareOptionsMenu(Menu menu) { 293 // Do not show menu when data is not ready, 294 menu.findItem(MENU_BY_REGION).setVisible(mTimeZoneData != null && !mSelectByRegion); 295 menu.findItem(MENU_BY_OFFSET).setVisible(mTimeZoneData != null && mSelectByRegion); 296 } 297 298 @Override onOptionsItemSelected(MenuItem item)299 public boolean onOptionsItemSelected(MenuItem item) { 300 switch (item.getItemId()) { 301 case MENU_BY_REGION: 302 startRegionPicker(); 303 return true; 304 305 case MENU_BY_OFFSET: 306 startFixedOffsetPicker(); 307 return true; 308 309 default: 310 return false; 311 } 312 } 313 setupForCurrentTimeZone()314 private void setupForCurrentTimeZone() { 315 mSelectedTimeZoneId = TimeZone.getDefault().getID(); 316 setSelectByRegion(!isFixedOffset(mSelectedTimeZoneId)); 317 } 318 isFixedOffset(String tzId)319 private static boolean isFixedOffset(String tzId) { 320 return tzId.startsWith("Etc/GMT") || tzId.equals("Etc/UTC"); 321 } 322 323 /** 324 * Switch the current view to select region or select fixed offset time zone. 325 * When showing the selected region, it guess the selected region from time zone id. 326 * See {@link #findRegionIdForTzId} for more info. 327 */ setSelectByRegion(boolean selectByRegion)328 private void setSelectByRegion(boolean selectByRegion) { 329 mSelectByRegion = selectByRegion; 330 setPreferenceCategoryVisible((PreferenceCategory) findPreference( 331 PREF_KEY_REGION_CATEGORY), selectByRegion); 332 setPreferenceCategoryVisible((PreferenceCategory) findPreference( 333 PREF_KEY_FIXED_OFFSET_CATEGORY), !selectByRegion); 334 final String localeRegionId = getLocaleRegionId(); 335 final Set<String> allCountryIsoCodes = mTimeZoneData.getRegionIds(); 336 337 String displayRegion = allCountryIsoCodes.contains(localeRegionId) ? localeRegionId : null; 338 setDisplayedRegion(displayRegion); 339 setDisplayedTimeZoneInfo(displayRegion, null); 340 341 if (!mSelectByRegion) { 342 setDisplayedFixedOffsetTimeZoneInfo(mSelectedTimeZoneId); 343 return; 344 } 345 346 String regionId = findRegionIdForTzId(mSelectedTimeZoneId); 347 if (regionId != null) { 348 setDisplayedRegion(regionId); 349 setDisplayedTimeZoneInfo(regionId, mSelectedTimeZoneId); 350 } 351 } 352 353 /** 354 * Find the a region associated with the specified time zone, based on the time zone data. 355 * If there are multiple regions associated with the given time zone, the priority will be given 356 * to the region the user last picked and the country in user's locale. 357 * 358 * @return null if no region associated with the time zone 359 */ findRegionIdForTzId(String tzId)360 private String findRegionIdForTzId(String tzId) { 361 return findRegionIdForTzId(tzId, 362 getPreferenceManager().getSharedPreferences().getString(PREF_KEY_REGION, null), 363 getLocaleRegionId()); 364 } 365 366 @VisibleForTesting findRegionIdForTzId(String tzId, String sharePrefRegionId, String localeRegionId)367 String findRegionIdForTzId(String tzId, String sharePrefRegionId, String localeRegionId) { 368 final Set<String> matchedRegions = mTimeZoneData.lookupCountryCodesForZoneId(tzId); 369 if (matchedRegions.size() == 0) { 370 return null; 371 } 372 if (sharePrefRegionId != null && matchedRegions.contains(sharePrefRegionId)) { 373 return sharePrefRegionId; 374 } 375 if (localeRegionId != null && matchedRegions.contains(localeRegionId)) { 376 return localeRegionId; 377 } 378 379 return matchedRegions.toArray(new String[matchedRegions.size()])[0]; 380 } 381 setPreferenceCategoryVisible(PreferenceCategory category, boolean isVisible)382 private void setPreferenceCategoryVisible(PreferenceCategory category, 383 boolean isVisible) { 384 // Hiding category doesn't hide all the children preference. Set visibility of its children. 385 // Do not care grandchildren as time_zone_pref.xml has only 2 levels. 386 category.setVisible(isVisible); 387 for (int i = 0; i < category.getPreferenceCount(); i++) { 388 category.getPreference(i).setVisible(isVisible); 389 } 390 } 391 getLocaleRegionId()392 private String getLocaleRegionId() { 393 return mLocale.getCountry().toUpperCase(Locale.US); 394 } 395 396 public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 397 new BaseSearchIndexProvider(R.xml.time_zone_prefs) { 398 @Override 399 protected boolean isPageSearchEnabled(Context context) { 400 // We can't enter this page if the auto time zone is enabled. 401 final int autoTimeZone = Settings.Global.getInt(context.getContentResolver(), 402 Settings.Global.AUTO_TIME_ZONE, 1); 403 return autoTimeZone == 1 ? false : true; 404 } 405 }; 406 } 407