New manual time zone picker.

This implements a new manual time zone picker that allows selection of a
time zone for a selected country. It also allows selecting a fixed
offset time zone (most importantly Etc/UTC, which is a frequently
requested feature).

The new time zone picker is currently behind a feature flag
(settings_zone_picker_v2), which is disabled by default.

Test: manual
Test: SettingsFunctionalTests
Test: SettingsRobotTests
Bug: 62255208
Change-Id: I89c5a04bcb562b6facf5f31a8aa4ad1cdd51ab10
diff --git a/res/layout/time_zone_list.xml b/res/layout/time_zone_list.xml
new file mode 100644
index 0000000..a3c47cd
--- /dev/null
+++ b/res/layout/time_zone_list.xml
@@ -0,0 +1,44 @@
+<!--
+    Copyright (C) 2017 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:id="@+id/tz_region_spinner_layout"
+        android:layout_width="match_parent"
+        android:layout_height="?android:attr/actionBarSize"
+        android:background="?android:attr/colorAccent"
+        android:gravity="center_vertical"
+        android:paddingEnd="@dimen/switchbar_subsettings_margin_end"
+        android:orientation="horizontal">
+
+        <Spinner
+            android:id="@+id/tz_region_spinner"
+            android:layout_height="wrap_content"
+            android:layout_width="0dp"
+            android:paddingStart="64dp"
+            android:layout_weight="1"
+            android:background="@drawable/app_filter_spinner_background"/>
+    </LinearLayout>
+
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/tz_list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+</LinearLayout>
diff --git a/res/layout/time_zone_list_item.xml b/res/layout/time_zone_list_item.xml
new file mode 100644
index 0000000..471c9d8
--- /dev/null
+++ b/res/layout/time_zone_list_item.xml
@@ -0,0 +1,62 @@
+<!--
+    Copyright (C) 2017 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?android:attr/selectableItemBackground"
+    android:gravity="center_vertical"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:orientation="vertical"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+    <TextView
+        android:id="@+id/tz_item_name"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="TimeZone name"
+        android:textAppearance="?android:attr/textAppearanceListItem" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/tz_item_details"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+            android:textColor="?android:attr/textColorSecondary" />
+
+        <TextView
+            android:id="@+id/tz_item_time"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAlignment="viewEnd"
+            android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+            android:textColor="?android:attr/textColorSecondary" />
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/tz_item_dst"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+        android:textColor="?android:attr/textColorSecondary" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 8c7b6f4..3708a3e 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -752,6 +752,17 @@
     <string name="zone_list_menu_sort_alphabetically">Sort alphabetically</string>
     <!-- Menu item on Select time zone screen -->
     <string name="zone_list_menu_sort_by_timezone">Sort by time zone</string>
+    <!-- Label describing when a given time zone changes to DST or standard time -->
+    <string name="zone_change_to_from_dst"><xliff:g id="time_type" example="Pacific Summer Time">%1$s</xliff:g> starts on <xliff:g id="transition_date" example="Mar 11 2018">%2$s</xliff:g>.</string>
+    <!-- Describes the time type "daylight savings time" (used in zone_change_to_from_dst, when no zone specific name is available) -->
+    <string name="zone_time_type_dst">Daylight savings time</string>
+    <!-- Describes the time type "standard time" (used in zone_change_to_from_dst, when no zone specific name is available) -->
+    <string name="zone_time_type_standard">Standard time</string>
+    <!-- The menu item to switch to selecting a time zone by region (default) -->
+    <string name="zone_menu_by_region">Time zone by region</string>
+    <!-- The menu item to switch to selecting a time zone with a fixed offset (such as UTC or GMT+0200) -->
+    <string name="zone_menu_by_offset">Fixed offset time zones</string>
+
     <!-- Title string shown above DatePicker, letting a user select system date
          [CHAR LIMIT=20] -->
     <string name="date_picker_title">Date</string>
