--- /dev/null
+<?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
--- /dev/null
+<?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
<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] -->
--- /dev/null
+/*
+ * 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();
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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());
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
}
@VisibleForTesting
- TimeZoneData(CountryZonesFinder countryZonesFinder) {
+ public TimeZoneData(CountryZonesFinder countryZonesFinder) {
mCountryZonesFinder = countryZonesFinder;
mRegionIds = getNormalizedRegionIds(mCountryZonesFinder.lookupAllCountryIsoCodes());
}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+ }
+}
--- /dev/null
+/*
+ * 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");
+ }
+}
--- /dev/null
+/*
+ * 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");
+ }
+}
--- /dev/null
+/*
+ * 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));
+ }
+
+}
--- /dev/null
+/*
+ * 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)");
+ }
+}