OSDN Git Service

Time zone, Region, UTC picker
authorVictor Chang <vichang@google.com>
Wed, 28 Feb 2018 19:31:31 +0000 (19:31 +0000)
committerVictor Chang <vichang@google.com>
Tue, 6 Mar 2018 11:13:59 +0000 (11:13 +0000)
- Extract most common view related codes into BaseTimeZoneAdapter
  and BaseTimeZonePicker. Subclass handles the text formatting and
  order.
- Search view is added compared to previous version of time
  zone picker
- SpannableUtil is added to preserve spannable when formatting
  String resource.
- Fix the bug using GMT+<arabic> as time zone id. b/73132985
- Fix Talkback treating flags on screens as a separate element

Bug: 72146259
Bug: 73132985
Bug: 73952488
Test: mm RunSettingsRoboTests
Change-Id: I42c6ac369199c09d11e7f5cc4707358fa4780fed
(cherry picked from commit fbd30acef09d1db6aaf767c2bcbb17787fd08ba1)

17 files changed:
res/layout/time_zone_search_item.xml [new file with mode: 0644]
res/menu/time_zone_base_search_menu.xml [new file with mode: 0644]
res/values/strings.xml
src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java [new file with mode: 0644]
src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPicker.java [new file with mode: 0644]
src/com/android/settings/datetime/timezone/BaseTimeZonePicker.java [new file with mode: 0644]
src/com/android/settings/datetime/timezone/FixedOffsetPicker.java [new file with mode: 0644]
src/com/android/settings/datetime/timezone/RegionSearchPicker.java [new file with mode: 0644]
src/com/android/settings/datetime/timezone/RegionZonePicker.java [new file with mode: 0644]
src/com/android/settings/datetime/timezone/SpannableUtil.java [new file with mode: 0644]
src/com/android/settings/datetime/timezone/model/TimeZoneData.java
tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapterTest.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPickerTest.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/datetime/timezone/FixedOffsetPickerTest.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/datetime/timezone/RegionSearchPickerTest.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/datetime/timezone/RegionZonePickerTest.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/datetime/timezone/SpannableUtilTest.java [new file with mode: 0644]

diff --git a/res/layout/time_zone_search_item.xml b/res/layout/time_zone_search_item.xml
new file mode 100644 (file)
index 0000000..bb75226
--- /dev/null
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<!-- similar to preference_material.xml but textview for emoji country flag
+instead of an ImageView -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:minHeight="?android:attr/listPreferredItemHeightSmall"
+    android:gravity="center_vertical"
+    android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+    android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+    android:background="?android:attr/selectableItemBackground"
+    android:clipToPadding="false">
+
+    <LinearLayout
+        android:id="@+id/icon_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="-4dp"
+        android:minWidth="60dp"
+        android:gravity="start|center_vertical"
+        android:orientation="horizontal"
+        android:paddingRight="12dp"
+        android:paddingTop="4dp"
+        android:paddingBottom="4dp">
+        <!-- It's not ImageView because the icon is Unicode emoji. -->
+        <TextView
+            android:id="@+id/icon_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:contentDescription=""
+            android:textAppearance="?android:attr/textAppearanceLarge"
+            android:textColor="?android:attr/textColorPrimary"
+            android:importantForAccessibility="no"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:orientation="vertical"
+        android:paddingTop="16dp"
+        android:paddingBottom="16dp">
+
+        <TextView android:id="@android:id/title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:textAppearance="@style/Preference_TextAppearanceMaterialSubhead"
+            android:ellipsize="marquee" />
+
+        <RelativeLayout
+            android:id="@+id/summary_frame"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_below="@android:id/title"
+            android:layout_alignLeft="@android:id/title">
+
+            <TextView
+                android:id="@+id/current_time"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textAppearance="?android:attr/textAppearanceSmall"
+                android:textColor="?android:attr/textColorSecondary"
+                android:layout_alignParentEnd="true"/>
+
+            <!-- Use layout_alignParentStart and layout_toStartOf to make the TextView multi-lines
+                if needed -->
+            <TextView
+                android:id="@android:id/summary"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textAppearance="?android:attr/textAppearanceSmall"
+                android:textColor="?android:attr/textColorSecondary"
+                android:maxLines="10"
+                android:layout_alignParentStart="true"
+                android:layout_toStartOf="@+id/current_time"/>
+
+        </RelativeLayout>
+
+    </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/menu/time_zone_base_search_menu.xml b/res/menu/time_zone_base_search_menu.xml
new file mode 100644 (file)
index 0000000..92241af
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/time_zone_search_menu"
+        android:title="@string/search_settings"
+        android:icon="@*android:drawable/ic_search_api_material"
+        android:showAsAction="always|collapseActionView"
+        android:actionViewClass="android.widget.SearchView" />
+
+</menu>
\ No newline at end of file
index 4e81b7a..3452af0 100644 (file)
     <string name="date_time_set_date_title">Date</string>
     <!-- Date & time setting screen setting option title -->
     <string name="date_time_set_date">Set date</string>
+    <!-- Setting option title to select region in time zone setting screen [CHAR LIMIT=30] -->
+    <string name="date_time_select_region">Region</string>
+    <!-- Setting option title to select time zone in time zone setting screen [CHAR LIMIT=30]-->
+    <string name="date_time_select_zone">Time Zone</string>
+    <!-- Setting option title in time zone setting screen [CHAR LIMIT=30] -->
+    <string name="date_time_select_fixed_offset_time_zones">Select UTC offset</string>
     <!-- Menu item on Select time zone screen -->
     <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>
