Time zone, Region, UTC picker

- Extract most common view related codes into BaseTimeZoneAdapter
  and BaseTimeZonePicker. Subclass handles the text formatting and
  order.
- Search view is added compared to previous version of time
  zone picker
- SpannableUtil is added to preserve spannable when formatting
  String resource.
- Fix the bug using GMT+<arabic> as time zone id. b/73132985
- Fix Talkback treating flags on screens as a separate element

Bug: 72146259
Bug: 73132985
Bug: 73952488
Test: mm RunSettingsRoboTests
Change-Id: I42c6ac369199c09d11e7f5cc4707358fa4780fed
This commit is contained in:
Victor Chang
2018-02-28 19:31:31 +00:00
parent 22a39c2b93
commit fbd30acef0
17 changed files with 1603 additions and 3 deletions

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- similar to preference_material.xml but textview for emoji country flag
instead of an ImageView -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
android:paddingRight="?android:attr/listPreferredItemPaddingRight"
android:background="?android:attr/selectableItemBackground"
android:clipToPadding="false">
<LinearLayout
android:id="@+id/icon_frame"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="-4dp"
android:minWidth="60dp"
android:gravity="start|center_vertical"
android:orientation="horizontal"
android:paddingRight="12dp"
android:paddingTop="4dp"
android:paddingBottom="4dp">
<!-- It's not ImageView because the icon is Unicode emoji. -->
<TextView
android:id="@+id/icon_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:contentDescription=""
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="?android:attr/textColorPrimary"
android:importantForAccessibility="no"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<TextView android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="@style/Preference_TextAppearanceMaterialSubhead"
android:ellipsize="marquee" />
<RelativeLayout
android:id="@+id/summary_frame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@android:id/title"
android:layout_alignLeft="@android:id/title">
<TextView
android:id="@+id/current_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:layout_alignParentEnd="true"/>
<!-- Use layout_alignParentStart and layout_toStartOf to make the TextView multi-lines
if needed -->
<TextView
android:id="@android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
android:maxLines="10"
android:layout_alignParentStart="true"
android:layout_toStartOf="@+id/current_time"/>
</RelativeLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/time_zone_search_menu"
android:title="@string/search_settings"
android:icon="@*android:drawable/ic_search_api_material"
android:showAsAction="always|collapseActionView"
android:actionViewClass="android.widget.SearchView" />
</menu>

View File

@@ -748,20 +748,34 @@
<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] -->

View File

