diff --git a/res/layout/time_zone_search_item.xml b/res/layout/time_zone_search_item.xml new file mode 100644 index 00000000000..bb75226714d --- /dev/null +++ b/res/layout/time_zone_search_item.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 index 00000000000..92241af4551 --- /dev/null +++ b/res/menu/time_zone_base_search_menu.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 4e81b7a129c..3452af0e970 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -748,20 +748,34 @@ Date Set date + + Region + + Time Zone + + Select UTC offset Sort alphabetically Sort by time zone %1$s starts on %2$s. + + %1$s (%2$s) + + %2$s (%1$s) + + Uses %1$s. %2$s starts on %3$s. + + Uses %1$s. No daylight savings time. Daylight savings time Standard time - Time zone by region + Show time zones by region - Fixed offset time zones + Show time zones by UTC offset diff --git a/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java b/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java new file mode 100644 index 00000000000..effa9485a2f --- /dev/null +++ b/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java @@ -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 + extends RecyclerView.Adapter { + + private final List mOriginalItems; + private final OnListItemClickListener mOnListItemClickListener; + private final Locale mLocale; + private final boolean mShowItemSummary; + + private List mItems; + private ArrayFilter mFilter; + + public BaseTimeZoneAdapter(List 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); + } + } + + /** + *

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.

+ * + * 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 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) 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 index 00000000000..b1335823af5 --- /dev/null +++ b/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPicker.java @@ -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 getAllTimeZoneInfos(TimeZoneData timeZoneData); + + protected static class ZoneAdapter extends BaseTimeZoneAdapter { + + public ZoneAdapter(Context context, List timeZones, + OnListItemClickListener onListItemClickListener, Locale locale) { + super(createTimeZoneInfoItems(context, timeZones, locale), + onListItemClickListener, locale, true /* showItemSummary */); + } + + private static List createTimeZoneInfoItems(Context context, + List timeZones, Locale locale) { + final DateFormat currentTimeFormat = new SimpleDateFormat( + android.text.format.DateFormat.getTimeFormatString(context), locale); + final ArrayList 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 index 00000000000..c5e1ccb431a --- /dev/null +++ b/src/com/android/settings/datetime/timezone/BaseTimeZonePicker.java @@ -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 index 00000000000..3d8b826a73d --- /dev/null +++ b/src/com/android/settings/datetime/timezone/FixedOffsetPicker.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datetime.timezone; + +import 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 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 loadFixedOffsets() { + final TimeZoneInfo.Formatter formatter = new TimeZoneInfo.Formatter(getLocale(), + new Date()); + final List 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 index 00000000000..1381b209feb --- /dev/null +++ b/src/com/android/settings/datetime/timezone/RegionSearchPicker.java @@ -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 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 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 createAdapterItem(Set regionIds) { + final Collator collator = Collator.getInstance(getLocale()); + final TreeSet 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 { + 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 index 00000000000..7805241aec8 --- /dev/null +++ b/src/com/android/settings/datetime/timezone/RegionZonePicker.java @@ -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 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 getRegionTimeZoneInfo(Collection timeZoneIds) { + final TimeZoneInfo.Formatter formatter = new TimeZoneInfo.Formatter(getLocale(), + new Date()); + final TreeSet 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 { + 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 index 00000000000..49c3e7d55e0 --- /dev/null +++ b/src/com/android/settings/datetime/timezone/SpannableUtil.java @@ -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; + } +} diff --git a/src/com/android/settings/datetime/timezone/model/TimeZoneData.java b/src/com/android/settings/datetime/timezone/model/TimeZoneData.java index a863bfc7ab2..b78534d6671 100644 --- a/src/com/android/settings/datetime/timezone/model/TimeZoneData.java +++ b/src/com/android/settings/datetime/timezone/model/TimeZoneData.java @@ -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 index 00000000000..c85c598491a --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapterTest.java @@ -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 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 { + + public TestTimeZoneAdapter(List 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 index 00000000000..0d47a3a9144 --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPickerTest.java @@ -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 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 index 00000000000..1c555b078ca --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/timezone/FixedOffsetPickerTest.java @@ -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 infos = picker.getAllTimeZoneInfos(new TimeZoneData(finder)); + List 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 index 00000000000..b2c7f035c36 --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/timezone/RegionSearchPickerTest.java @@ -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 index 00000000000..e5272704ecb --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/timezone/RegionZonePickerTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.datetime.timezone; + +import android.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 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 index 00000000000..55179070b58 --- /dev/null +++ b/tests/robotests/src/com/android/settings/datetime/timezone/SpannableUtilTest.java @@ -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)"); + } +}