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 218 // Only clickable when the region has more than 1 time zones or no time zone is selected. 219 use(RegionZonePreferenceController.class).setClickable(tzInfo == null || 220 (countryTimeZones != null 221 && countryTimeZones.getPreferredTimeZoneIds().size() > 1)); 222 use(TimeZoneInfoPreferenceController.class).setTimeZoneInfo(tzInfo); 223 224 updatePreferenceStates(); 225 } 226 setDisplayedFixedOffsetTimeZoneInfo(String tzId)227 private void setDisplayedFixedOffsetTimeZoneInfo(String tzId) { 228 if (isFixedOffset(tzId)) { 229 use(FixedOffsetPreferenceController.class).setTimeZoneInfo( 230 mTimeZoneInfoFormatter.format(tzId)); 231 } else { 232 use(FixedOffsetPreferenceController.class).setTimeZoneInfo(null); 233 } 234 updatePreferenceStates(); 235 } 236 onZonePickerRequestResult(TimeZoneData timeZoneData, Intent data)237 private void onZonePickerRequestResult(TimeZoneData timeZoneData, Intent data) { 238 String regionId = data.getStringExtra(RegionSearchPicker.EXTRA_RESULT_REGION_ID); 239 String tzId = data.getStringExtra(RegionZonePicker.EXTRA_RESULT_TIME_ZONE_ID); 240 // Ignore the result if user didn't change the region or time zone. 241 if (Objects.equals(regionId, use(RegionPreferenceController.class).getRegionId()) 242 && Objects.equals(tzId, mSelectedTimeZoneId)) { 243 return; 244 } 245 246 FilteredCountryTimeZones countryTimeZones = 247 timeZoneData.lookupCountryTimeZones(regionId); 248 if (countryTimeZones == null 249 || !countryTimeZones.getPreferredTimeZoneIds().contains(tzId)) { 250 Log.e(TAG, "Unknown time zone id is selected: " + tzId); 251 return; 252 } 253 254 mSelectedTimeZoneId = tzId; 255 setDisplayedRegion(regionId); 256 setDisplayedTimeZoneInfo(regionId, mSelectedTimeZoneId); 257 saveTimeZone(regionId, mSelectedTimeZoneId); 258 259 // Switch to the region mode if the user switching from the fixed offset 260 setSelectByRegion(true); 261 } 262 onFixedOffsetZoneChanged(String tzId)263 private void onFixedOffsetZoneChanged(String tzId) { 264 mSelectedTimeZoneId = tzId; 265 setDisplayedFixedOffsetTimeZoneInfo(tzId); 266 saveTimeZone(null, mSelectedTimeZoneId); 267 268 // Switch to the fixed offset mode if the user switching from the region mode 269 setSelectByRegion(false); 270 } 271 saveTimeZone(String regionId, String tzId)272 private void saveTimeZone(String regionId, String tzId) { 273 SharedPreferences.Editor editor = getPreferenceManager().getSharedPreferences().edit(); 274 if (regionId == null) { 275 editor.remove(PREF_KEY_REGION); 276 } else { 277 editor.putString(PREF_KEY_REGION, regionId); 278 } 279 editor.apply(); 280 ManualTimeZoneSuggestion manualTimeZoneSuggestion = 281 TimeZoneDetector.createManualTimeZoneSuggestion(tzId, "Settings: Set time zone"); 282 TimeZoneDetector timeZoneDetector = getActivity().getSystemService(TimeZoneDetector.class); 283 timeZoneDetector.suggestManualTimeZone(manualTimeZoneSuggestion); 284 } 285 286 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)287 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 288 menu.add(0, MENU_BY_REGION, 0, R.string.zone_menu_by_region); 289 menu.add(0, MENU_BY_OFFSET, 0, R.string.zone_menu_by_offset); 290 super.onCreateOptionsMenu(menu, inflater); 291 } 292 293 @Override onPrepareOptionsMenu(Menu menu)294 public void onPrepareOptionsMenu(Menu menu) { 295 // Do not show menu when data is not ready, 296 menu.findItem(MENU_BY_REGION).setVisible(mTimeZoneData != null && !mSelectByRegion); 297 menu.findItem(MENU_BY_OFFSET).setVisible(mTimeZoneData != null && mSelectByRegion); 298 } 299 300 @Override onOptionsItemSelected(MenuItem item)301 public boolean onOptionsItemSelected(MenuItem item) { 302 switch (item.getItemId()) { 303 case MENU_BY_REGION: 304 startRegionPicker(); 305 return true; 306 307 case MENU_BY_OFFSET: 308 startFixedOffsetPicker(); 309 return true; 310 311 default: 312 return false; 313 } 314 } 315 setupForCurrentTimeZone()316 private void setupForCurrentTimeZone() { 317 mSelectedTimeZoneId = TimeZone.getDefault().getID(); 318 setSelectByRegion(!isFixedOffset(mSelectedTimeZoneId)); 319 } 320 isFixedOffset(String tzId)321 private static boolean isFixedOffset(String tzId) { 322 return tzId.startsWith("Etc/GMT") || tzId.equals("Etc/UTC"); 323 } 324 325 /** 326 * Switch the current view to select region or select fixed offset time zone. 327 * When showing the selected region, it guess the selected region from time zone id. 328 * See {@link #findRegionIdForTzId} for more info. 329 */ setSelectByRegion(boolean selectByRegion)330 private void setSelectByRegion(boolean selectByRegion) { 331 mSelectByRegion = selectByRegion; 332 setPreferenceCategoryVisible((PreferenceCategory) findPreference( 333 PREF_KEY_REGION_CATEGORY), selectByRegion); 334 setPreferenceCategoryVisible((PreferenceCategory) findPreference( 335 PREF_KEY_FIXED_OFFSET_CATEGORY), !selectByRegion); 336 final String localeRegionId = getLocaleRegionId(); 337 final Set<String> allCountryIsoCodes = mTimeZoneData.getRegionIds(); 338 339 String displayRegion = allCountryIsoCodes.contains(localeRegionId) ? localeRegionId : null; 340 setDisplayedRegion(displayRegion); 341 setDisplayedTimeZoneInfo(displayRegion, null); 342 343 if (!mSelectByRegion) { 344 setDisplayedFixedOffsetTimeZoneInfo(mSelectedTimeZoneId); 345 return; 346 } 347 348 String regionId = findRegionIdForTzId(mSelectedTimeZoneId); 349 if (regionId != null) { 350 setDisplayedRegion(regionId); 351 setDisplayedTimeZoneInfo(regionId, mSelectedTimeZoneId); 352 } 353 } 354 355 /** 356 * Find the a region associated with the specified time zone, based on the time zone data. 357 * If there are multiple regions associated with the given time zone, the priority will be given 358 * to the region the user last picked and the country in user's locale. 359 * 360 * @return null if no region associated with the time zone 361 */ findRegionIdForTzId(String tzId)362 private String findRegionIdForTzId(String tzId) { 363 return findRegionIdForTzId(tzId, 364 getPreferenceManager().getSharedPreferences().getString(PREF_KEY_REGION, null), 365 getLocaleRegionId()); 366 } 367 368 @VisibleForTesting findRegionIdForTzId(String tzId, String sharePrefRegionId, String localeRegionId)369 String findRegionIdForTzId(String tzId, String sharePrefRegionId, String localeRegionId) { 370 final Set<String> matchedRegions = mTimeZoneData.lookupCountryCodesForZoneId(tzId); 371 if (matchedRegions.size() == 0) { 372 return null; 373 } 374 if (sharePrefRegionId != null && matchedRegions.contains(sharePrefRegionId)) { 375 return sharePrefRegionId; 376 } 377 if (localeRegionId != null && matchedRegions.contains(localeRegionId)) { 378 return localeRegionId; 379 } 380 381 return matchedRegions.toArray(new String[matchedRegions.size()])[0]; 382 } 383 setPreferenceCategoryVisible(PreferenceCategory category, boolean isVisible)384 private void setPreferenceCategoryVisible(PreferenceCategory category, 385 boolean isVisible) { 386 // Hiding category doesn't hide all the children preference. Set visibility of its children. 387 // Do not care grandchildren as time_zone_pref.xml has only 2 levels. 388 category.setVisible(isVisible); 389 for (int i = 0; i < category.getPreferenceCount(); i++) { 390 category.getPreference(i).setVisible(isVisible); 391 } 392 } 393 getLocaleRegionId()394 private String getLocaleRegionId() { 395 return mLocale.getCountry().toUpperCase(Locale.US); 396 } 397 398 public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 399 new BaseSearchIndexProvider(R.xml.time_zone_prefs) { 400 @Override 401 protected boolean isPageSearchEnabled(Context context) { 402 // We can't enter this page if the auto time zone is enabled. 403 final int autoTimeZone = Settings.Global.getInt(context.getContentResolver(), 404 Settings.Global.AUTO_TIME_ZONE, 1); 405 return autoTimeZone == 1 ? false : true; 406 } 407 }; 408 } 409