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