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
(cherry picked from commit fbd30acef0)
This commit is contained in:
Victor Chang
2018-02-28 19:31:31 +00:00
parent e8acc0c4bd
commit 2b6876ccab
17 changed files with 1603 additions and 3 deletions

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