diff --git a/src/com/android/settings/core/FeatureFlags.java b/src/com/android/settings/core/FeatureFlags.java
index e88fb11..7d9b331 100644
--- a/src/com/android/settings/core/FeatureFlags.java
+++ b/src/com/android/settings/core/FeatureFlags.java
@@ -26,4 +26,5 @@
     public static final String BATTERY_SETTINGS_V2 = "settings_battery_v2";
     public static final String BATTERY_DISPLAY_APP_LIST = "settings_battery_display_app_list";
     public static final String SECURITY_SETTINGS_V2 = "settings_security_settings_v2";
+    public static final String ZONE_PICKER_V2 = "settings_zone_picker_v2";
 }
diff --git a/src/com/android/settings/datetime/TimeZonePreferenceController.java b/src/com/android/settings/datetime/TimeZonePreferenceController.java
index 435b1fe..e29e245 100644
--- a/src/com/android/settings/datetime/TimeZonePreferenceController.java
+++ b/src/com/android/settings/datetime/TimeZonePreferenceController.java
@@ -20,7 +20,10 @@
 import android.support.annotation.VisibleForTesting;
 import android.support.v7.preference.Preference;
 
+import android.util.FeatureFlagUtils;
+import com.android.settings.core.FeatureFlags;
 import com.android.settings.core.PreferenceControllerMixin;
+import com.android.settings.datetime.timezone.ZonePicker;
 import com.android.settingslib.RestrictedPreference;
 import com.android.settingslib.core.AbstractPreferenceController;
 import com.android.settingslib.datetime.ZoneGetter;
@@ -33,11 +36,13 @@
     private static final String KEY_TIMEZONE = "timezone";
 
     private final AutoTimeZonePreferenceController mAutoTimeZonePreferenceController;
+    private final boolean mZonePickerV2;
 
     public TimeZonePreferenceController(Context context,
             AutoTimeZonePreferenceController autoTimeZonePreferenceController) {
         super(context);
         mAutoTimeZonePreferenceController = autoTimeZonePreferenceController;
+        mZonePickerV2 = FeatureFlagUtils.isEnabled(mContext, FeatureFlags.ZONE_PICKER_V2);
     }
 
     @Override
@@ -45,6 +50,9 @@
         if (!(preference instanceof RestrictedPreference)) {
             return;
         }