@@ -0,0 +1,206 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.datetime.timezone;
import android.icu.text.BreakIterator;
import android.support.annotation.NonNull;
import android.support.annotation.WorkerThread;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.TextView;
import com.android.settings.R;
import com.android.settings.datetime.timezone.BaseTimeZonePicker.OnListItemClickListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* Used with {@class BaseTimeZonePicker}. It renders text in each item into list view. A list of
* {@class AdapterItem} must be provided when an instance is created.
*/
public class BaseTimeZoneAdapter<T extends BaseTimeZoneAdapter.AdapterItem>
extends RecyclerView.Adapter<BaseTimeZoneAdapter.ItemViewHolder> {
private final List<T> mOriginalItems;
private final OnListItemClickListener mOnListItemClickListener;
private final Locale mLocale;
private final boolean mShowItemSummary;
private List<T> mItems;
private ArrayFilter mFilter;
public BaseTimeZoneAdapter(List<T> items, OnListItemClickListener
onListItemClickListener, Locale locale, boolean showItemSummary) {
mOriginalItems = items;
mItems = items;
mOnListItemClickListener = onListItemClickListener;
mLocale = locale;
mShowItemSummary = showItemSummary;
setHasStableIds(true);
}
@NonNull
@Override
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
final View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.time_zone_search_item, parent, false);
return new ItemViewHolder(view, mOnListItemClickListener);
}
@Override
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) {
final AdapterItem item = mItems.get(position);
holder.mSummaryFrame.setVisibility(
mShowItemSummary ? View.VISIBLE : View.GONE);
holder.mTitleView.setText(item.getTitle());
holder.mIconTextView.setText(item.getIconText());
holder.mSummaryView.setText(item.getSummary());
holder.mTimeView.setText(item.getCurrentTime());
holder.setPosition(position);
}
@Override
public long getItemId(int position) {
return getItem(position).getItemId();
}
@Override
public int getItemCount() {
return mItems.size();
}
public @NonNull
Filter getFilter() {
if (mFilter == null) {
mFilter = new ArrayFilter();
}
return mFilter;
}
public T getItem(int position) {
return mItems.get(position);
}
public interface AdapterItem {
CharSequence getTitle();
CharSequence getSummary();
String getIconText();
String getCurrentTime();
long getItemId();
String[] getSearchKeys();
}
public static class ItemViewHolder extends RecyclerView.ViewHolder
implements View.OnClickListener{
final OnListItemClickListener mOnListItemClickListener;
final View mSummaryFrame;
final TextView mTitleView;
final TextView mIconTextView;
final TextView mSummaryView;
final TextView mTimeView;
private int mPosition;
public ItemViewHolder(View itemView, OnListItemClickListener onListItemClickListener) {
super(itemView);
itemView.setOnClickListener(this);
mSummaryFrame = itemView.findViewById(R.id.summary_frame);
mTitleView = itemView.findViewById(android.R.id.title);
mIconTextView = itemView.findViewById(R.id.icon_text);
mSummaryView = itemView.findViewById(android.R.id.summary);
mTimeView = itemView.findViewById(R.id.current_time);
mOnListItemClickListener = onListItemClickListener;
}
public void setPosition(int position) {
mPosition = position;
}
@Override
public void onClick(View v) {
mOnListItemClickListener.onListItemClick(mPosition);
}
}
/**
* <p>An array filter constrains the content of the array adapter with
* a prefix. Each item that does not start with the supplied prefix
* is removed from the list.</p>
*
* The filtering operation is not optimized, due to small data size (~260 regions),
* require additional pre-processing. Potentially, a trie structure can be used to match
* prefixes of the search keys.
*/
private class ArrayFilter extends Filter {
private BreakIterator mBreakIterator = BreakIterator.getWordInstance(mLocale);
@WorkerThread
@Override
protected FilterResults performFiltering(CharSequence prefix) {
final List<T> newItems;
if (TextUtils.isEmpty(prefix)) {
newItems = mOriginalItems;
} else {
final String prefixString = prefix.toString().toLowerCase(mLocale);
newItems = new ArrayList<>();
for (T item : mOriginalItems) {
outer:
for (String searchKey : item.getSearchKeys()) {
searchKey = searchKey.toLowerCase(mLocale);
// First match against the whole, non-splitted value
if (searchKey.startsWith(prefixString)) {
newItems.add(item);
break outer;
} else {
mBreakIterator.setText(searchKey);
for (int wordStart = 0, wordLimit = mBreakIterator.next();
wordLimit != BreakIterator.DONE;
wordStart = wordLimit,
wordLimit = mBreakIterator.next()) {
if (mBreakIterator.getRuleStatus() != BreakIterator.WORD_NONE
&& searchKey.startsWith(prefixString, wordStart)) {
newItems.add(item);
break outer;
}
}
}
}
}
}
final FilterResults results = new FilterResults();
results.values = newItems;
results.count = newItems.size();
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
mItems = (List<T>) results.values;
notifyDataSetChanged();
}
}
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.datetime.timezone;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.icu.text.DateFormat;
import android.icu.text.SimpleDateFormat;
import android.icu.util.Calendar;
import com.android.settings.R;
import com.android.settings.datetime.timezone.model.TimeZoneData;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
/**
* Render a list of {@class TimeZoneInfo} into the list view in {@class BaseTimeZonePicker}
*/
public abstract class BaseTimeZoneInfoPicker extends BaseTimeZonePicker {
protected static final String TAG = "RegionZoneSearchPicker";
protected ZoneAdapter mAdapter;
protected BaseTimeZoneInfoPicker(int titleResId, int searchHintResId,
boolean searchEnabled, boolean defaultExpandSearch) {
super(titleResId, searchHintResId, searchEnabled, defaultExpandSearch);
}
@Override
protected BaseTimeZoneAdapter createAdapter(TimeZoneData timeZoneData) {
mAdapter = new ZoneAdapter(getContext(), getAllTimeZoneInfos(timeZoneData),
this::onListItemClick, getLocale());
return mAdapter;
}
private void onListItemClick(int position) {
final TimeZoneInfo timeZoneInfo = mAdapter.getItem(position).mTimeZoneInfo;
getActivity().setResult(Activity.RESULT_OK, prepareResultData(timeZoneInfo));
getActivity().finish();
}
protected Intent prepareResultData(TimeZoneInfo selectedTimeZoneInfo) {
return new Intent().putExtra(EXTRA_RESULT_TIME_ZONE_ID, selectedTimeZoneInfo.getId());
}
public abstract List<TimeZoneInfo> getAllTimeZoneInfos(TimeZoneData timeZoneData);
protected static class ZoneAdapter extends BaseTimeZoneAdapter<TimeZoneInfoItem> {
public ZoneAdapter(Context context, List<TimeZoneInfo> timeZones,
OnListItemClickListener onListItemClickListener, Locale locale) {
super(createTimeZoneInfoItems(context, timeZones, locale),
onListItemClickListener, locale, true /* showItemSummary */);
}
private static List<TimeZoneInfoItem> createTimeZoneInfoItems(Context context,
List<TimeZoneInfo> timeZones, Locale locale) {
final DateFormat currentTimeFormat = new SimpleDateFormat(
android.text.format.DateFormat.getTimeFormatString(context), locale);
final ArrayList<TimeZoneInfoItem> results = new ArrayList<>(timeZones.size());
final Resources resources = context.getResources();
long i = 0;
for (TimeZoneInfo timeZone : timeZones) {
results.add(new TimeZoneInfoItem(i++, timeZone, resources, currentTimeFormat));
}
return results;
}
}
private static class TimeZoneInfoItem implements BaseTimeZoneAdapter.AdapterItem {
private final long mItemId;
private final TimeZoneInfo mTimeZoneInfo;
private final Resources mResources;
private final DateFormat mTimeFormat;
private final String mTitle;
private final String[] mSearchKeys;
private TimeZoneInfoItem(long itemId, TimeZoneInfo timeZoneInfo, Resources resources,
DateFormat timeFormat) {
mItemId = itemId;
mTimeZoneInfo = timeZoneInfo;
mResources = resources;
mTimeFormat = timeFormat;
mTitle = createTitle(timeZoneInfo);
mSearchKeys = new String[] { mTitle };
}
private static String createTitle(TimeZoneInfo timeZoneInfo) {
String name = timeZoneInfo.getExemplarLocation();
if (name == null) {
name = timeZoneInfo.getGenericName();
}
if (name == null && timeZoneInfo.getTimeZone().inDaylightTime(new Date())) {
name = timeZoneInfo.getDaylightName();
}
if (name == null) {
name = timeZoneInfo.getStandardName();
}
if (name == null) {
name = String.valueOf(timeZoneInfo.getGmtOffset());
}
return name;
}
@Override
public CharSequence getTitle() {
return mTitle;
}
@Override
public CharSequence getSummary() {
String name = mTimeZoneInfo.getGenericName();
if (name == null) {
if (mTimeZoneInfo.getTimeZone().inDaylightTime(new Date())) {
name = mTimeZoneInfo.getDaylightName();
} else {
name = mTimeZoneInfo.getStandardName();
}
}
if (name == null) {
return mTimeZoneInfo.getGmtOffset();
} else {
return SpannableUtil.getResourcesText(mResources,
R.string.zone_info_offset_and_name, mTimeZoneInfo.getGmtOffset(), name);
}
}
@Override
public String getIconText() {
return null;
}
@Override
public String getCurrentTime() {
return mTimeFormat.format(Calendar.getInstance(mTimeZoneInfo.getTimeZone()));
}
@Override
public long getItemId() {
return mItemId;
}
@Override
public String[] getSearchKeys() {
return mSearchKeys;
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.datetime.timezone;
import android.icu.util.TimeZone;
import com.android.settings.R;
import com.android.settings.datetime.timezone.model.TimeZoneData;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
/**
* Render a list of fixed offset time zone {@class TimeZoneInfo} into a list view.
*/
public class FixedOffsetPicker extends BaseTimeZoneInfoPicker {
/**
* Range of integer fixed UTC offsets shown in the pickers.
*/
private static final int MIN_HOURS_OFFSET = -14;
private static final int MAX_HOURS_OFFSET = +12;
public FixedOffsetPicker() {
super(R.string.date_time_select_fixed_offset_time_zones,
R.string.search_settings, false, false);
}
@Override
public List<TimeZoneInfo> getAllTimeZoneInfos(TimeZoneData timeZoneData) {
return loadFixedOffsets();
}
/**
* Returns a {@link TimeZoneInfo} for each fixed offset time zone, such as UTC or GMT+4. The
* returned list will be sorted in a reasonable way for display.
*/
private List<TimeZoneInfo> loadFixedOffsets() {
final TimeZoneInfo.Formatter formatter = new TimeZoneInfo.Formatter(getLocale(),
new Date());
final List<TimeZoneInfo> timeZoneInfos = new ArrayList<>();
timeZoneInfos.add(formatter.format(TimeZone.getFrozenTimeZone("Etc/UTC")));
for (int hoursOffset = MAX_HOURS_OFFSET; hoursOffset >= MIN_HOURS_OFFSET; --hoursOffset) {
if (hoursOffset == 0) {
// UTC is handled above, so don't add GMT +/-0 again.
continue;
}
final String id = String.format(Locale.US, "Etc/GMT%+d", hoursOffset);
timeZoneInfos.add(formatter.format(TimeZone.getFrozenTimeZone(id)));
}
return Collections.unmodifiableList(timeZoneInfos);
}
}

View File

@@ -0,0 +1,211 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.datetime.timezone;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Paint;
import android.icu.text.Collator;
import android.icu.text.LocaleDisplayNames;
import android.os.Bundle;
import android.util.Log;
import com.android.settings.R;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.datetime.timezone.model.FilteredCountryTimeZones;
import com.android.settings.datetime.timezone.model.TimeZoneData;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/**
* Render a list of regions into a list view.
*/
public class RegionSearchPicker extends BaseTimeZonePicker {
private static final int REQUEST_CODE_ZONE_PICKER = 1;
private static final String TAG = "RegionSearchPicker";
private BaseTimeZoneAdapter<RegionItem> mAdapter;
private TimeZoneData mTimeZoneData;
public RegionSearchPicker() {
super(R.string.date_time_select_region, R.string.search_settings, true, true);
}
@Override
protected BaseTimeZoneAdapter createAdapter(TimeZoneData timeZoneData) {
mTimeZoneData = timeZoneData;
mAdapter = new BaseTimeZoneAdapter<>(createAdapterItem(timeZoneData.getRegionIds()),
this::onListItemClick, getLocale(), false);
return mAdapter;
}
private void onListItemClick(int position) {
final String regionId = mAdapter.getItem(position).getId();
final FilteredCountryTimeZones countryTimeZones = mTimeZoneData.lookupCountryTimeZones(
regionId);
final Activity activity = getActivity();
if (countryTimeZones == null || countryTimeZones.getTimeZoneIds().isEmpty()) {
Log.e(TAG, "Region has no time zones: " + regionId);
activity.setResult(Activity.RESULT_CANCELED);
activity.finish();
return;
}
List<String> timeZoneIds = countryTimeZones.getTimeZoneIds();
// Choose the time zone associated the region if there is only one time zone in that region
if (timeZoneIds.size() == 1) {
final Intent resultData = new Intent()
.putExtra(EXTRA_RESULT_REGION_ID, regionId)
.putExtra(EXTRA_RESULT_TIME_ZONE_ID, timeZoneIds.get(0));
getActivity().setResult(Activity.RESULT_OK, resultData);
getActivity().finish();
} else {
// Launch the zone picker and let the user choose a time zone from the list of
// time zones associated with the region.
final Bundle args = new Bundle();
args.putString(RegionZonePicker.EXTRA_REGION_ID, regionId);
new SubSettingLauncher(getContext())
.setDestination(RegionZonePicker.class.getCanonicalName())
.setArguments(args)
.setSourceMetricsCategory(getMetricsCategory())
.setResultListener(this, REQUEST_CODE_ZONE_PICKER)
.launch();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_ZONE_PICKER) {
if (resultCode == Activity.RESULT_OK) {
getActivity().setResult(Activity.RESULT_OK, data);
}
getActivity().finish();
}
}
private List<RegionItem> createAdapterItem(Set<String> regionIds) {
final Collator collator = Collator.getInstance(getLocale());
final TreeSet<RegionItem> items = new TreeSet<>(new RegionInfoComparator(collator));
final Paint paint = new Paint();
final LocaleDisplayNames localeDisplayNames = LocaleDisplayNames.getInstance(getLocale());
long i = 0;
for (String regionId : regionIds) {
String name = localeDisplayNames.regionDisplayName(regionId);
String regionalIndicator = createRegionalIndicator(regionId, paint);
items.add(new RegionItem(i++, regionId, name, regionalIndicator));
}
return new ArrayList<>(items);
}
/**
* Create a Unicode Region Indicator Symbol for a given region id (a.k.a flag emoji). If the
* system can't render a flag for this region or the input is not a region id, this returns
* {@code null}.
*
* @param id the two-character region id.
* @param paint Paint contains the glyph
* @return a String representing the flag of the region or {@code null}.
*/
private static String createRegionalIndicator(String id, Paint paint) {
if (id.length() != 2) {
return null;
}
final char c1 = id.charAt(0);
final char c2 = id.charAt(1);
if ('A' > c1 || c1 > 'Z' || 'A' > c2 || c2 > 'Z') {
return null;
}
// Regional Indicator A is U+1F1E6 which is 0xD83C 0xDDE6 in UTF-16.
final String regionalIndicator = new String(
new char[]{0xd83c, (char) (0xdde6 - 'A' + c1), 0xd83c, (char) (0xdde6 - 'A' + c2)});
if (!paint.hasGlyph(regionalIndicator)) {
return null;
}
return regionalIndicator;
}
private static class RegionItem implements BaseTimeZoneAdapter.AdapterItem {
private final String mId;
private final String mName;
private final String mRegionalIndicator;
private final long mItemId;
private final String[] mSearchKeys;
RegionItem(long itemId, String id, String name, String regionalIndicator) {
mId = id;
mName = name;
mRegionalIndicator = regionalIndicator;
mItemId = itemId;
// Allow to search with ISO_3166-1 alpha-2 code. It's handy for english users in some
// countries, e.g. US for United States. It's not best search keys for users, but
// ICU doesn't have the data for the alias names of a region.
mSearchKeys = new String[] {mId, mName};
}
public String getId() {
return mId;
}
@Override
public CharSequence getTitle() {
return mName;
}
@Override
public CharSequence getSummary() {
return null;
}
@Override
public String getIconText() {
return mRegionalIndicator;
}
@Override
public String getCurrentTime() {
return null;
}
@Override
public long getItemId() {
return mItemId;
}
@Override
public String[] getSearchKeys() {
return mSearchKeys;
}
}
private static class RegionInfoComparator implements Comparator<RegionItem> {
private final Collator mCollator;
RegionInfoComparator(Collator collator) {
mCollator = collator;
}
@Override
public int compare(RegionItem r1, RegionItem r2) {
return mCollator.compare(r1.getTitle(), r2.getTitle());
}
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.datetime.timezone;
import android.content.Intent;
import android.icu.text.Collator;
import android.icu.util.TimeZone;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import com.android.settings.R;
import com.android.settings.datetime.timezone.model.FilteredCountryTimeZones;
import com.android.settings.datetime.timezone.model.TimeZoneData;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.TreeSet;
/**
* Given a region, render a list of time zone {@class TimeZoneInfo} into a list view.
*/
public class RegionZonePicker extends BaseTimeZoneInfoPicker {
public static final String EXTRA_REGION_ID =
"com.android.settings.datetime.timezone.region_id";
public RegionZonePicker() {
super(R.string.date_time_select_zone, R.string.search_settings, true, false);
}
/**
* Add the extra region id into the result.
*/
@Override
protected Intent prepareResultData(TimeZoneInfo selectedTimeZoneInfo) {
final Intent intent = super.prepareResultData(selectedTimeZoneInfo);
intent.putExtra(EXTRA_RESULT_REGION_ID, getArguments().getString(EXTRA_REGION_ID));
return intent;
}
@Override
public List<TimeZoneInfo> getAllTimeZoneInfos(TimeZoneData timeZoneData) {
if (getArguments() == null) {
Log.e(TAG, "getArguments() == null");
getActivity().finish();
}
String regionId = getArguments().getString(EXTRA_REGION_ID);
FilteredCountryTimeZones filteredCountryTimeZones = timeZoneData.lookupCountryTimeZones(
regionId);
if (filteredCountryTimeZones == null) {
Log.e(TAG, "region id is not valid: " + regionId);
getActivity().finish();
}
// It could be a timely operations if there are many time zones. A region in time zone data
// contains a maximum of 29 time zones currently. It may change in the future, but it's
// unlikely to be changed drastically.
return getRegionTimeZoneInfo(filteredCountryTimeZones.getTimeZoneIds());
}
/**
* Returns a list of {@link TimeZoneInfo} objects. The returned list will be sorted properly for
* display in the locale.It may be smaller than the input collection, if equivalent IDs are
* passed in.
*
* @param timeZoneIds a list of Olson IDs.
*/
public List<TimeZoneInfo> getRegionTimeZoneInfo(Collection<String> timeZoneIds) {
final TimeZoneInfo.Formatter formatter = new TimeZoneInfo.Formatter(getLocale(),
new Date());
final TreeSet<TimeZoneInfo> timeZoneInfos =
new TreeSet<>(new TimeZoneInfoComparator(Collator.getInstance(getLocale()),
new Date()));
for (final String timeZoneId : timeZoneIds) {
final TimeZone timeZone = TimeZone.getFrozenTimeZone(timeZoneId);
// Skip time zone ICU isn't aware.
if (timeZone.getID().equals(TimeZone.UNKNOWN_ZONE_ID)) {
continue;
}
timeZoneInfos.add(formatter.format(timeZone));
}
return Collections.unmodifiableList(new ArrayList<>(timeZoneInfos));
}
@VisibleForTesting
static class TimeZoneInfoComparator implements Comparator<TimeZoneInfo> {
private Collator mCollator;
private final Date mNow;
@VisibleForTesting
TimeZoneInfoComparator(Collator collator, Date now) {
mCollator = collator;
mNow = now;
}
@Override
public int compare(TimeZoneInfo tzi1, TimeZoneInfo tzi2) {
int result = Integer.compare(tzi1.getTimeZone().getOffset(mNow.getTime()),
tzi2.getTimeZone().getOffset(mNow.getTime()));
if (result == 0) {
result = Integer.compare(tzi1.getTimeZone().getRawOffset(),
tzi2.getTimeZone().getRawOffset());
}
if (result == 0) {
result = mCollator.compare(tzi1.getExemplarLocation(), tzi2.getExemplarLocation());
}
if (result == 0 && tzi1.getGenericName() != null && tzi2.getGenericName() != null) {
result = mCollator.compare(tzi1.getGenericName(), tzi2.getGenericName());
}
return result;
}
}
}

View File

@@ -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;
}
}

View File

@@ -57,7 +57,7 @@ public class TimeZoneData {
}
@VisibleForTesting
TimeZoneData(CountryZonesFinder countryZonesFinder) {
public TimeZoneData(CountryZonesFinder countryZonesFinder) {
mCountryZonesFinder = countryZonesFinder;
mRegionIds = getNormalizedRegionIds(mCountryZonesFinder.lookupAllCountryIsoCodes());
}

View File

@@ -0,0 +1,141 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.datetime.timezone;
import android.support.v7.widget.RecyclerView.AdapterDataObserver;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static com.google.common.truth.Truth.assertThat;
@RunWith(SettingsRobolectricTestRunner.class)
public class BaseTimeZoneAdapterTest {
@Test
public void testFilter() throws InterruptedException {
TestItem US = new TestItem("United States");
TestItem HK = new TestItem("Hong Kong");
TestItem UK = new TestItem("United Kingdom", new String[] { "United Kingdom",
"Great Britain"});
TestItem secretCountry = new TestItem("no name", new String[] { "Secret"});
List<TestItem> items = new ArrayList<>();
items.add(US);
items.add(HK);
items.add(UK);
items.add(secretCountry);
TestTimeZoneAdapter adapter = new TestTimeZoneAdapter(items);
assertSearch(adapter, "", items.toArray(new TestItem[items.size()]));
assertSearch(adapter, "Unit", US, UK);
assertSearch(adapter, "kon", HK);
assertSearch(adapter, "brit", UK);
assertSearch(adapter, "sec", secretCountry);
}
private void assertSearch(TestTimeZoneAdapter adapter , String searchText, TestItem... items)
throws InterruptedException {
Observer observer = new Observer(adapter);
adapter.getFilter().filter(searchText);
observer.await();
assertThat(adapter.getItemCount()).isEqualTo(items.length);
for (int i = 0; i < items.length; i++) {
assertThat(adapter.getItem(i)).isEqualTo(items[i]);
}
}
private static class Observer extends AdapterDataObserver {
private final CountDownLatch mLatch = new CountDownLatch(1);
private final TestTimeZoneAdapter mAdapter;
public Observer(TestTimeZoneAdapter adapter) {
mAdapter = adapter;
mAdapter.registerAdapterDataObserver(this);
}
@Override
public void onChanged() {
mAdapter.unregisterAdapterDataObserver(this);
mLatch.countDown();
}
public void await() throws InterruptedException {
mLatch.await(2L, TimeUnit.SECONDS);
}
}
private static class TestTimeZoneAdapter extends BaseTimeZoneAdapter<TestItem> {
public TestTimeZoneAdapter(List<TestItem> items) {
super(items, position -> {}, Locale.US, false);
}
}
private static class TestItem implements BaseTimeZoneAdapter.AdapterItem {
private final String mTitle;
private final String[] mSearchKeys;
TestItem(String title) {
this(title, new String[] { title });
}
TestItem(String title, String[] searchKeys) {
mTitle = title;
mSearchKeys = searchKeys;
}
@Override
public CharSequence getTitle() {
return mTitle;
}
@Override
public CharSequence getSummary() {
return null;
}
@Override
public String getIconText() {
return null;
}
@Override
public String getCurrentTime() {
return null;
}
@Override
public long getItemId() {
return 0;
}
@Override
public String[] getSearchKeys() {
return mSearchKeys;
}
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.datetime.timezone;
import android.content.Context;
import android.icu.util.TimeZone;
import com.android.settings.datetime.timezone.model.TimeZoneData;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import com.google.common.truth.Truth;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import static org.mockito.Mockito.mock;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(shadows = { BaseTimeZoneInfoPickerTest.ShadowDataFormat.class })
public class BaseTimeZoneInfoPickerTest {
@Implements(android.text.format.DateFormat.class)
public static class ShadowDataFormat {
public static String sTimeFormatString = "";
@Implementation
public static String getTimeFormatString(Context context) {
return sTimeFormatString;
}
}
/**
* Verify the summary, title, and time label in a time zone item are formatted properly.
*/
@Test
public void createAdapter_matchTimeZoneInfoAndOrder() {
ShadowDataFormat.sTimeFormatString = "HH:MM";
BaseTimeZoneInfoPicker picker = new TestBaseTimeZoneInfoPicker();
BaseTimeZoneAdapter adapter = picker.createAdapter(mock(TimeZoneData.class));
Truth.assertThat(adapter.getItemCount()).isEqualTo(2);
BaseTimeZoneAdapter.AdapterItem item1 = adapter.getItem(0);
Truth.assertThat(item1.getTitle().toString()).isEqualTo("Los Angeles");
Truth.assertThat(item1.getSummary().toString()).isEqualTo("Pacific Time (GMT-08:00)");
Truth.assertThat(item1.getCurrentTime())
.hasLength(ShadowDataFormat.sTimeFormatString.length());
BaseTimeZoneAdapter.AdapterItem item2 = adapter.getItem(1);
Truth.assertThat(item2.getTitle().toString()).isEqualTo("New York");
Truth.assertThat(item2.getSummary().toString()).isEqualTo("Eastern Time (GMT-05:00)");
Truth.assertThat(item2.getCurrentTime())
.hasLength(ShadowDataFormat.sTimeFormatString.length());
}
public static class TestBaseTimeZoneInfoPicker extends BaseTimeZoneInfoPicker {
public TestBaseTimeZoneInfoPicker() {
super(0, 0, false, false);
}
@Override
public List<TimeZoneInfo> getAllTimeZoneInfos(TimeZoneData timeZoneData) {
TimeZoneInfo zone1 = new TimeZoneInfo.Builder(
TimeZone.getFrozenTimeZone("America/Los_Angeles"))
.setGenericName("Pacific Time")
.setStandardName("Pacific Standard Time")
.setDaylightName("Pacific Daylight Time")
.setExemplarLocation("Los Angeles")
.setGmtOffset("GMT-08:00")
.setItemId(0)
.build();
TimeZoneInfo zone2 = new TimeZoneInfo.Builder(
TimeZone.getFrozenTimeZone("America/New_York"))
.setGenericName("Eastern Time")
.setStandardName("Eastern Standard Time")
.setDaylightName("Eastern Daylight Time")
.setExemplarLocation("New York")
.setGmtOffset("GMT-05:00")
.setItemId(1)
.build();
return Arrays.asList(zone1, zone2);
}
// Make the method public
@Override
public BaseTimeZoneAdapter createAdapter(TimeZoneData timeZoneData) {
return super.createAdapter(timeZoneData);
}
@Override
protected Locale getLocale() {
return Locale.US;
}
@Override
public Context getContext() {
return RuntimeEnvironment.application;
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.datetime.timezone;
import com.android.settings.datetime.timezone.model.TimeZoneData;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import libcore.util.CountryZonesFinder;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(SettingsRobolectricTestRunner.class)
public class FixedOffsetPickerTest {
@Test
public void getAllTimeZoneInfos_containsUtcAndGmtZones() {
List regionList = Collections.emptyList();
CountryZonesFinder finder = mock(CountryZonesFinder.class);
when(finder.lookupAllCountryIsoCodes()).thenReturn(regionList);
FixedOffsetPicker picker = new FixedOffsetPicker() {
@Override
protected Locale getLocale() {
return Locale.US;
}
};
List<TimeZoneInfo> infos = picker.getAllTimeZoneInfos(new TimeZoneData(finder));
List<String> tzIds = infos.stream().map(info -> info.getId()).collect(Collectors.toList());
tzIds.contains("Etc/Utc");
tzIds.contains("Etc/GMT-12");
tzIds.contains("Etc/GMT+14");
}
}

View File

@@ -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");
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.datetime.timezone;
import android.icu.text.Collator;
import com.android.settings.datetime.timezone.RegionZonePicker.TimeZoneInfoComparator;
import com.android.settings.datetime.timezone.TimeZoneInfo.Formatter;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import static com.google.common.truth.Truth.assertThat;
@RunWith(SettingsRobolectricTestRunner.class)
public class RegionZonePickerTest {
@Test
public void compareTimeZoneInfo_matchGmtOrder() {
Date now = new Date(0); // 00:00 1, Jan 1970
Formatter formatter = new Formatter(Locale.US, now);
TimeZoneInfo timeZone1 = formatter.format("Pacific/Honolulu");
TimeZoneInfo timeZone2 = formatter.format("America/Los_Angeles");
TimeZoneInfo timeZone3 = formatter.format("America/Indiana/Marengo");
TimeZoneInfo timeZone4 = formatter.format("America/New_York");
TimeZoneInfoComparator comparator =
new TimeZoneInfoComparator(Collator.getInstance(Locale.US), now);
// Verify the sorted order
List<TimeZoneInfo> list = Arrays.asList(timeZone4, timeZone2, timeZone3, timeZone1);
Collections.sort(list, comparator);
assertThat(list).isEqualTo(Arrays.asList(timeZone1, timeZone2, timeZone3, timeZone4));
}
}

View File

@@ -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)");
}
}