Home | History | Annotate | Download | only in timezone
      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