• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
getMetricsCategory()76     public int getMetricsCategory() {
77         return MetricsProto.MetricsEvent.ZONE_PICKER;
78     }
79 
80     @Override
getPreferenceScreenResId()81     protected int getPreferenceScreenResId() {
82         return R.xml.time_zone_prefs;
83     }
84 
85     @Override
getLogTag()86     protected String getLogTag() {
87         return TAG;
88     }
89 
90     /**
91      * Called during onAttach
92      */
93     @VisibleForTesting
94     @Override
createPreferenceControllers(Context context)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
onCreate(Bundle icicle)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
onActivityResult(int requestCode, int resultCode, Intent data)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
setTimeZoneData(TimeZoneData timeZoneData)159     void setTimeZoneData(TimeZoneData timeZoneData) {
160         mTimeZoneData = timeZoneData;
161     }
162 
onTimeZoneDataReady(TimeZoneData timeZoneData)163     private void onTimeZoneDataReady(TimeZoneData timeZoneData) {
164         if (mTimeZoneData == null && timeZoneData != null) {
165             mTimeZoneData = timeZoneData;
166             setupForCurrentTimeZone();
167             getActivity().invalidateOptionsMenu();
168         }
169 
170     }
171 
startRegionPicker()172     private void startRegionPicker() {
173         startPickerFragment(RegionSearchPicker.class, new Bundle(), REQUEST_CODE_REGION_PICKER);
174     }
175 
onRegionZonePreferenceClicked()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 
startFixedOffsetPicker()183     private void startFixedOffsetPicker() {
184         startPickerFragment(FixedOffsetPicker.class, new Bundle(),
185                 REQUEST_CODE_FIXED_OFFSET_ZONE_PICKER);
186     }
187 
startPickerFragment(Class<? extends BaseTimeZonePicker> fragmentClass, Bundle args, int resultRequestCode)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 
setDisplayedRegion(String regionId)198     private void setDisplayedRegion(String regionId) {
199         use(RegionPreferenceController.class).setRegionId(regionId);
200         updatePreferenceStates();
201     }
202 
setDisplayedTimeZoneInfo(String regionId, String tzId)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 
setDisplayedFixedOffsetTimeZoneInfo(String tzId)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 
onRegionZoneChanged(String regionId, String tzId)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 
onFixedOffsetZoneChanged(String tzId)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 
saveTimeZone(String regionId, String tzId)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
onCreateOptionsMenu(Menu menu, MenuInflater inflater)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
onPrepareOptionsMenu(Menu menu)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
onOptionsItemSelected(MenuItem item)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 
setupForCurrentTimeZone()295     private void setupForCurrentTimeZone() {
296         mSelectedTimeZoneId = TimeZone.getDefault().getID();
297         setSelectByRegion(!isFixedOffset(mSelectedTimeZoneId));
298     }
299 
isFixedOffset(String tzId)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      */
setSelectByRegion(boolean selectByRegion)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      */
findRegionIdForTzId(String tzId)340     private String findRegionIdForTzId(String tzId) {
341         return findRegionIdForTzId(tzId,
342                 getPreferenceManager().getSharedPreferences().getString(PREF_KEY_REGION, null),
343                 getLocaleRegionId());
344     }
345 
346     @VisibleForTesting
findRegionIdForTzId(String tzId, String sharePrefRegionId, String localeRegionId)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 
setPreferenceCategoryVisible(PreferenceCategory category, boolean isVisible)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 
getLocaleRegionId()372     private String getLocaleRegionId() {
373         return mLocale.getCountry().toUpperCase(Locale.US);
374     }
375 }
376