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