diff --git a/res/xml/time_zone_prefs.xml b/res/xml/time_zone_prefs.xml new file mode 100644 index 00000000000..f80de8c1e5c --- /dev/null +++ b/res/xml/time_zone_prefs.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + diff --git a/src/com/android/settings/datetime/timezone/BaseTimeZonePreferenceController.java b/src/com/android/settings/datetime/timezone/BaseTimeZonePreferenceController.java new file mode 100644 index 00000000000..846fce02ffc --- /dev/null +++ b/src/com/android/settings/datetime/timezone/BaseTimeZonePreferenceController.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datetime.timezone; + +import android.content.Context; +import android.support.v7.preference.Preference; + +import com.android.settings.core.BasePreferenceController; + +import com.google.common.base.Objects; + +public abstract class BaseTimeZonePreferenceController extends BasePreferenceController { + private OnPreferenceClickListener mOnClickListener; + + protected BaseTimeZonePreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public boolean handlePreferenceTreeClick(Preference preference) { + if (mOnClickListener == null || !Objects.equal(getPreferenceKey(), preference.getKey())) { + return false; + } + + mOnClickListener.onClick(); + return true; + } + + public void setOnClickListener(OnPreferenceClickListener listener) { + mOnClickListener = listener; + } +} diff --git a/src/com/android/settings/datetime/timezone/FixedOffsetPreferenceController.java b/src/com/android/settings/datetime/timezone/FixedOffsetPreferenceController.java new file mode 100644 index 00000000000..16c1f193e5a --- /dev/null +++ b/src/com/android/settings/datetime/timezone/FixedOffsetPreferenceController.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datetime.timezone; + +import android.content.Context; +import android.support.v7.preference.Preference; + +public class FixedOffsetPreferenceController extends BaseTimeZonePreferenceController { + + private static final String PREFERENCE_KEY = "fixed_offset"; + + private TimeZoneInfo mTimeZoneInfo; + + public FixedOffsetPreferenceController(Context context) { + super(context, PREFERENCE_KEY); + } + + @Override + public CharSequence getSummary() { + // This is a Spannable object, which contains TTS span. It shouldn't be converted to String. + return mTimeZoneInfo == null ? "" : mTimeZoneInfo.getGmtOffset(); + } + + public void setTimeZoneInfo(TimeZoneInfo timeZoneInfo) { + mTimeZoneInfo = timeZoneInfo; + } + + public TimeZoneInfo getTimeZoneInfo() { + return mTimeZoneInfo; + } +} + diff --git a/src/com/android/settings/datetime/timezone/OnPreferenceClickListener.java b/src/com/android/settings/datetime/timezone/OnPreferenceClickListener.java new file mode 100644 index 00000000000..3e4d7152908 --- /dev/null +++ b/src/com/android/settings/datetime/timezone/OnPreferenceClickListener.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datetime.timezone; + +/** + * Callback when a preference is clicked in {@class TimeZoneSettings} + */ +public interface OnPreferenceClickListener { + void onClick(); +} diff --git a/src/com/android/settings/datetime/timezone/RegionPreferenceController.java b/src/com/android/settings/datetime/timezone/RegionPreferenceController.java new file mode 100644 index 00000000000..201b9bd2e22 --- /dev/null +++ b/src/com/android/settings/datetime/timezone/RegionPreferenceController.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.datetime.timezone; + +import android.content.Context; +import android.icu.text.LocaleDisplayNames; +import android.support.v7.preference.Preference; + +import java.util.Locale; + +public class RegionPreferenceController extends BaseTimeZonePreferenceController { + private static final String PREFERENCE_KEY = "region"; + + private final LocaleDisplayNames mLocaleDisplayNames; + private String mRegionId = ""; + + public RegionPreferenceController(Context context) { + super(context, PREFERENCE_KEY); + Locale locale = context.getResources().getConfiguration().getLocales().get(0); + mLocaleDisplayNames = LocaleDisplayNames.getInstance(locale); + + } + + @Override + public CharSequence getSummary() { + return mLocaleDisplayNames.regionDisplayName(mRegionId); + } + + public void setRegionId(String regionId) { + mRegionId = regionId; + } + + public String getRegionId() { + return mRegionId; + } +} diff --git a/src/com/android/settings/datetime/timezone/RegionZonePreferenceController.java b/src/com/android/settings/datetime/timezone/RegionZonePreferenceController.java new file mode 100644 index 00000000000..85f41658fe4 --- /dev/null +++ b/src/com/android/settings/datetime/timezone/RegionZonePreferenceController.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datetime.timezone; + +import android.content.Context; +import android.support.v7.preference.Preference; + +import com.android.settings.R; + +public class RegionZonePreferenceController extends BaseTimeZonePreferenceController { + private static final String PREFERENCE_KEY = "region_zone"; + + private TimeZoneInfo mTimeZoneInfo; + private boolean mIsClickable; + + public RegionZonePreferenceController(Context context) { + super(context, PREFERENCE_KEY); + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + preference.setEnabled(isClickable()); + } + + @Override + public CharSequence getSummary() { + return mTimeZoneInfo == null ? "" + : SpannableUtil.getResourcesText(mContext.getResources(), + R.string.zone_info_exemplar_location_and_offset, + mTimeZoneInfo.getExemplarLocation(), mTimeZoneInfo.getGmtOffset()); + } + + public void setTimeZoneInfo(TimeZoneInfo timeZoneInfo) { + mTimeZoneInfo = timeZoneInfo; + } + + public TimeZoneInfo getTimeZoneInfo() { + return mTimeZoneInfo; + } + + public void setClickable(boolean clickable) { + mIsClickable = clickable; + } + + public boolean isClickable() { + return mIsClickable; + } +} diff --git a/src/com/android/settings/datetime/timezone/TimeZoneInfoPreferenceController.java b/src/com/android/settings/datetime/timezone/TimeZoneInfoPreferenceController.java new file mode 100644 index 00000000000..0f0264f516c --- /dev/null +++ b/src/com/android/settings/datetime/timezone/TimeZoneInfoPreferenceController.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datetime.timezone; + +import android.content.Context; +import android.icu.impl.OlsonTimeZone; +import android.icu.text.DateFormat; +import android.icu.text.DisplayContext; +import android.icu.text.SimpleDateFormat; +import android.icu.util.Calendar; +import android.icu.util.TimeZone; +import android.icu.util.TimeZoneTransition; +import android.support.annotation.VisibleForTesting; +import android.support.v7.preference.Preference; + +import com.android.settings.R; +import com.android.settingslib.widget.FooterPreference; + +import java.util.Date; + +public class TimeZoneInfoPreferenceController extends BaseTimeZonePreferenceController { + private static final String PREFERENCE_KEY = FooterPreference.KEY_FOOTER; + + private TimeZoneInfo mTimeZoneInfo; + private final DateFormat mDateFormat; + private final Date mDate; + + public TimeZoneInfoPreferenceController(Context context) { + this(context, new Date()); + } + + @VisibleForTesting + TimeZoneInfoPreferenceController(Context context, Date date) { + super(context, PREFERENCE_KEY); + mDateFormat = DateFormat.getDateInstance(SimpleDateFormat.LONG); + mDateFormat.setContext(DisplayContext.CAPITALIZATION_NONE); + mDate = date; + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public void updateState(Preference preference) { + CharSequence formattedTimeZone = mTimeZoneInfo == null ? "" : formatInfo(mTimeZoneInfo); + preference.setTitle(formattedTimeZone); + preference.setVisible(mTimeZoneInfo != null); + } + + public void setTimeZoneInfo(TimeZoneInfo timeZoneInfo) { + mTimeZoneInfo = timeZoneInfo; + } + + public TimeZoneInfo getTimeZoneInfo() { + return mTimeZoneInfo; + } + + private CharSequence formatOffsetAndName(TimeZoneInfo item) { + String name = item.getGenericName(); + if (name == null) { + if (item.getTimeZone().inDaylightTime(mDate)) { + name = item.getDaylightName(); + } else { + name = item.getStandardName(); + } + } + if (name == null) { + return item.getGmtOffset().toString(); + } else { + return SpannableUtil.getResourcesText(mContext.getResources(), + R.string.zone_info_offset_and_name, item.getGmtOffset(), + name); + } + } + + private CharSequence formatInfo(TimeZoneInfo item) { + final CharSequence offsetAndName = formatOffsetAndName(item); + final TimeZone timeZone = item.getTimeZone(); + if (!timeZone.observesDaylightTime()) { + return mContext.getString(R.string.zone_info_footer_no_dst, offsetAndName); + } + + final TimeZoneTransition nextDstTransition = findNextDstTransition(timeZone); + if (nextDstTransition == null) { + return null; + } + final boolean toDst = nextDstTransition.getTo().getDSTSavings() != 0; + String timeType = toDst ? item.getDaylightName() : item.getStandardName(); + if (timeType == null) { + // Fall back to generic "summer time" and "standard time" if the time zone has no + // specific names. + timeType = toDst ? + mContext.getString(R.string.zone_time_type_dst) : + mContext.getString(R.string.zone_time_type_standard); + + } + final Calendar transitionTime = Calendar.getInstance(timeZone); + transitionTime.setTimeInMillis(nextDstTransition.getTime()); + final String date = mDateFormat.format(transitionTime); + return SpannableUtil.getResourcesText(mContext.getResources(), + R.string.zone_info_footer, offsetAndName, timeType, date); + } + + private TimeZoneTransition findNextDstTransition(TimeZone timeZone) { + if (!(timeZone instanceof OlsonTimeZone)) { + return null; + } + final OlsonTimeZone olsonTimeZone = (OlsonTimeZone) timeZone; + TimeZoneTransition transition = olsonTimeZone.getNextTransition( + mDate.getTime(), /* inclusive */ false); + do { + if (transition.getTo().getDSTSavings() != transition.getFrom().getDSTSavings()) { + break; + } + transition = olsonTimeZone.getNextTransition( + transition.getTime(), /*inclusive */ false); + } while (transition != null); + return transition; + } + +} diff --git a/src/com/android/settings/datetime/timezone/TimeZoneSettings.java b/src/com/android/settings/datetime/timezone/TimeZoneSettings.java new file mode 100644 index 00000000000..aeb5a8003bb --- /dev/null +++ b/src/com/android/settings/datetime/timezone/TimeZoneSettings.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datetime.timezone; + +import android.app.Activity; +import android.app.AlarmManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.icu.util.TimeZone; +import android.os.Bundle; +import android.support.annotation.VisibleForTesting; +import android.support.v7.preference.PreferenceCategory; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import com.android.internal.logging.nano.MetricsProto; +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.datetime.timezone.model.FilteredCountryTimeZones; +import com.android.settings.datetime.timezone.model.TimeZoneData; +import com.android.settings.datetime.timezone.model.TimeZoneDataLoader; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; + +/** + * The class displays a time zone picker either by regions or fixed offset time zones. + */ +public class TimeZoneSettings extends DashboardFragment { + + private static final String TAG = "TimeZoneSettings"; + + private static final int MENU_BY_REGION = Menu.FIRST; + private static final int MENU_BY_OFFSET = Menu.FIRST + 1; + + private static final int REQUEST_CODE_REGION_PICKER = 1; + private static final int REQUEST_CODE_ZONE_PICKER = 2; + private static final int REQUEST_CODE_FIXED_OFFSET_ZONE_PICKER = 3; + + private static final String PREF_KEY_REGION = "time_zone_region"; + private static final String PREF_KEY_REGION_CATEGORY = "time_zone_region_preference_category"; + private static final String PREF_KEY_FIXED_OFFSET_CATEGORY = + "time_zone_fixed_offset_preference_category"; + + private Locale mLocale; + private boolean mSelectByRegion; + private TimeZoneData mTimeZoneData; + + private String mSelectedTimeZoneId; + private TimeZoneInfo.Formatter mTimeZoneInfoFormatter; + + @Override + public int getMetricsCategory() { + return MetricsProto.MetricsEvent.ZONE_PICKER; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.time_zone_prefs; + } + + @Override + protected String getLogTag() { + return TAG; + } + + /** + * Called during onAttach + */ + @VisibleForTesting + @Override + public List createPreferenceControllers(Context context) { + mLocale = context.getResources().getConfiguration().getLocales().get(0); + mTimeZoneInfoFormatter = new TimeZoneInfo.Formatter(mLocale, new Date()); + final List controllers = new ArrayList<>(); + RegionPreferenceController regionPreferenceController = + new RegionPreferenceController(context); + regionPreferenceController.setOnClickListener(this::onRegionPreferenceClicked); + RegionZonePreferenceController regionZonePreferenceController = + new RegionZonePreferenceController(context); + regionZonePreferenceController.setOnClickListener(this::onRegionZonePreferenceClicked); + TimeZoneInfoPreferenceController timeZoneInfoPreferenceController = + new TimeZoneInfoPreferenceController(context); + FixedOffsetPreferenceController fixedOffsetPreferenceController = + new FixedOffsetPreferenceController(context); + fixedOffsetPreferenceController.setOnClickListener(this::onFixedOffsetPreferenceClicked); + + controllers.add(regionPreferenceController); + controllers.add(regionZonePreferenceController); + controllers.add(timeZoneInfoPreferenceController); + controllers.add(fixedOffsetPreferenceController); + return controllers; + } + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + // Hide all interactive preferences + setPreferenceCategoryVisible((PreferenceCategory) findPreference( + PREF_KEY_REGION_CATEGORY), false); + setPreferenceCategoryVisible((PreferenceCategory) findPreference( + PREF_KEY_FIXED_OFFSET_CATEGORY), false); + + // Start loading TimeZoneData + getLoaderManager().initLoader(0, null, new TimeZoneDataLoader.LoaderCreator( + getContext(), this::onTimeZoneDataReady)); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode != Activity.RESULT_OK || data == null) { + return; + } + + switch (requestCode) { + case REQUEST_CODE_REGION_PICKER: + case REQUEST_CODE_ZONE_PICKER: { + String regionId = data.getStringExtra(RegionSearchPicker.EXTRA_RESULT_REGION_ID); + String tzId = data.getStringExtra(RegionZonePicker.EXTRA_RESULT_TIME_ZONE_ID); + // Ignore the result if user didn't change the region or time zone. + if (!Objects.equals(regionId, use(RegionPreferenceController.class).getRegionId()) + || !Objects.equals(tzId, mSelectedTimeZoneId)) { + onRegionZoneChanged(regionId, tzId); + } + break; + } + case REQUEST_CODE_FIXED_OFFSET_ZONE_PICKER: { + String tzId = data.getStringExtra(FixedOffsetPicker.EXTRA_RESULT_TIME_ZONE_ID); + // Ignore the result if user didn't change the time zone. + if (tzId != null && !tzId.equals(mSelectedTimeZoneId)) { + onFixedOffsetZoneChanged(tzId); + } + break; + } + } + } + + @VisibleForTesting + void setTimeZoneData(TimeZoneData timeZoneData) { + mTimeZoneData = timeZoneData; + } + + private void onTimeZoneDataReady(TimeZoneData timeZoneData) { + if (mTimeZoneData == null && timeZoneData != null) { + mTimeZoneData = timeZoneData; + setupForCurrentTimeZone(); + getActivity().invalidateOptionsMenu(); + } + + } + + private void onRegionPreferenceClicked() { + startPickerFragment(RegionSearchPicker.class, new Bundle(), REQUEST_CODE_REGION_PICKER); + } + + private void onRegionZonePreferenceClicked() { + final Bundle args = new Bundle(); + args.putString(RegionZonePicker.EXTRA_REGION_ID, + use(RegionPreferenceController.class).getRegionId()); + startPickerFragment(RegionZonePicker.class, args, REQUEST_CODE_ZONE_PICKER); + } + + private void onFixedOffsetPreferenceClicked() { + startPickerFragment(FixedOffsetPicker.class, new Bundle(), + REQUEST_CODE_FIXED_OFFSET_ZONE_PICKER); + } + + private void startPickerFragment(Class fragmentClass, Bundle args, + int resultRequestCode) { + new SubSettingLauncher(getContext()) + .setDestination(fragmentClass.getCanonicalName()) + .setArguments(args) + .setSourceMetricsCategory(getMetricsCategory()) + .setResultListener(this, resultRequestCode) + .launch(); + } + + private void setDisplayedRegion(String regionId) { + use(RegionPreferenceController.class).setRegionId(regionId); + updatePreferenceStates(); + } + + private void setDisplayedTimeZoneInfo(String regionId, String tzId) { + final TimeZoneInfo tzInfo = tzId == null ? null : mTimeZoneInfoFormatter.format(tzId); + final FilteredCountryTimeZones countryTimeZones = + mTimeZoneData.lookupCountryTimeZones(regionId); + + use(RegionZonePreferenceController.class).setTimeZoneInfo(tzInfo); + // Only clickable when the region has more than 1 time zones or no time zone is selected. + + use(RegionZonePreferenceController.class).setClickable(tzInfo == null || + (countryTimeZones != null && countryTimeZones.getTimeZoneIds().size() > 1)); + use(TimeZoneInfoPreferenceController.class).setTimeZoneInfo(tzInfo); + + updatePreferenceStates(); + } + + private void setDisplayedFixedOffsetTimeZoneInfo(String tzId) { + if (isFixedOffset(tzId)) { + use(FixedOffsetPreferenceController.class).setTimeZoneInfo( + mTimeZoneInfoFormatter.format(tzId)); + } else { + use(FixedOffsetPreferenceController.class).setTimeZoneInfo(null); + } + updatePreferenceStates(); + } + + private void onRegionZoneChanged(String regionId, String tzId) { + FilteredCountryTimeZones countryTimeZones = + mTimeZoneData.lookupCountryTimeZones(regionId); + if (countryTimeZones == null || !countryTimeZones.getTimeZoneIds().contains(tzId)) { + Log.e(TAG, "Unknown time zone id is selected: " + tzId); + return; + } + + mSelectedTimeZoneId = tzId; + setDisplayedRegion(regionId); + setDisplayedTimeZoneInfo(regionId, mSelectedTimeZoneId); + saveTimeZone(regionId, mSelectedTimeZoneId); + } + + private void onFixedOffsetZoneChanged(String tzId) { + mSelectedTimeZoneId = tzId; + setDisplayedFixedOffsetTimeZoneInfo(tzId); + saveTimeZone(null, mSelectedTimeZoneId); + } + + private void saveTimeZone(String regionId, String tzId) { + SharedPreferences.Editor editor = getPreferenceManager().getSharedPreferences().edit(); + if (regionId == null) { + editor.remove(PREF_KEY_REGION); + } else { + editor.putString(PREF_KEY_REGION, regionId); + } + editor.apply(); + getActivity().getSystemService(AlarmManager.class).setTimeZone(tzId); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + menu.add(0, MENU_BY_REGION, 0, R.string.zone_menu_by_region); + menu.add(0, MENU_BY_OFFSET, 0, R.string.zone_menu_by_offset); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + // Do not show menu when data is not ready, + menu.findItem(MENU_BY_REGION).setVisible(mTimeZoneData != null && !mSelectByRegion); + menu.findItem(MENU_BY_OFFSET).setVisible(mTimeZoneData != null && mSelectByRegion); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case MENU_BY_REGION: + setSelectByRegion(true); + return true; + + case MENU_BY_OFFSET: + setSelectByRegion(false); + return true; + + default: + return false; + } + } + + private void setupForCurrentTimeZone() { + mSelectedTimeZoneId = TimeZone.getDefault().getID(); + setSelectByRegion(!isFixedOffset(mSelectedTimeZoneId)); + } + + private static boolean isFixedOffset(String tzId) { + return tzId.startsWith("Etc/GMT") || tzId.equals("Etc/UTC"); + } + + /** + * Switch the current view to select region or select fixed offset time zone. + * When showing the selected region, it guess the selected region from time zone id. + * See {@link #findRegionIdForTzId} for more info. + */ + private void setSelectByRegion(boolean selectByRegion) { + mSelectByRegion = selectByRegion; + setPreferenceCategoryVisible((PreferenceCategory) findPreference( + PREF_KEY_REGION_CATEGORY), selectByRegion); + setPreferenceCategoryVisible((PreferenceCategory) findPreference( + PREF_KEY_FIXED_OFFSET_CATEGORY), !selectByRegion); + final String localeRegionId = getLocaleRegionId(); + final Set allCountryIsoCodes = mTimeZoneData.getRegionIds(); + + String displayRegion = allCountryIsoCodes.contains(localeRegionId) ? localeRegionId : null; + setDisplayedRegion(displayRegion); + setDisplayedTimeZoneInfo(displayRegion, null); + + if (!mSelectByRegion) { + setDisplayedFixedOffsetTimeZoneInfo(mSelectedTimeZoneId); + return; + } + + String regionId = findRegionIdForTzId(mSelectedTimeZoneId); + if (regionId != null) { + setDisplayedRegion(regionId); + setDisplayedTimeZoneInfo(regionId, mSelectedTimeZoneId); + } + } + + /** + * Find the a region associated with the specified time zone, based on the time zone data. + * If there are multiple regions associated with the given time zone, the priority will be given + * to the region the user last picked and the country in user's locale. + * @return null if no region associated with the time zone + */ + private String findRegionIdForTzId(String tzId) { + return findRegionIdForTzId(tzId, + getPreferenceManager().getSharedPreferences().getString(PREF_KEY_REGION, null), + getLocaleRegionId()); + } + + @VisibleForTesting + String findRegionIdForTzId(String tzId, String sharePrefRegionId, String localeRegionId) { + final Set matchedRegions = mTimeZoneData.lookupCountryCodesForZoneId(tzId); + if (matchedRegions.size() == 0) { + return null; + } + if (sharePrefRegionId != null && matchedRegions.contains(sharePrefRegionId)) { + return sharePrefRegionId; + } + if (localeRegionId != null && matchedRegions.contains(localeRegionId)) { + return localeRegionId; + } + + return matchedRegions.toArray(new String[matchedRegions.size()])[0]; + } + + private void setPreferenceCategoryVisible(PreferenceCategory category, + boolean isVisible) { + // Hiding category doesn't hide all the children preference. Set visibility of its children. + // Do not care grandchildren as time_zone_pref.xml has only 2 levels. + category.setVisible(isVisible); + for (int i = 0; i < category.getPreferenceCount(); i++) { + category.getPreference(i).setVisible(isVisible); + } + } + + private String getLocaleRegionId() { + return mLocale.getCountry().toUpperCase(Locale.US); + } +} diff --git a/tests/robotests/assets/grandfather_not_implementing_index_provider b/tests/robotests/assets/grandfather_not_implementing_index_provider index 43697bdd529..223d8f8d6df 100644 --- a/tests/robotests/assets/grandfather_not_implementing_index_provider +++ b/tests/robotests/assets/grandfather_not_implementing_index_provider @@ -24,3 +24,4 @@ com.android.settings.wifi.SavedAccessPointsWifiSettings com.android.settings.notification.ZenModeEventRuleSettings com.android.settings.notification.ZenModeScheduleRuleSettings com.android.settings.fuelgauge.RestrictedAppDetails +com.android.settings.datetime.timezone.TimeZoneSettings diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZonePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZonePreferenceControllerTest.java new file mode 100644 index 00000000000..49c468edef0 --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZonePreferenceControllerTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datetime.timezone; + +import android.app.Activity; +import android.content.Context; +import android.support.v7.preference.Preference; + +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RuntimeEnvironment; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(SettingsRobolectricTestRunner.class) +public class BaseTimeZonePreferenceControllerTest { + + private Activity mActivity; + + @Before + public void setUp() { + mActivity = Robolectric.setupActivity(Activity.class); + } + + @Test + public void handlePreferenceTreeClick_correctKey_triggerOnClickListener() { + String prefKey = "key1"; + TestClickListener clickListener = new TestClickListener(); + TestPreference preference = new TestPreference(mActivity, prefKey); + TestPreferenceController controller = new TestPreferenceController(mActivity, prefKey); + controller.setOnClickListener(clickListener); + + controller.handlePreferenceTreeClick(preference); + assertThat(clickListener.isClicked()).isTrue(); + } + + @Test + public void handlePreferenceTreeClick_wrongKey_triggerOnClickListener() { + String prefKey = "key1"; + TestClickListener clickListener = new TestClickListener(); + TestPreference preference = new TestPreference(mActivity, "wrong_key"); + TestPreferenceController controller = new TestPreferenceController(mActivity, prefKey); + controller.setOnClickListener(clickListener); + + controller.handlePreferenceTreeClick(preference); + assertThat(clickListener.isClicked()).isFalse(); + } + + private static class TestPreferenceController extends BaseTimeZonePreferenceController { + + private final Preference mTestPreference; + + public TestPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mTestPreference = new Preference(context); + mTestPreference.setKey(preferenceKey); + } + } + + private static class TestPreference extends Preference { + public TestPreference(Context context, String preferenceKey) { + super(context); + setKey(preferenceKey); + } + } + + private static class TestClickListener implements OnPreferenceClickListener { + + private boolean isClicked = false; + + @Override + public void onClick() { + isClicked = true; + } + + public boolean isClicked() { + return isClicked; + } + + } +} diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/FixedOffsetPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/FixedOffsetPreferenceControllerTest.java new file mode 100644 index 00000000000..0ffb7d2c45d --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/timezone/FixedOffsetPreferenceControllerTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datetime.timezone; + +import android.app.Activity; +import android.icu.util.TimeZone; +import android.support.v7.preference.Preference; + +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(SettingsRobolectricTestRunner.class) +public class FixedOffsetPreferenceControllerTest { + + private Activity mActivity; + + @Before + public void setUp() { + mActivity = Robolectric.setupActivity(Activity.class); + } + + @Test + public void updateState_matchTimeZoneSummary() { + TimeZoneInfo fixedOffsetZone = new TimeZoneInfo.Builder( + TimeZone.getFrozenTimeZone("Etc/GMT-8")) + .setExemplarLocation("Los Angeles") + .setGmtOffset("GMT-08:00") + .setItemId(0) + .build(); + Preference preference = new Preference(mActivity); + FixedOffsetPreferenceController controller = new FixedOffsetPreferenceController(mActivity); + controller.setTimeZoneInfo(fixedOffsetZone); + controller.updateState(preference); + assertThat(preference.getSummary()).isEqualTo("GMT-08:00"); + + } +} diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/RegionPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/RegionPreferenceControllerTest.java new file mode 100644 index 00000000000..7a8f267c178 --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/timezone/RegionPreferenceControllerTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datetime.timezone; + +import android.app.Activity; +import android.support.v7.preference.Preference; + +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(SettingsRobolectricTestRunner.class) +public class RegionPreferenceControllerTest { + + private Activity mActivity; + + @Before + public void setUp() { + mActivity = Robolectric.setupActivity(Activity.class); + } + + @Test + public void updateState_matchCountryName() { + Preference preference = new Preference(mActivity); + RegionPreferenceController controller = new RegionPreferenceController(mActivity); + controller.setRegionId("US"); + controller.updateState(preference); + assertThat(controller.getSummary()).isEqualTo("United States"); + assertThat(preference.getSummary()).isEqualTo("United States"); + + } +} diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/RegionZonePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/RegionZonePreferenceControllerTest.java new file mode 100644 index 00000000000..b39641fc1a7 --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/timezone/RegionZonePreferenceControllerTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datetime.timezone; + +import android.app.Activity; +import android.icu.util.TimeZone; +import android.support.v7.preference.Preference; + +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; + +import static com.google.common.truth.Truth.assertThat; + +@RunWith(SettingsRobolectricTestRunner.class) +public class RegionZonePreferenceControllerTest { + + private Activity mActivity; + + @Before + public void setUp() { + mActivity = Robolectric.setupActivity(Activity.class); + } + + @Test + public void updateState_matchTimeZoneName() { + TimeZoneInfo tzInfo = new TimeZoneInfo.Builder( + TimeZone.getFrozenTimeZone("America/Los_Angeles")) + .setGenericName("Pacific Time") + .setStandardName("Pacific Standard Time") + .setDaylightName("Pacific Daylight Time") + .setExemplarLocation("Los Angeles") + .setGmtOffset("GMT-08:00") + .setItemId(0) + .build(); + Preference preference = new Preference(mActivity); + RegionZonePreferenceController controller = new RegionZonePreferenceController(mActivity); + controller.setTimeZoneInfo(tzInfo); + controller.setClickable(false); + controller.updateState(preference); + String expectedSummary = "Los Angeles (GMT-08:00)"; + assertThat(controller.getSummary().toString()).isEqualTo(expectedSummary); + assertThat(preference.getSummary().toString()).isEqualTo(expectedSummary); + assertThat(preference.isEnabled()).isFalse(); + + } +} diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/TimeZoneInfoPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/TimeZoneInfoPreferenceControllerTest.java new file mode 100644 index 00000000000..2a587704e42 --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/timezone/TimeZoneInfoPreferenceControllerTest.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datetime.timezone; + +import android.support.v7.preference.Preference; + +import com.android.settings.datetime.timezone.TimeZoneInfo.Formatter; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; + +import java.util.Date; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.spy; + +@RunWith(SettingsRobolectricTestRunner.class) +public class TimeZoneInfoPreferenceControllerTest { + + @Test + public void updateState_matchExpectedFormattedText() { + Date now = new Date(0L); // 00:00 1/1/1970 + Formatter formatter = new Formatter(Locale.US, now); + + TimeZoneInfo timeZoneInfo = formatter.format("America/Los_Angeles"); + TimeZoneInfoPreferenceController controller = + new TimeZoneInfoPreferenceController(RuntimeEnvironment.application, now); + controller.setTimeZoneInfo(timeZoneInfo); + Preference preference = spy(new Preference(RuntimeEnvironment.application)); + controller.updateState(preference); + assertEquals("Uses Pacific Time (GMT-08:00). " + + "Pacific Daylight Time starts on April 26, 1970.", + preference.getTitle().toString()); + } +} diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/TimeZoneSettingsTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/TimeZoneSettingsTest.java new file mode 100644 index 00000000000..21ca30d7464 --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/timezone/TimeZoneSettingsTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datetime.timezone; + +import com.android.settings.datetime.timezone.model.TimeZoneData; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.testutils.shadow.SettingsShadowResources; +import com.android.settingslib.core.AbstractPreferenceController; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(shadows = { + SettingsShadowResources.class, + SettingsShadowResources.SettingsShadowTheme.class, + }) +public class TimeZoneSettingsTest { + + @Test + public void findRegionIdForTzId_matchExpectedCountry() { + String tzId = "Unknown/Secret_City"; + TimeZoneData timeZoneData = mock(TimeZoneData.class); + when(timeZoneData.lookupCountryCodesForZoneId(tzId)) + .thenReturn(new HashSet<>(Arrays.asList("US", "GB"))); + + TimeZoneSettings settings = new TimeZoneSettings(); + settings.setTimeZoneData(timeZoneData); + assertThat(settings.findRegionIdForTzId(tzId, null, "")).matches("US|GB"); + assertThat(settings.findRegionIdForTzId(tzId, "GB", "")).isEqualTo("GB"); + assertThat(settings.findRegionIdForTzId(tzId, null, "GB")).isEqualTo("GB"); + } + + @Test + public void createPreferenceControllers_matchExpectedControllers() { + TimeZoneSettings settings = new TimeZoneSettings(); + List controllers = + settings.createPreferenceControllers(RuntimeEnvironment.application); + assertThat(controllers).hasSize(4); + assertThat(controllers.get(0)).isInstanceOf(RegionPreferenceController.class); + assertThat(controllers.get(1)).isInstanceOf(RegionZonePreferenceController.class); + assertThat(controllers.get(2)).isInstanceOf(TimeZoneInfoPreferenceController.class); + assertThat(controllers.get(3)).isInstanceOf(FixedOffsetPreferenceController.class); + } +}