+    <!-- Label describing a exemplar location and time zone offset[CHAR LIMIT=NONE] -->
+    <string name="zone_info_exemplar_location_and_offset"><xliff:g id="exemplar_location" example="Los Angeles">%1$s</xliff:g> (<xliff:g id="offset" example="GMT-08:00">%2$s</xliff:g>)</string>
+    <!-- Label describing a time zone offset and name[CHAR LIMIT=NONE] -->
+    <string name="zone_info_offset_and_name"><xliff:g id="time_type" example="Pacific Time">%2$s</xliff:g> (<xliff:g id="offset" example="GMT-08:00">%1$s</xliff:g>)</string>
+    <!-- Label describing a time zone and changes to DST or standard time [CHAR LIMIT=NONE] -->
+    <string name="zone_info_footer">Uses <xliff:g id="offset_and_name" example="Pacific Time (GMT-08:00)">%1$s</xliff:g>. <xliff:g id="dst_time_type" example="Pacific Daylight Time">%2$s</xliff:g> starts on <xliff:g id="transition_date" example="Mar 11 2018">%3$s</xliff:g>.</string>
+    <!-- Label describing a time zone without DST [CHAR LIMIT=NONE] -->
+    <string name="zone_info_footer_no_dst">Uses <xliff:g id="offset_and_name" example="GMT-08:00 Pacific Time">%1$s</xliff:g>. No daylight savings time.</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>
+    <string name="zone_menu_by_region">Show time zones 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>
+    <string name="zone_menu_by_offset">Show time zones by UTC offset</string>
 
     <!-- Title string shown above DatePicker, letting a user select system date
          [CHAR LIMIT=20] -->
