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:
99
res/layout/time_zone_search_item.xml
Normal file
99
res/layout/time_zone_search_item.xml
Normal 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>
|
26
res/menu/time_zone_base_search_menu.xml
Normal file
26
res/menu/time_zone_base_search_menu.xml
Normal 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>
|
@@ -748,20 +748,34 @@
|
|||||||
<string name="date_time_set_date_title">Date</string>
|
<string name="date_time_set_date_title">Date</string>
|
||||||
<!-- Date & time setting screen setting option title -->
|
<!-- Date & time setting screen setting option title -->
|
||||||
<string name="date_time_set_date">Set date</string>
|
<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 -->
|
<!-- Menu item on Select time zone screen -->
|
||||||
<string name="zone_list_menu_sort_alphabetically">Sort alphabetically</string>
|
<string name="zone_list_menu_sort_alphabetically">Sort alphabetically</string>
|
||||||
<!-- Menu item on Select time zone screen -->
|
<!-- Menu item on Select time zone screen -->
|
||||||
<string name="zone_list_menu_sort_by_timezone">Sort by time zone</string>
|
<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 -->
|
<!-- 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>
|
<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) -->
|
<!-- 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>
|
<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) -->
|
<!-- 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>
|
<string name="zone_time_type_standard">Standard time</string>
|
||||||
<!-- The menu item to switch to selecting a time zone by region (default) -->
|
<!-- 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) -->
|
<!-- 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
|
<!-- Title string shown above DatePicker, letting a user select system date
|
||||||
[CHAR LIMIT=20] -->
|
[CHAR LIMIT=20] -->
|
||||||
|
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
133
src/com/android/settings/datetime/timezone/RegionZonePicker.java
Normal file
133
src/com/android/settings/datetime/timezone/RegionZonePicker.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
@@ -57,7 +57,7 @@ public class TimeZoneData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
TimeZoneData(CountryZonesFinder countryZonesFinder) {
|
public TimeZoneData(CountryZonesFinder countryZonesFinder) {
|
||||||
mCountryZonesFinder = countryZonesFinder;
|
mCountryZonesFinder = countryZonesFinder;
|
||||||
mRegionIds = getNormalizedRegionIds(mCountryZonesFinder.lookupAllCountryIsoCodes());
|
mRegionIds = getNormalizedRegionIds(mCountryZonesFinder.lookupAllCountryIsoCodes());
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
@@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -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)");
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user