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 extends BaseTimeZonePicker> 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);
+ }
+}