+        if (mZonePickerV2) {
+            preference.setFragment(ZonePicker.class.getName());
+        }
         preference.setSummary(getTimeZoneOffsetAndName());
         if( !((RestrictedPreference) preference).isDisabledByAdmin()) {
             preference.setEnabled(!mAutoTimeZonePreferenceController.isEnabled());
diff --git a/src/com/android/settings/datetime/timezone/TimeZoneAdapter.java b/src/com/android/settings/datetime/timezone/TimeZoneAdapter.java
new file mode 100644
index 0000000..79075ca
--- /dev/null
+++ b/src/com/android/settings/datetime/timezone/TimeZoneAdapter.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2017 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.graphics.Typeface;
+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.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.settings.R;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Adapter for showing {@link TimeZoneInfo} objects in a recycler view.
+ */
+class TimeZoneAdapter extends RecyclerView.Adapter {
+
+    static final int VIEW_TYPE_NORMAL = 1;
+    static final int VIEW_TYPE_SELECTED = 2;
+
+    private final DateFormat mTimeFormat;
+    private final DateFormat mDateFormat;
+    private final View.OnClickListener mOnClickListener;
+    private final Context mContext;
+    private final String mCurrentTimeZone;
+
+    private List<TimeZoneInfo> mTimeZoneInfos;
+
+    TimeZoneAdapter(View.OnClickListener onClickListener, Context context) {
+        mOnClickListener = onClickListener;
+        mContext = context;
+        mTimeFormat = DateFormat.getTimeInstance(SimpleDateFormat.SHORT);
+        mDateFormat = DateFormat.getDateInstance(SimpleDateFormat.MEDIUM);
+        mDateFormat.setContext(DisplayContext.CAPITALIZATION_NONE);
+        mCurrentTimeZone = TimeZone.getDefault().getID();
+        setHasStableIds(true);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return getItem(position).getItemId();
+    }
+
+    @NonNull
+    @Override
+    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        final View view = LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.time_zone_list_item, parent, false);
+        view.setOnClickListener(mOnClickListener);
+        final ViewHolder viewHolder = new ViewHolder(view);
+        if (viewType == VIEW_TYPE_SELECTED) {
+            viewHolder.mNameView.setTypeface(
+                    viewHolder.mNameView.getTypeface(), Typeface.BOLD);
+        }
+        return viewHolder;
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
+        final TimeZoneInfo item = getItem(position);
+        final ViewHolder tzHolder = (ViewHolder) holder;
+        tzHolder.mNameView.setText(formatName(item));
+        tzHolder.mDetailsView.setText(formatDetails(item));
+        tzHolder.mTimeView.setText(formatTime(item));
+        String dstText = formatDstText(item);
+        tzHolder.mDstView.setText(dstText);
+        // Hide DST TextView when it has no content.
+        tzHolder.mDstView.setVisibility(dstText != null ? View.VISIBLE : View.GONE);
+
+    }
+
+    @Override
+    public int getItemCount() {
+        return getTimeZones().size();
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        final TimeZoneInfo tz = getItem(position);
+        if (tz.getId().equals(mCurrentTimeZone)) {
+            return VIEW_TYPE_SELECTED;
+        } else {
+            return VIEW_TYPE_NORMAL;
+        }
+    }
+
+    public TimeZoneInfo getItem(int position) {
+        return getTimeZones().get(position);
+    }
+
+    private CharSequence formatName(TimeZoneInfo item) {
+        CharSequence name = item.getExemplarLocation();
+        if (name == null) {
+            name = item.getGenericName();
+        }
+        if (name == null && item.getTimeZone().inDaylightTime(new Date())) {
+            name = item.getDaylightName();
+        }
+        if (name == null) {
+            name = item.getStandardName();
+        }
+        if (name == null) {
+            name = item.getGmtOffset();
+        }
+        return name;
+    }
+
+    private CharSequence formatDetails(TimeZoneInfo item) {
+        String name = item.getGenericName();
+        if (name == null) {
+            if (item.getTimeZone().inDaylightTime(new Date())) {
+                name = item.getDaylightName();
+            } else {
+                name = item.getStandardName();
+            }
+        }
+        if (name == null) {
+            return item.getGmtOffset();
+        } else {
+            return TextUtils.concat(item.getGmtOffset(), " ", name);
+        }
+    }
+
+    private String formatDstText(TimeZoneInfo item) {
+        final TimeZone timeZone = item.getTimeZone();
+        if (!timeZone.observesDaylightTime()) {
+            return null;
+        }
+
+        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 mContext.getString(R.string.zone_change_to_from_dst, timeType, date);
+    }
+
+    private TimeZoneTransition findNextDstTransition(TimeZone timeZone) {
+        if (!(timeZone instanceof OlsonTimeZone)) {
+            return null;
+        }
+        final OlsonTimeZone olsonTimeZone = (OlsonTimeZone) timeZone;
+        TimeZoneTransition transition = olsonTimeZone.getNextTransition(
+                System.currentTimeMillis(), /* inclusive */ false);
+        do {
+            if (transition.getTo().getDSTSavings() != transition.getFrom().getDSTSavings()) {
+                break;
+            }
+            transition = olsonTimeZone.getNextTransition(
+                    transition.getTime(), /*inclusive */ false);
+        } while (transition != null);
+        return transition;
+    }
+
+    private String formatTime(TimeZoneInfo item) {
+        return mTimeFormat.format(Calendar.getInstance(item.getTimeZone()));
+    }
+
+    private List<TimeZoneInfo> getTimeZones() {
+        if (mTimeZoneInfos == null) {
+            return Collections.emptyList();
+        }
+        return mTimeZoneInfos;
+    }
+
+    void setTimeZoneInfos(List<TimeZoneInfo> timeZoneInfos) {
+        mTimeZoneInfos = timeZoneInfos;
+        notifyDataSetChanged();
+    }
+}
diff --git a/src/com/android/settings/datetime/timezone/ViewHolder.java b/src/com/android/settings/datetime/timezone/ViewHolder.java
new file mode 100644
index 0000000..3cb2c4e
--- /dev/null
+++ b/src/com/android/settings/datetime/timezone/ViewHolder.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2017 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.widget.RecyclerView;
+import android.view.View;
+import android.widget.TextView;
+import com.android.settings.R;
+
+/**
+ * View holder for a time zone list item.
+ */
+class ViewHolder extends RecyclerView.ViewHolder {
+
+    final TextView mNameView;
+    final TextView mDstView;
+    final TextView mDetailsView;
+    final TextView mTimeView;
+
+    public ViewHolder(View itemView) {
+        super(itemView);
+        mNameView = itemView.findViewById(R.id.tz_item_name);
+        mDstView = itemView.findViewById(R.id.tz_item_dst);
+        mDetailsView = itemView.findViewById(R.id.tz_item_details);
+        mTimeView = itemView.findViewById(R.id.tz_item_time);
+    }
+}
diff --git a/src/com/android/settings/datetime/timezone/ZonePicker.java b/src/com/android/settings/datetime/timezone/ZonePicker.java
new file mode 100644
index 0000000..eafbaa2
--- /dev/null
+++ b/src/com/android/settings/datetime/timezone/ZonePicker.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2017 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.icu.util.TimeZone;
+import android.os.Bundle;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.settings.R;
+import com.android.settings.core.InstrumentedFragment;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * The class displaying a region list and a list of time zones for the selected region.
+ * Choosing an item from the list will set the time zone. Pressing Back without choosing from the
+ * list will not result in a change in the time zone setting.
+ */
+public class ZonePicker extends InstrumentedFragment
+    implements AdapterView.OnItemSelectedListener, View.OnClickListener {
+
+    private static final int MENU_BY_REGION = Menu.FIRST;
+    private static final int MENU_BY_OFFSET = Menu.FIRST + 1;
+
+    private Locale mLocale;
+    private List<RegionInfo> mRegions;
+    private Map<String, List<TimeZoneInfo>> mZoneInfos;
+    private List<TimeZoneInfo> mFixedOffsetTimeZones;
+    private TimeZoneAdapter mTimeZoneAdapter;
+    private String mSelectedTimeZone;
+    private boolean mSelectByRegion;
+    private DataLoader mDataLoader;
+    private RecyclerView mRecyclerView;
+
+    @Override
+    public int getMetricsCategory() {
+        return MetricsProto.MetricsEvent.ZONE_PICKER;
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+        final View view = inflater.inflate(R.layout.time_zone_list, container, false);
+
+        mLocale = getContext().getResources().getConfiguration().locale;
+        mDataLoader = new DataLoader(mLocale);
+        // TOOD: move this off the UI thread.
+        mRegions = mDataLoader.loadRegionInfos();
+        mZoneInfos = new HashMap<>();
+        mSelectByRegion = true;
+        mSelectedTimeZone = TimeZone.getDefault().getID();
+
+        mTimeZoneAdapter = new TimeZoneAdapter(this, getContext());
+        mRecyclerView = view.findViewById(R.id.tz_list);
+        mRecyclerView.setAdapter(mTimeZoneAdapter);
+        mRecyclerView.setLayoutManager(
+                new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, /* reverseLayout */ false));
+
+        final ArrayAdapter<RegionInfo> regionAdapter = new ArrayAdapter<>(getContext(),
+                R.layout.filter_spinner_item, mRegions);
+        regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        final Spinner spinner = view.findViewById(R.id.tz_region_spinner);
+        spinner.setAdapter(regionAdapter);
+        spinner.setOnItemSelectedListener(this);
+        setupForCurrentTimeZone(spinner);
+        setHasOptionsMenu(true);
+        return view;
+    }
+
+    private void setupForCurrentTimeZone(Spinner spinner) {
+        final String localeRegionId = mLocale.getCountry().toUpperCase(Locale.ROOT);
+        final String currentTimeZone = TimeZone.getDefault().getID();
+        boolean fixedOffset = currentTimeZone.startsWith("Etc/GMT") ||
+            currentTimeZone.equals("Etc/UTC");
+
+        for (int regionIndex = 0; regionIndex < mRegions.size(); regionIndex++) {
+            final RegionInfo region = mRegions.get(regionIndex);
+            if (localeRegionId.equals(region.getId())) {
+                spinner.setSelection(regionIndex);
+            }
+            if (!fixedOffset) {
+                for (String timeZoneId: region.getTimeZoneIds()) {
+                    if (TextUtils.equals(timeZoneId, mSelectedTimeZone)) {
+                        spinner.setSelection(regionIndex);
+                        return;
+                    }
+                }
+            }
+        }
+
+        if (fixedOffset) {
+            setSelectByRegion(false);
+        }
+    }
+
+    @Override
+    public void onClick(View view) {
+        // Ignore extra clicks
+        if (!isResumed()) {
+            return;
+        }
+        final int position = mRecyclerView.getChildAdapterPosition(view);
+        if (position == RecyclerView.NO_POSITION) {
+            return;
+        }
+        final TimeZoneInfo timeZoneInfo = mTimeZoneAdapter.getItem(position);
+
+        // Update the system timezone value
+        final Activity activity = getActivity();
+        final AlarmManager alarm = (AlarmManager) activity.getSystemService(Context.ALARM_SERVICE);
+        alarm.setTimeZone(timeZoneInfo.getId());
+
+        activity.onBackPressed();
+    }
+
+    @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) {
+        if (mSelectByRegion) {
+            menu.findItem(MENU_BY_REGION).setVisible(false);
+            menu.findItem(MENU_BY_OFFSET).setVisible(true);
+        } else {
+            menu.findItem(MENU_BY_REGION).setVisible(true);
+            menu.findItem(MENU_BY_OFFSET).setVisible(false);
+        }
+    }
+
+    @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 setSelectByRegion(boolean selectByRegion) {
+        mSelectByRegion = selectByRegion;
+        getView().findViewById(R.id.tz_region_spinner_layout).setVisibility(
+            mSelectByRegion ? View.VISIBLE : View.GONE);
+        List<TimeZoneInfo> tzInfos;
+        if (selectByRegion) {
+            Spinner regionSpinner = getView().findViewById(R.id.tz_region_spinner);
+            int selectedRegion = regionSpinner.getSelectedItemPosition();
+            if (selectedRegion == -1) {
+                // Arbitrarily pick the first item if no region was selected above.
+                selectedRegion = 0;
+                regionSpinner.setSelection(selectedRegion);
+            }
+            tzInfos = getTimeZoneInfos(mRegions.get(selectedRegion));
+        } else {
+            if (mFixedOffsetTimeZones == null) {
+                mFixedOffsetTimeZones = mDataLoader.loadFixedOffsets();
+            }
+            tzInfos = mFixedOffsetTimeZones;
+        }
+        mTimeZoneAdapter.setTimeZoneInfos(tzInfos);
+    }
+
+    private List<TimeZoneInfo> getTimeZoneInfos(RegionInfo regionInfo) {
+        List<TimeZoneInfo> tzInfos = mZoneInfos.get(regionInfo.getId());
+        if (tzInfos == null) {
+            // TODO: move this off the UI thread.
+            Collection<String> tzIds = regionInfo.getTimeZoneIds();
+            tzInfos = mDataLoader.loadTimeZoneInfos(tzIds);
+            mZoneInfos.put(regionInfo.getId(), tzInfos);
+        }
+        return tzInfos;
+    }
+
+    @Override
+    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+        mTimeZoneAdapter.setTimeZoneInfos(getTimeZoneInfos(mRegions.get(position)));
+    }
+
+    @Override
+    public void onNothingSelected(AdapterView<?> parent) {
+        mTimeZoneAdapter.setTimeZoneInfos(null);
+    }
+
+}
diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/TimeZoneAdapterTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/TimeZoneAdapterTest.java
new file mode 100644
index 0000000..5f29a0b
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/datetime/timezone/TimeZoneAdapterTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2017 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.icu.util.TimeZone;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.FrameLayout;
+import com.android.settings.TestConfig;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+import com.android.settings.testutils.shadow.SettingsShadowResources;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+import java.util.Collections;
+
+import static com.google.common.truth.Truth.assertThat;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION,
+        shadows = {
+                SettingsShadowResources.class,
+                SettingsShadowResources.SettingsShadowTheme.class})
+public class TimeZoneAdapterTest {
+    @Mock
+    private View.OnClickListener mOnClickListener;
+
+    private TimeZoneAdapter mTimeZoneAdapter;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mTimeZoneAdapter = new TimeZoneAdapter(mOnClickListener, RuntimeEnvironment.application);
+    }
+
+    @Test
+    public void getItemViewType_onDefaultTimeZone_returnsTypeSelected() {
+        final TimeZoneInfo tzi = dummyTimeZoneInfo(TimeZone.getDefault());
+        mTimeZoneAdapter.setTimeZoneInfos(Collections.singletonList(tzi));
+        assertThat(mTimeZoneAdapter.getItemViewType(0)).isEqualTo(TimeZoneAdapter.VIEW_TYPE_SELECTED);
+    }
+
+    @Test
+    public void getItemViewType_onNonDefaultTimeZone_returnsTypeNormal() {
+        final TimeZoneInfo tzi = dummyTimeZoneInfo(getNonDefaultTimeZone());
+        mTimeZoneAdapter.setTimeZoneInfos(Collections.singletonList(tzi));
+        assertThat(mTimeZoneAdapter.getItemViewType(0)).isEqualTo(TimeZoneAdapter.VIEW_TYPE_NORMAL);
+    }
+
+    @Test
+    public void bindViewHolder_onDstTimeZone_showsDstLabel() {
+        final TimeZoneInfo tzi = dummyTimeZoneInfo(TimeZone.getTimeZone("America/Los_Angeles"));
+        mTimeZoneAdapter.setTimeZoneInfos(Collections.singletonList(tzi));
+
+        final FrameLayout parent = new FrameLayout(RuntimeEnvironment.application);
+
+        final ViewHolder viewHolder = (ViewHolder) mTimeZoneAdapter.createViewHolder(parent, TimeZoneAdapter.VIEW_TYPE_NORMAL);
+        mTimeZoneAdapter.bindViewHolder(viewHolder, 0);
+        assertThat(viewHolder.mDstView.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void bindViewHolder_onNonDstTimeZone_hidesDstLabel() {
+        final TimeZoneInfo tzi = dummyTimeZoneInfo(TimeZone.getTimeZone("Etc/UTC"));
+        mTimeZoneAdapter.setTimeZoneInfos(Collections.singletonList(tzi));
+
+        final FrameLayout parent = new FrameLayout(RuntimeEnvironment.application);
+
+        final ViewHolder viewHolder = (ViewHolder) mTimeZoneAdapter.createViewHolder(parent, TimeZoneAdapter.VIEW_TYPE_NORMAL);
+        mTimeZoneAdapter.bindViewHolder(viewHolder, 0);
+        assertThat(viewHolder.mDstView.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    // Pick an arbitrary time zone that's not the current default.
+    private static TimeZone getNonDefaultTimeZone() {
+        final String[] availableIDs = TimeZone.getAvailableIDs();
+        int index = 0;
+        if (TextUtils.equals(availableIDs[index], TimeZone.getDefault().getID())) {
+            index++;
+        }
+        return TimeZone.getTimeZone(availableIDs[index]);
+    }
+
+    private TimeZoneInfo dummyTimeZoneInfo(TimeZone timeZone) {
+        return new TimeZoneInfo.Builder(timeZone).setGmtOffset("GMT+0").setItemId(1).build();
+    }
+}