diff --git a/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java b/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java
new file mode 100644 (file)
index 0000000..effa948
--- /dev/null
@@ -0,0 +1,206 @@
+/*
+ * 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.icu.text.BreakIterator;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Filter;
+import android.widget.TextView;
+
+import com.android.settings.R;
+import com.android.settings.datetime.timezone.BaseTimeZonePicker.OnListItemClickListener;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Used with {@class BaseTimeZonePicker}. It renders text in each item into list view. A list of
+ * {@class AdapterItem} must be provided when an instance is created.
+ */
+public class BaseTimeZoneAdapter<T extends BaseTimeZoneAdapter.AdapterItem>
+        extends RecyclerView.Adapter<BaseTimeZoneAdapter.ItemViewHolder> {
+
+    private final List<T> mOriginalItems;
+    private final OnListItemClickListener mOnListItemClickListener;
+    private final Locale mLocale;
+    private final boolean mShowItemSummary;
+
+    private List<T> mItems;
+    private ArrayFilter mFilter;
+
+    public BaseTimeZoneAdapter(List<T> items, OnListItemClickListener
+            onListItemClickListener, Locale locale, boolean showItemSummary) {
+        mOriginalItems = items;
+        mItems = items;
+        mOnListItemClickListener = onListItemClickListener;
+        mLocale = locale;
+        mShowItemSummary = showItemSummary;
+        setHasStableIds(true);
+    }
+
+    @NonNull
+    @Override
+    public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        final View view = LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.time_zone_search_item, parent, false);
+        return new ItemViewHolder(view, mOnListItemClickListener);
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) {
+        final AdapterItem item = mItems.get(position);
+        holder.mSummaryFrame.setVisibility(
+                mShowItemSummary ? View.VISIBLE : View.GONE);
+        holder.mTitleView.setText(item.getTitle());
+        holder.mIconTextView.setText(item.getIconText());
+        holder.mSummaryView.setText(item.getSummary());
+        holder.mTimeView.setText(item.getCurrentTime());
+        holder.setPosition(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return getItem(position).getItemId();
+    }
+
+    @Override
+    public int getItemCount() {
+        return mItems.size();
+    }
+
+    public  @NonNull
+    Filter getFilter() {
+        if (mFilter == null) {
+            mFilter = new ArrayFilter();
+        }
+        return mFilter;
+    }
+
+    public T getItem(int position) {
+        return mItems.get(position);
+    }
+
+    public interface AdapterItem {
+        CharSequence getTitle();
+        CharSequence getSummary();
+        String getIconText();
+        String getCurrentTime();
+        long getItemId();
+        String[] getSearchKeys();
+    }
+
+    public static class ItemViewHolder extends RecyclerView.ViewHolder
+            implements View.OnClickListener{
+
+        final OnListItemClickListener mOnListItemClickListener;
+        final View mSummaryFrame;
+        final TextView mTitleView;
+        final TextView mIconTextView;
+        final TextView mSummaryView;
+        final TextView mTimeView;
+        private int mPosition;
+
+        public ItemViewHolder(View itemView, OnListItemClickListener onListItemClickListener) {
+            super(itemView);
+            itemView.setOnClickListener(this);
+            mSummaryFrame = itemView.findViewById(R.id.summary_frame);
+            mTitleView = itemView.findViewById(android.R.id.title);
+            mIconTextView = itemView.findViewById(R.id.icon_text);
+            mSummaryView = itemView.findViewById(android.R.id.summary);
+            mTimeView = itemView.findViewById(R.id.current_time);
+            mOnListItemClickListener = onListItemClickListener;
+        }
+
+        public void setPosition(int position) {
+            mPosition = position;
+        }
+
+        @Override
+        public void onClick(View v) {
+            mOnListItemClickListener.onListItemClick(mPosition);
+        }
+    }
+
+    /**
+     * <p>An array filter constrains the content of the array adapter with
+     * a prefix. Each item that does not start with the supplied prefix
+     * is removed from the list.</p>
+     *
+     * The filtering operation is not optimized, due to small data size (~260 regions),
+     * require additional pre-processing. Potentially, a trie structure can be used to match
+     * prefixes of the search keys.
+     */
+    private class ArrayFilter extends Filter {
+
+        private BreakIterator mBreakIterator = BreakIterator.getWordInstance(mLocale);
+
+        @WorkerThread
+        @Override
+        protected FilterResults performFiltering(CharSequence prefix) {
+            final List<T> newItems;
+            if (TextUtils.isEmpty(prefix)) {
+                newItems = mOriginalItems;
+            } else {
+                final String prefixString = prefix.toString().toLowerCase(mLocale);
+                newItems = new ArrayList<>();
+
+                for (T item : mOriginalItems) {
+                    outer:
+                    for (String searchKey : item.getSearchKeys()) {
+                        searchKey = searchKey.toLowerCase(mLocale);
+                        // First match against the whole, non-splitted value
+                        if (searchKey.startsWith(prefixString)) {
+                            newItems.add(item);
+                            break outer;
+                        } else {
+                            mBreakIterator.setText(searchKey);
+                            for (int wordStart = 0, wordLimit = mBreakIterator.next();
+                                    wordLimit != BreakIterator.DONE;
+                                    wordStart = wordLimit,
+                                            wordLimit = mBreakIterator.next()) {
+                                if (mBreakIterator.getRuleStatus() != BreakIterator.WORD_NONE
+                                        && searchKey.startsWith(prefixString, wordStart)) {
+                                    newItems.add(item);
+                                    break outer;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            final FilterResults results = new FilterResults();
+            results.values = newItems;
+            results.count = newItems.size();
+
+            return results;
+        }
+
+        @Override
+        protected void publishResults(CharSequence constraint, FilterResults results) {
+            mItems = (List<T>) results.values;
+            notifyDataSetChanged();
+        }
+    }
+}
diff --git a/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPicker.java b/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPicker.java
new file mode 100644 (file)
index 0000000..b133582
--- /dev/null
@@ -0,0 +1,166 @@
+/*
+ * 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.content.Intent;
+import android.content.res.Resources;
+import android.icu.text.DateFormat;
+import android.icu.text.SimpleDateFormat;
+import android.icu.util.Calendar;
+
+import com.android.settings.R;
+import com.android.settings.datetime.timezone.model.TimeZoneData;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Render a list of {@class TimeZoneInfo} into the list view in {@class BaseTimeZonePicker}
+ */
+public abstract class BaseTimeZoneInfoPicker extends BaseTimeZonePicker {
+    protected static final String TAG = "RegionZoneSearchPicker";
+    protected ZoneAdapter mAdapter;
+
+    protected BaseTimeZoneInfoPicker(int titleResId, int searchHintResId,
+            boolean searchEnabled, boolean defaultExpandSearch) {
+        super(titleResId, searchHintResId, searchEnabled, defaultExpandSearch);
+    }
+
+    @Override
+    protected BaseTimeZoneAdapter createAdapter(TimeZoneData timeZoneData) {
+        mAdapter = new ZoneAdapter(getContext(), getAllTimeZoneInfos(timeZoneData),
+                this::onListItemClick, getLocale());
+        return mAdapter;
+    }
+
+    private void onListItemClick(int position) {
+        final TimeZoneInfo timeZoneInfo = mAdapter.getItem(position).mTimeZoneInfo;
+        getActivity().setResult(Activity.RESULT_OK, prepareResultData(timeZoneInfo));
+        getActivity().finish();
+    }
+
+    protected Intent prepareResultData(TimeZoneInfo selectedTimeZoneInfo) {
+        return new Intent().putExtra(EXTRA_RESULT_TIME_ZONE_ID, selectedTimeZoneInfo.getId());
+    }
+
+    public abstract List<TimeZoneInfo> getAllTimeZoneInfos(TimeZoneData timeZoneData);
+
+    protected static class ZoneAdapter extends BaseTimeZoneAdapter<TimeZoneInfoItem> {
+
+        public ZoneAdapter(Context context, List<TimeZoneInfo> timeZones,
+                OnListItemClickListener onListItemClickListener, Locale locale) {
+            super(createTimeZoneInfoItems(context, timeZones, locale),
+                    onListItemClickListener, locale,  true /* showItemSummary */);
+        }
+
+        private static List<TimeZoneInfoItem> createTimeZoneInfoItems(Context context,
+                List<TimeZoneInfo> timeZones, Locale locale) {
+            final DateFormat currentTimeFormat = new SimpleDateFormat(
+                    android.text.format.DateFormat.getTimeFormatString(context), locale);
+            final ArrayList<TimeZoneInfoItem> results = new ArrayList<>(timeZones.size());
+            final Resources resources = context.getResources();
+            long i = 0;
+            for (TimeZoneInfo timeZone : timeZones) {
+                results.add(new TimeZoneInfoItem(i++, timeZone, resources, currentTimeFormat));
+            }
+            return results;
+        }
+    }
+
+    private static class TimeZoneInfoItem implements BaseTimeZoneAdapter.AdapterItem {
+        private final long mItemId;
+        private final TimeZoneInfo mTimeZoneInfo;
+        private final Resources mResources;
+        private final DateFormat mTimeFormat;
+        private final String mTitle;
+        private final String[] mSearchKeys;
+
+        private TimeZoneInfoItem(long itemId, TimeZoneInfo timeZoneInfo, Resources resources,
+                DateFormat timeFormat) {
+            mItemId = itemId;
+            mTimeZoneInfo = timeZoneInfo;
+            mResources = resources;
+            mTimeFormat = timeFormat;
+            mTitle = createTitle(timeZoneInfo);
+            mSearchKeys = new String[] { mTitle };
+        }
+
+        private static String createTitle(TimeZoneInfo timeZoneInfo) {
+            String name = timeZoneInfo.getExemplarLocation();
+            if (name == null) {
+                name = timeZoneInfo.getGenericName();
+            }
+            if (name == null && timeZoneInfo.getTimeZone().inDaylightTime(new Date())) {
+                name = timeZoneInfo.getDaylightName();
+            }
+            if (name == null) {
+                name = timeZoneInfo.getStandardName();
+            }
+            if (name == null) {
+                name = String.valueOf(timeZoneInfo.getGmtOffset());
+            }
+            return name;
+        }
+
+        @Override
+        public CharSequence getTitle() {
+            return mTitle;
+        }
+
+        @Override
+        public CharSequence getSummary() {
+            String name = mTimeZoneInfo.getGenericName();
+            if (name == null) {
+                if (mTimeZoneInfo.getTimeZone().inDaylightTime(new Date())) {
+                    name = mTimeZoneInfo.getDaylightName();
+                } else {
+                    name = mTimeZoneInfo.getStandardName();
+                }
+            }
+            if (name == null) {
+                return mTimeZoneInfo.getGmtOffset();
+            } else {
+                return SpannableUtil.getResourcesText(mResources,
+                        R.string.zone_info_offset_and_name, mTimeZoneInfo.getGmtOffset(), name);
+            }
+        }
+
+        @Override
+        public String getIconText() {
+            return null;
+        }
+
+        @Override
+        public String getCurrentTime() {
+            return mTimeFormat.format(Calendar.getInstance(mTimeZoneInfo.getTimeZone()));
+        }
+
+        @Override
+        public long getItemId() {
+            return mItemId;
+        }
+
+        @Override
+        public String[] getSearchKeys() {
+            return mSearchKeys;
+        }
+    }
+}
diff --git a/src/com/android/settings/datetime/timezone/BaseTimeZonePicker.java b/src/com/android/settings/datetime/timezone/BaseTimeZonePicker.java
new file mode 100644 (file)
index 0000000..c5e1ccb
--- /dev/null
@@ -0,0 +1,159 @@
+/*
+ * 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.os.Bundle;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+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.SearchView;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.settings.R;
+import com.android.settings.core.InstrumentedFragment;
+import com.android.settings.datetime.timezone.model.TimeZoneData;
+import com.android.settings.datetime.timezone.model.TimeZoneDataLoader;
+
+import java.util.Locale;
+
+/**
+ * It's abstract class. Subclass should use it with {@class BaseTimeZoneAdapter} and
+ * {@class AdapterItem} to provide a list view with text search capability.
+ * The search matches the prefix of words in the search text.
+ */
+public abstract class BaseTimeZonePicker extends InstrumentedFragment
+        implements SearchView.OnQueryTextListener{
+
+    public static final String EXTRA_RESULT_REGION_ID =
+            "com.android.settings.datetime.timezone.result_region_id";
+    public static final String EXTRA_RESULT_TIME_ZONE_ID =
+            "com.android.settings.datetime.timezone.result_time_zone_id";
+    private final int mTitleResId;
+    private final int mSearchHintResId;
+    private final boolean mSearchEnabled;
+    private final boolean mDefaultExpandSearch;
+
+    protected Locale mLocale;
+    private BaseTimeZoneAdapter mAdapter;
+    private RecyclerView mRecyclerView;
+    private TimeZoneData mTimeZoneData;
+
+    private SearchView mSearchView;
+
+    /**
+     * Constructor called by subclass.
+     * @param defaultExpandSearch whether expand the search view when first launching the fragment
+     */
+    protected BaseTimeZonePicker(int titleResId, int searchHintResId,
+            boolean searchEnabled, boolean defaultExpandSearch) {
+        mTitleResId = titleResId;
+        mSearchHintResId = searchHintResId;
+        mSearchEnabled = searchEnabled;
+        mDefaultExpandSearch = defaultExpandSearch;
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setHasOptionsMenu(true);
+        getActivity().setTitle(mTitleResId);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        final View view = inflater.inflate(R.layout.recycler_view, container, false);
+        mRecyclerView = view.findViewById(R.id.recycler_view);
+        mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext(),
+                LinearLayoutManager.VERTICAL, /* reverseLayout */ false));
+        mRecyclerView.setAdapter(mAdapter);
+
+        // Initialize TimeZoneDataLoader only when mRecyclerView is ready to avoid race
+        // during onDateLoaderReady callback.
+        getLoaderManager().initLoader(0, null, new TimeZoneDataLoader.LoaderCreator(
+                getContext(), this::onTimeZoneDataReady));
+        return view;
+    }
+
+    public void onTimeZoneDataReady(TimeZoneData timeZoneData) {
+        if (mTimeZoneData == null && timeZoneData != null) {
+            mTimeZoneData = timeZoneData;
+            mAdapter = createAdapter(mTimeZoneData);
+            if (mRecyclerView != null) {
+                mRecyclerView.setAdapter(mAdapter);
+            }
+        }
+    }
+
+    protected Locale getLocale() {
+        return getContext().getResources().getConfiguration().getLocales().get(0);
+    }
+
+    /**
+     * Called when TimeZoneData is ready.
+     */
+    protected abstract BaseTimeZoneAdapter createAdapter(TimeZoneData timeZoneData);
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        if (mSearchEnabled) {
+            inflater.inflate(R.menu.time_zone_base_search_menu, menu);
+
+            final MenuItem searchMenuItem = menu.findItem(R.id.time_zone_search_menu);
+            mSearchView = (SearchView) searchMenuItem.getActionView();
+
+            mSearchView.setQueryHint(getText(mSearchHintResId));
+            mSearchView.setOnQueryTextListener(this);
+
+            if (mDefaultExpandSearch) {
+                searchMenuItem.expandActionView();
+                mSearchView.setIconified(false);
+                mSearchView.setActivated(true);
+                mSearchView.setQuery("", true /* submit */);
+            }
+        }
+    }
+
+    @Override
+    public boolean onQueryTextSubmit(String query) {
+        return false;
+    }
+
+    @Override
+    public boolean onQueryTextChange(String newText) {
+        if (mAdapter != null) {
+            mAdapter.getFilter().filter(newText);
+        }
+        return false;
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        // TODO: use a new metrics id?
+        return MetricsEvent.ZONE_PICKER;
+    }
+
+    public interface OnListItemClickListener {
+        void onListItemClick(int position);
+    }
+
+}
diff --git a/src/com/android/settings/datetime/timezone/FixedOffsetPicker.java b/src/com/android/settings/datetime/timezone/FixedOffsetPicker.java
new file mode 100644 (file)
index 0000000..3d8b826
--- /dev/null
@@ -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 android.icu.util.TimeZone;
+
+import com.android.settings.R;
+import com.android.settings.datetime.timezone.model.TimeZoneData;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Render a list of fixed offset time zone {@class TimeZoneInfo} into a list view.
+ */
+public class FixedOffsetPicker extends BaseTimeZoneInfoPicker {
+    /**
+     * Range of integer fixed UTC offsets shown in the pickers.
+     */
+    private static final int MIN_HOURS_OFFSET = -14;
+    private static final int MAX_HOURS_OFFSET = +12;
+
+    public FixedOffsetPicker() {
+        super(R.string.date_time_select_fixed_offset_time_zones,
+                R.string.search_settings, false, false);
+    }
+
+    @Override
+    public List<TimeZoneInfo> getAllTimeZoneInfos(TimeZoneData timeZoneData) {
+        return loadFixedOffsets();
+    }
+
+    /**
+     * Returns a {@link TimeZoneInfo} for each fixed offset time zone, such as UTC or GMT+4. The
+     * returned list will be sorted in a reasonable way for display.
+     */
+    private List<TimeZoneInfo> loadFixedOffsets() {
+        final TimeZoneInfo.Formatter formatter = new TimeZoneInfo.Formatter(getLocale(),
+                new Date());
+        final List<TimeZoneInfo> timeZoneInfos = new ArrayList<>();
+        timeZoneInfos.add(formatter.format(TimeZone.getFrozenTimeZone("Etc/UTC")));
+        for (int hoursOffset = MAX_HOURS_OFFSET; hoursOffset >= MIN_HOURS_OFFSET; --hoursOffset) {
+            if (hoursOffset == 0) {
+                // UTC is handled above, so don't add GMT +/-0 again.
+                continue;
+            }
+            final String id = String.format(Locale.US, "Etc/GMT%+d", hoursOffset);
+            timeZoneInfos.add(formatter.format(TimeZone.getFrozenTimeZone(id)));
+        }
+        return Collections.unmodifiableList(timeZoneInfos);
+    }
+}
diff --git a/src/com/android/settings/datetime/timezone/RegionSearchPicker.java b/src/com/android/settings/datetime/timezone/RegionSearchPicker.java
new file mode 100644 (file)
index 0000000..1381b20
--- /dev/null
@@ -0,0 +1,211 @@
+/*
+ * 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.Intent;
+import android.graphics.Paint;
+import android.icu.text.Collator;
+import android.icu.text.LocaleDisplayNames;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.settings.R;
+import com.android.settings.core.SubSettingLauncher;
+import com.android.settings.datetime.timezone.model.FilteredCountryTimeZones;
+import com.android.settings.datetime.timezone.model.TimeZoneData;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Render a list of regions into a list view.
+ */
+public class RegionSearchPicker extends BaseTimeZonePicker {
+    private static final int REQUEST_CODE_ZONE_PICKER = 1;
+    private static final String TAG = "RegionSearchPicker";
+
+    private BaseTimeZoneAdapter<RegionItem> mAdapter;
+    private TimeZoneData mTimeZoneData;
+
+    public RegionSearchPicker() {
+        super(R.string.date_time_select_region, R.string.search_settings, true, true);
+    }
+
+    @Override
+    protected BaseTimeZoneAdapter createAdapter(TimeZoneData timeZoneData) {
+        mTimeZoneData = timeZoneData;
+        mAdapter = new BaseTimeZoneAdapter<>(createAdapterItem(timeZoneData.getRegionIds()),
+                this::onListItemClick, getLocale(), false);
+        return mAdapter;
+    }
+
+    private void onListItemClick(int position) {
+        final String regionId = mAdapter.getItem(position).getId();
+        final FilteredCountryTimeZones countryTimeZones = mTimeZoneData.lookupCountryTimeZones(
+                regionId);
+        final Activity activity = getActivity();
+        if (countryTimeZones == null || countryTimeZones.getTimeZoneIds().isEmpty()) {
+            Log.e(TAG, "Region has no time zones: " + regionId);
+            activity.setResult(Activity.RESULT_CANCELED);
+            activity.finish();
+            return;
+        }
+
+        List<String> timeZoneIds = countryTimeZones.getTimeZoneIds();
+        // Choose the time zone associated the region if there is only one time zone in that region
+        if (timeZoneIds.size() == 1) {
+            final Intent resultData = new Intent()
+                    .putExtra(EXTRA_RESULT_REGION_ID, regionId)
+                    .putExtra(EXTRA_RESULT_TIME_ZONE_ID, timeZoneIds.get(0));
+            getActivity().setResult(Activity.RESULT_OK, resultData);
+            getActivity().finish();
+        } else {
+            // Launch the zone picker and let the user choose a time zone from the list of
+            // time zones associated with the region.
+            final Bundle args = new Bundle();
+            args.putString(RegionZonePicker.EXTRA_REGION_ID, regionId);
+            new SubSettingLauncher(getContext())
+                    .setDestination(RegionZonePicker.class.getCanonicalName())
+                    .setArguments(args)
+                    .setSourceMetricsCategory(getMetricsCategory())
+                    .setResultListener(this, REQUEST_CODE_ZONE_PICKER)
+                    .launch();
+        }
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == REQUEST_CODE_ZONE_PICKER) {
+            if (resultCode == Activity.RESULT_OK) {
+                getActivity().setResult(Activity.RESULT_OK, data);
+            }
+            getActivity().finish();
+        }
+    }
+
+    private List<RegionItem> createAdapterItem(Set<String> regionIds) {
+        final Collator collator = Collator.getInstance(getLocale());
+        final TreeSet<RegionItem> items = new TreeSet<>(new RegionInfoComparator(collator));
+        final Paint paint = new Paint();
+        final LocaleDisplayNames localeDisplayNames = LocaleDisplayNames.getInstance(getLocale());
+        long i = 0;
+        for (String regionId : regionIds) {
+            String name = localeDisplayNames.regionDisplayName(regionId);
+            String regionalIndicator = createRegionalIndicator(regionId, paint);
+            items.add(new RegionItem(i++, regionId, name, regionalIndicator));
+        }
+        return new ArrayList<>(items);
+    }
+
+    /**
+     * Create a Unicode Region Indicator Symbol for a given region id (a.k.a flag emoji). If the
+     * system can't render a flag for this region or the input is not a region id, this returns
+     * {@code null}.
+     *
+     * @param id the two-character region id.
+     * @param paint Paint contains the glyph
+     * @return a String representing the flag of the region or {@code null}.
+     */
+    private static String createRegionalIndicator(String id, Paint paint) {
+        if (id.length() != 2) {
+            return null;
+        }
+        final char c1 = id.charAt(0);
+        final char c2 = id.charAt(1);
+        if ('A' > c1 || c1 > 'Z' || 'A' > c2 || c2 > 'Z') {
+            return null;
+        }
+        // Regional Indicator A is U+1F1E6 which is 0xD83C 0xDDE6 in UTF-16.
+        final String regionalIndicator = new String(
+            new char[]{0xd83c, (char) (0xdde6 - 'A' + c1), 0xd83c, (char) (0xdde6 - 'A' + c2)});
+        if (!paint.hasGlyph(regionalIndicator)) {
+            return null;
+        }
+        return regionalIndicator;
+    }
+
+    private static class RegionItem implements BaseTimeZoneAdapter.AdapterItem {
+
+        private final String mId;
+        private final String mName;
+        private final String mRegionalIndicator;
+        private final long mItemId;
+        private final String[] mSearchKeys;
+
+        RegionItem(long itemId, String id, String name, String regionalIndicator) {
+            mId = id;
+            mName = name;
+            mRegionalIndicator = regionalIndicator;
+            mItemId = itemId;
+            // Allow to search with ISO_3166-1 alpha-2 code. It's handy for english users in some
+            // countries, e.g. US for United States. It's not best search keys for users, but
+            // ICU doesn't have the data for the alias names of a region.
+            mSearchKeys = new String[] {mId, mName};
+        }
+
+        public String getId() {
+            return mId;
+        }
+
+        @Override
+        public CharSequence getTitle() {
+            return mName;
+        }
+
+        @Override
+        public CharSequence getSummary() {
+            return null;
+        }
+
+        @Override
+        public String getIconText() {
+            return mRegionalIndicator;
+        }
+
+        @Override
+        public String getCurrentTime() {
+            return null;
+        }
+
+        @Override
+        public long getItemId() {
+            return mItemId;
+        }
+
+        @Override
+        public String[] getSearchKeys() {
+            return mSearchKeys;
+        }
+    }
+
+    private static class RegionInfoComparator implements Comparator<RegionItem> {
+        private final Collator mCollator;
+
+        RegionInfoComparator(Collator collator) {
+            mCollator = collator;
+        }
+
+        @Override
+        public int compare(RegionItem r1, RegionItem r2) {
+            return mCollator.compare(r1.getTitle(), r2.getTitle());
+        }
+    }
+}
diff --git a/src/com/android/settings/datetime/timezone/RegionZonePicker.java b/src/com/android/settings/datetime/timezone/RegionZonePicker.java
new file mode 100644 (file)
index 0000000..7805241
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ * 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.Intent;
+import android.icu.text.Collator;
+import android.icu.util.TimeZone;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+import com.android.settings.R;
+import com.android.settings.datetime.timezone.model.FilteredCountryTimeZones;
+import com.android.settings.datetime.timezone.model.TimeZoneData;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.TreeSet;
+
+/**
+ * Given a region, render a list of time zone {@class TimeZoneInfo} into a list view.
+ */
+public class RegionZonePicker extends BaseTimeZoneInfoPicker {
+
+    public static final String EXTRA_REGION_ID =
+            "com.android.settings.datetime.timezone.region_id";
+
+    public RegionZonePicker() {
+        super(R.string.date_time_select_zone, R.string.search_settings, true, false);
+    }
+
+    /**
+     * Add the extra region id into the result.
+     */
+    @Override
+    protected Intent prepareResultData(TimeZoneInfo selectedTimeZoneInfo) {
+        final Intent intent = super.prepareResultData(selectedTimeZoneInfo);
+        intent.putExtra(EXTRA_RESULT_REGION_ID, getArguments().getString(EXTRA_REGION_ID));
+        return intent;
+    }
+
+    @Override
+    public List<TimeZoneInfo> getAllTimeZoneInfos(TimeZoneData timeZoneData) {
+        if (getArguments() == null) {
+            Log.e(TAG, "getArguments() == null");
+            getActivity().finish();
+        }
+        String regionId = getArguments().getString(EXTRA_REGION_ID);
+
+        FilteredCountryTimeZones filteredCountryTimeZones = timeZoneData.lookupCountryTimeZones(
+                regionId);
+        if (filteredCountryTimeZones == null) {
+            Log.e(TAG, "region id is not valid: " + regionId);
+            getActivity().finish();
+        }
+
+        // It could be a timely operations if there are many time zones. A region in time zone data
+        // contains a maximum of 29 time zones currently. It may change in the future, but it's
+        // unlikely to be changed drastically.
+        return getRegionTimeZoneInfo(filteredCountryTimeZones.getTimeZoneIds());
+    }
+
+    /**
+     * Returns a list of {@link TimeZoneInfo} objects. The returned list will be sorted properly for
+     * display in the locale.It may be smaller than the input collection, if equivalent IDs are
+     * passed in.
+     *
+     * @param timeZoneIds a list of Olson IDs.
+     */
+    public List<TimeZoneInfo> getRegionTimeZoneInfo(Collection<String> timeZoneIds) {
+        final TimeZoneInfo.Formatter formatter = new TimeZoneInfo.Formatter(getLocale(),
+                new Date());
+        final TreeSet<TimeZoneInfo> timeZoneInfos =
+                new TreeSet<>(new TimeZoneInfoComparator(Collator.getInstance(getLocale()),
+                        new Date()));
+
+        for (final String timeZoneId : timeZoneIds) {
+            final TimeZone timeZone = TimeZone.getFrozenTimeZone(timeZoneId);
+            // Skip time zone ICU isn't aware.
+            if (timeZone.getID().equals(TimeZone.UNKNOWN_ZONE_ID)) {
+                continue;
+            }
+            timeZoneInfos.add(formatter.format(timeZone));
+        }
+        return Collections.unmodifiableList(new ArrayList<>(timeZoneInfos));
+    }
+
+    @VisibleForTesting
+    static class TimeZoneInfoComparator implements Comparator<TimeZoneInfo> {
+        private Collator mCollator;
+        private final Date mNow;
+
+        @VisibleForTesting
+        TimeZoneInfoComparator(Collator collator, Date now) {
+            mCollator = collator;
+            mNow = now;
+        }
+
+        @Override
+        public int compare(TimeZoneInfo tzi1, TimeZoneInfo tzi2) {
+            int result = Integer.compare(tzi1.getTimeZone().getOffset(mNow.getTime()),
+                    tzi2.getTimeZone().getOffset(mNow.getTime()));
+            if (result == 0) {
+                result = Integer.compare(tzi1.getTimeZone().getRawOffset(),
+                    tzi2.getTimeZone().getRawOffset());
+            }
+            if (result == 0) {
+                result = mCollator.compare(tzi1.getExemplarLocation(), tzi2.getExemplarLocation());
+            }
+            if (result == 0 && tzi1.getGenericName() != null && tzi2.getGenericName() != null) {
+                result = mCollator.compare(tzi1.getGenericName(), tzi2.getGenericName());
+            }
+            return result;
+        }
+    }
+}
diff --git a/src/com/android/settings/datetime/timezone/SpannableUtil.java b/src/com/android/settings/datetime/timezone/SpannableUtil.java
new file mode 100644 (file)
index 0000000..49c3e7d
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * 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.annotation.StringRes;
+import android.content.res.Resources;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+
+import java.util.Formatter;
+import java.util.Locale;
+
+
+public class SpannableUtil {
+
+    /**
+     * {@class Resources} has no method to format string resource with {@class Spannable} a
+     * rguments. It's a helper method for this purpose.
+     */
+    public static Spannable getResourcesText(Resources res, @StringRes int resId,
+            Object... args) {
+        final Locale locale = res.getConfiguration().getLocales().get(0);
+        final SpannableStringBuilder builder = new SpannableStringBuilder();
+        new Formatter(builder, locale).format(res.getString(resId), args);
+        return builder;
+    }
+}
index a863bfc..b78534d 100644 (file)
@@ -57,7 +57,7 @@ public class TimeZoneData {
     }
 
     @VisibleForTesting
-    TimeZoneData(CountryZonesFinder countryZonesFinder) {
+    public TimeZoneData(CountryZonesFinder countryZonesFinder) {
         mCountryZonesFinder = countryZonesFinder;
         mRegionIds = getNormalizedRegionIds(mCountryZonesFinder.lookupAllCountryIsoCodes());
     }
diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapterTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapterTest.java
new file mode 100644 (file)
index 0000000..c85c598
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+ * 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.widget.RecyclerView.AdapterDataObserver;
+
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static com.google.common.truth.Truth.assertThat;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+public class BaseTimeZoneAdapterTest {
+
+    @Test
+    public void testFilter() throws InterruptedException {
+        TestItem US = new TestItem("United States");
+        TestItem HK = new TestItem("Hong Kong");
+        TestItem UK = new TestItem("United Kingdom", new String[] { "United Kingdom",
+                "Great Britain"});
+        TestItem secretCountry = new TestItem("no name", new String[] { "Secret"});
+        List<TestItem> items = new ArrayList<>();
+        items.add(US);
+        items.add(HK);
+        items.add(UK);
+        items.add(secretCountry);
+
+        TestTimeZoneAdapter adapter = new TestTimeZoneAdapter(items);
+        assertSearch(adapter, "", items.toArray(new TestItem[items.size()]));
+        assertSearch(adapter, "Unit", US, UK);
+        assertSearch(adapter, "kon", HK);
+        assertSearch(adapter, "brit", UK);
+        assertSearch(adapter, "sec", secretCountry);
+    }
+
+    private void assertSearch(TestTimeZoneAdapter adapter , String searchText, TestItem... items)
+            throws InterruptedException {
+        Observer observer = new Observer(adapter);
+        adapter.getFilter().filter(searchText);
+        observer.await();
+        assertThat(adapter.getItemCount()).isEqualTo(items.length);
+        for (int i = 0; i < items.length; i++) {
+            assertThat(adapter.getItem(i)).isEqualTo(items[i]);
+        }
+    }
+
+    private static class Observer extends AdapterDataObserver {
+
+        private final CountDownLatch mLatch = new CountDownLatch(1);
+        private final TestTimeZoneAdapter mAdapter;
+
+        public Observer(TestTimeZoneAdapter adapter) {
+            mAdapter = adapter;
+            mAdapter.registerAdapterDataObserver(this);
+        }
+
+        @Override
+        public void onChanged() {
+            mAdapter.unregisterAdapterDataObserver(this);
+            mLatch.countDown();
+        }
+
+        public void await() throws InterruptedException {
+            mLatch.await(2L, TimeUnit.SECONDS);
+        }
+    }
+
+    private static class TestTimeZoneAdapter extends BaseTimeZoneAdapter<TestItem> {
+
+        public TestTimeZoneAdapter(List<TestItem> items) {
+            super(items, position -> {}, Locale.US, false);
+        }
+    }
+
+    private static class TestItem implements BaseTimeZoneAdapter.AdapterItem {
+
+        private final String mTitle;
+        private final String[] mSearchKeys;
+
+        TestItem(String title) {
+            this(title, new String[] { title });
+        }
+
+        TestItem(String title, String[] searchKeys) {
+            mTitle = title;
+            mSearchKeys = searchKeys;
+        }
+
+        @Override
+        public CharSequence getTitle() {
+            return mTitle;
+        }
+
+        @Override
+        public CharSequence getSummary() {
+            return null;
+        }
+
+        @Override
+        public String getIconText() {
+            return null;
+        }
+
+        @Override
+        public String getCurrentTime() {
+            return null;
+        }
+
+        @Override
+        public long getItemId() {
+            return 0;
+        }
+
+        @Override
+        public String[] getSearchKeys() {
+            return mSearchKeys;
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPickerTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPickerTest.java
new file mode 100644 (file)
index 0000000..0d47a3a
--- /dev/null
@@ -0,0 +1,123 @@
+/*
+ * 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.util.TimeZone;
+
+import com.android.settings.datetime.timezone.model.TimeZoneData;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+
+import com.google.common.truth.Truth;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+import static org.mockito.Mockito.mock;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(shadows = { BaseTimeZoneInfoPickerTest.ShadowDataFormat.class })
+public class BaseTimeZoneInfoPickerTest {
+    @Implements(android.text.format.DateFormat.class)
+    public static class ShadowDataFormat {
+
+        public static String sTimeFormatString = "";
+
+        @Implementation
+        public static String getTimeFormatString(Context context) {
+            return sTimeFormatString;
+        }
+    }
+
+    /**
+     * Verify the summary, title, and time label in a time zone item are formatted properly.
+     */
+    @Test
+    public void createAdapter_matchTimeZoneInfoAndOrder() {
+        ShadowDataFormat.sTimeFormatString = "HH:MM";
+        BaseTimeZoneInfoPicker picker = new TestBaseTimeZoneInfoPicker();
+        BaseTimeZoneAdapter adapter = picker.createAdapter(mock(TimeZoneData.class));
+        Truth.assertThat(adapter.getItemCount()).isEqualTo(2);
+
+        BaseTimeZoneAdapter.AdapterItem item1 = adapter.getItem(0);
+        Truth.assertThat(item1.getTitle().toString()).isEqualTo("Los Angeles");
+        Truth.assertThat(item1.getSummary().toString()).isEqualTo("Pacific Time (GMT-08:00)");
+        Truth.assertThat(item1.getCurrentTime())
+                .hasLength(ShadowDataFormat.sTimeFormatString.length());
+
+        BaseTimeZoneAdapter.AdapterItem item2 = adapter.getItem(1);
+        Truth.assertThat(item2.getTitle().toString()).isEqualTo("New York");
+        Truth.assertThat(item2.getSummary().toString()).isEqualTo("Eastern Time (GMT-05:00)");
+        Truth.assertThat(item2.getCurrentTime())
+                .hasLength(ShadowDataFormat.sTimeFormatString.length());
+    }
+
+    public static class TestBaseTimeZoneInfoPicker extends BaseTimeZoneInfoPicker {
+
+        public TestBaseTimeZoneInfoPicker() {
+            super(0, 0, false, false);
+        }
+
+        @Override
+        public List<TimeZoneInfo> getAllTimeZoneInfos(TimeZoneData timeZoneData) {
+            TimeZoneInfo zone1 = 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();
+            TimeZoneInfo zone2 = new TimeZoneInfo.Builder(
+                    TimeZone.getFrozenTimeZone("America/New_York"))
+                    .setGenericName("Eastern Time")
+                    .setStandardName("Eastern Standard Time")
+                    .setDaylightName("Eastern Daylight Time")
+                    .setExemplarLocation("New York")
+                    .setGmtOffset("GMT-05:00")
+                    .setItemId(1)
+                    .build();
+
+            return Arrays.asList(zone1, zone2);
+        }
+
+        // Make the method public
+        @Override
+        public BaseTimeZoneAdapter createAdapter(TimeZoneData timeZoneData) {
+            return super.createAdapter(timeZoneData);
+        }
+
+        @Override
+        protected Locale getLocale() {
+            return Locale.US;
+        }
+
+        @Override
+        public Context getContext() {
+            return RuntimeEnvironment.application;
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/FixedOffsetPickerTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/FixedOffsetPickerTest.java
new file mode 100644 (file)
index 0000000..1c555b0
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * 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 libcore.util.CountryZonesFinder;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+public class FixedOffsetPickerTest {
+
+    @Test
+    public void getAllTimeZoneInfos_containsUtcAndGmtZones() {
+        List regionList = Collections.emptyList();
+        CountryZonesFinder finder = mock(CountryZonesFinder.class);
+        when(finder.lookupAllCountryIsoCodes()).thenReturn(regionList);
+
+        FixedOffsetPicker picker = new FixedOffsetPicker() {
+            @Override
+            protected Locale getLocale() {
+                return Locale.US;
+            }
+        };
+        List<TimeZoneInfo> infos = picker.getAllTimeZoneInfos(new TimeZoneData(finder));
+        List<String> tzIds = infos.stream().map(info -> info.getId()).collect(Collectors.toList());
+        tzIds.contains("Etc/Utc");
+        tzIds.contains("Etc/GMT-12");
+        tzIds.contains("Etc/GMT+14");
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/RegionSearchPickerTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/RegionSearchPickerTest.java
new file mode 100644 (file)
index 0000000..b2c7f03
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * 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.BaseTimeZoneAdapter.AdapterItem;
+import com.android.settings.datetime.timezone.model.TimeZoneData;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+
+import libcore.util.CountryZonesFinder;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+public class RegionSearchPickerTest {
+
+    @Test
+    public void createAdapter_matchRegionName() {
+        List regionList = new ArrayList();
+        regionList.add("US");
+        CountryZonesFinder finder = mock(CountryZonesFinder.class);
+        when(finder.lookupAllCountryIsoCodes()).thenReturn(regionList);
+
+        RegionSearchPicker picker = new RegionSearchPicker() {
+            @Override
+            protected Locale getLocale() {
+                return Locale.US;
+            }
+        };
+        BaseTimeZoneAdapter adapter = picker.createAdapter(new TimeZoneData(finder));
+        assertEquals(1, adapter.getItemCount());
+        AdapterItem item = adapter.getItem(0);
+        assertEquals("United States", item.getTitle().toString());
+        assertThat(Arrays.asList(item.getSearchKeys())).contains("United States");
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/RegionZonePickerTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/RegionZonePickerTest.java
new file mode 100644 (file)
index 0000000..e527270
--- /dev/null
@@ -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.icu.text.Collator;
+
+import com.android.settings.datetime.timezone.RegionZonePicker.TimeZoneInfoComparator;
+import com.android.settings.datetime.timezone.TimeZoneInfo.Formatter;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+import static com.google.common.truth.Truth.assertThat;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+public class RegionZonePickerTest {
+
+    @Test
+    public void compareTimeZoneInfo_matchGmtOrder() {
+        Date now = new Date(0); // 00:00 1, Jan 1970
+        Formatter formatter = new Formatter(Locale.US, now);
+        TimeZoneInfo timeZone1 = formatter.format("Pacific/Honolulu");
+        TimeZoneInfo timeZone2 = formatter.format("America/Los_Angeles");
+        TimeZoneInfo timeZone3 = formatter.format("America/Indiana/Marengo");
+        TimeZoneInfo timeZone4 = formatter.format("America/New_York");
+
+        TimeZoneInfoComparator comparator =
+                new TimeZoneInfoComparator(Collator.getInstance(Locale.US), now);
+
+        // Verify the sorted order
+        List<TimeZoneInfo> list = Arrays.asList(timeZone4, timeZone2, timeZone3, timeZone1);
+        Collections.sort(list, comparator);
+        assertThat(list).isEqualTo(Arrays.asList(timeZone1, timeZone2, timeZone3, timeZone4));
+    }
+
+}
diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/SpannableUtilTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/SpannableUtilTest.java
new file mode 100644 (file)
index 0000000..5517907
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * 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.text.Spannable;
+
+import com.android.settings.R;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+
+import static com.google.common.truth.Truth.assertThat;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+public class SpannableUtilTest {
+
+    @Test
+    public void testFormat() {
+        Spannable spannable = SpannableUtil.getResourcesText(
+                RuntimeEnvironment.application.getResources(), R.string.zone_info_offset_and_name,
+                "GMT+00:00", "UTC");
+        assertThat(spannable.toString()).isEqualTo("UTC (GMT+00:00)");
+    }
+}