diff --git a/res/layout/time_zone_items_list.xml b/res/layout/time_zone_items_list.xml new file mode 100644 index 00000000000..8c9dd9421d6 --- /dev/null +++ b/res/layout/time_zone_items_list.xml @@ -0,0 +1,22 @@ + + + diff --git a/res/xml/time_zone_prefs.xml b/res/xml/time_zone_prefs.xml index f80de8c1e5c..598aa65f027 100644 --- a/res/xml/time_zone_prefs.xml +++ b/res/xml/time_zone_prefs.xml @@ -27,7 +27,7 @@ android:summary="@string/summary_placeholder" /> diff --git a/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java b/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java index 5a614a36888..f83d841e6af 100644 --- a/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java +++ b/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapter.java @@ -18,6 +18,7 @@ package com.android.settings.datetime.timezone; import android.icu.text.BreakIterator; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.annotation.WorkerThread; import android.support.v7.widget.RecyclerView; @@ -40,48 +41,98 @@ import java.util.Locale; * {@class AdapterItem} must be provided when an instance is created. */ public class BaseTimeZoneAdapter - extends RecyclerView.Adapter { + extends RecyclerView.Adapter { + @VisibleForTesting + static final int TYPE_HEADER = 0; + @VisibleForTesting + static final int TYPE_ITEM = 1; private final List mOriginalItems; private final OnListItemClickListener mOnListItemClickListener; private final Locale mLocale; private final boolean mShowItemSummary; + private final boolean mShowHeader; + private final CharSequence mHeaderText; private List mItems; private ArrayFilter mFilter; - public BaseTimeZoneAdapter(List items, OnListItemClickListener - onListItemClickListener, Locale locale, boolean showItemSummary) { + /** + * @param headerText the text shown in the header, or null to show no header. + */ + public BaseTimeZoneAdapter(List items, OnListItemClickListener onListItemClickListener, + Locale locale, boolean showItemSummary, @Nullable CharSequence headerText) { mOriginalItems = items; mItems = items; mOnListItemClickListener = onListItemClickListener; mLocale = locale; mShowItemSummary = showItemSummary; + mShowHeader = headerText != null; + mHeaderText = headerText; 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); + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + switch(viewType) { + case TYPE_HEADER: { + final View view = inflater.inflate(R.layout.preference_category_material_settings, + parent, false); + return new HeaderViewHolder(view); + } + case TYPE_ITEM: { + final View view = inflater.inflate(R.layout.time_zone_search_item, parent, false); + return new ItemViewHolder(view, mOnListItemClickListener); + } + default: + throw new IllegalArgumentException("Unexpected viewType: " + viewType); + } } @Override - public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) { - holder.setAdapterItem(mItems.get(position)); - holder.mSummaryFrame.setVisibility(mShowItemSummary ? View.VISIBLE : View.GONE); + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + if (holder instanceof HeaderViewHolder) { + ((HeaderViewHolder) holder).setText(mHeaderText); + } else if (holder instanceof ItemViewHolder) { + ItemViewHolder itemViewHolder = (ItemViewHolder) holder; + itemViewHolder.setAdapterItem(getDataItem(position)); + itemViewHolder.mSummaryFrame.setVisibility(mShowItemSummary ? View.VISIBLE : View.GONE); + } } @Override public long getItemId(int position) { - return getItem(position).getItemId(); + // Data item can't have negative id + return isPositionHeader(position) ? -1 : getDataItem(position).getItemId(); } @Override public int getItemCount() { - return mItems.size(); + return mItems.size() + getHeaderCount(); + } + + @Override + public int getItemViewType(int position) { + return isPositionHeader(position) ? TYPE_HEADER : TYPE_ITEM; + } + + /* + * Avoid being overridden by making the method final, since constructor shouldn't invoke + * overridable method. + */ + @Override + public final void setHasStableIds(boolean hasStableIds) { + super.setHasStableIds(hasStableIds); + } + + private int getHeaderCount() { + return mShowHeader ? 1 : 0; + } + + private boolean isPositionHeader(int position) { + return mShowHeader && position == 0; } public @NonNull ArrayFilter getFilter() { @@ -91,8 +142,12 @@ public class BaseTimeZoneAdapter return mFilter; } - public T getItem(int position) { - return mItems.get(position); + /** + * @throws IndexOutOfBoundsException if the view type at the position is a header + */ + @VisibleForTesting + public T getDataItem(int position) { + return mItems.get(position - getHeaderCount()); } public interface AdapterItem { @@ -100,10 +155,28 @@ public class BaseTimeZoneAdapter CharSequence getSummary(); String getIconText(); String getCurrentTime(); + + /** + * @return unique non-negative number + */ long getItemId(); String[] getSearchKeys(); } + private static class HeaderViewHolder extends RecyclerView.ViewHolder { + private final TextView mTextView; + + public HeaderViewHolder(View itemView) { + super(itemView); + mTextView = itemView.findViewById(android.R.id.title); + } + + public void setText(CharSequence text) { + mTextView.setText(text); + } + } + + @VisibleForTesting public static class ItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { diff --git a/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPicker.java b/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPicker.java index d7345428330..4d46c5c643f 100644 --- a/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPicker.java +++ b/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPicker.java @@ -23,6 +23,7 @@ import android.content.res.Resources; import android.icu.text.DateFormat; import android.icu.text.SimpleDateFormat; import android.icu.util.Calendar; +import android.support.annotation.Nullable; import com.android.settings.R; import com.android.settings.datetime.timezone.model.TimeZoneData; @@ -47,10 +48,17 @@ public abstract class BaseTimeZoneInfoPicker extends BaseTimeZonePicker { @Override protected BaseTimeZoneAdapter createAdapter(TimeZoneData timeZoneData) { mAdapter = new ZoneAdapter(getContext(), getAllTimeZoneInfos(timeZoneData), - this::onListItemClick, getLocale()); + this::onListItemClick, getLocale(), getHeaderText()); return mAdapter; } + /** + * @return the text shown in the header, or null to show no header. + */ + protected @Nullable CharSequence getHeaderText() { + return null; + } + private void onListItemClick(TimeZoneInfoItem item) { final TimeZoneInfo timeZoneInfo = item.mTimeZoneInfo; getActivity().setResult(Activity.RESULT_OK, prepareResultData(timeZoneInfo)); @@ -66,9 +74,11 @@ public abstract class BaseTimeZoneInfoPicker extends BaseTimeZonePicker { protected static class ZoneAdapter extends BaseTimeZoneAdapter { public ZoneAdapter(Context context, List timeZones, - OnListItemClickListener onListItemClickListener, Locale locale) { + OnListItemClickListener onListItemClickListener, Locale locale, + CharSequence headerText) { super(createTimeZoneInfoItems(context, timeZones, locale), - onListItemClickListener, locale, true /* showItemSummary */); + onListItemClickListener, locale, true /* showItemSummary */, + headerText /* headerText */); } private static List createTimeZoneInfoItems(Context context, diff --git a/src/com/android/settings/datetime/timezone/BaseTimeZonePicker.java b/src/com/android/settings/datetime/timezone/BaseTimeZonePicker.java index 5150045e0a5..7bf8abceb52 100644 --- a/src/com/android/settings/datetime/timezone/BaseTimeZonePicker.java +++ b/src/com/android/settings/datetime/timezone/BaseTimeZonePicker.java @@ -44,7 +44,7 @@ import java.util.Locale; * The search matches the prefix of words in the search text. */ public abstract class BaseTimeZonePicker extends InstrumentedFragment - implements SearchView.OnQueryTextListener{ + implements SearchView.OnQueryTextListener { public static final String EXTRA_RESULT_REGION_ID = "com.android.settings.datetime.timezone.result_region_id"; @@ -84,7 +84,7 @@ public abstract class BaseTimeZonePicker extends InstrumentedFragment @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final View view = inflater.inflate(R.layout.recycler_view, container, false); + final View view = inflater.inflate(R.layout.time_zone_items_list, container, false); mRecyclerView = view.findViewById(R.id.recycler_view); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, /* reverseLayout */ false)); diff --git a/src/com/android/settings/datetime/timezone/RegionSearchPicker.java b/src/com/android/settings/datetime/timezone/RegionSearchPicker.java index ca4e0bcddc5..134b6ac5896 100644 --- a/src/com/android/settings/datetime/timezone/RegionSearchPicker.java +++ b/src/com/android/settings/datetime/timezone/RegionSearchPicker.java @@ -48,7 +48,7 @@ public class RegionSearchPicker extends BaseTimeZonePicker { private TimeZoneData mTimeZoneData; public RegionSearchPicker() { - super(R.string.date_time_select_region, R.string.search_settings, true, true); + super(R.string.date_time_select_region, R.string.date_time_search_region, true, true); } @Override @@ -60,7 +60,8 @@ public class RegionSearchPicker extends BaseTimeZonePicker { protected BaseTimeZoneAdapter createAdapter(TimeZoneData timeZoneData) { mTimeZoneData = timeZoneData; mAdapter = new BaseTimeZoneAdapter<>(createAdapterItem(timeZoneData.getRegionIds()), - this::onListItemClick, getLocale(), false); + this::onListItemClick, getLocale(), false /* showItemSummary */, + null /* headerText */); return mAdapter; } diff --git a/src/com/android/settings/datetime/timezone/RegionZonePicker.java b/src/com/android/settings/datetime/timezone/RegionZonePicker.java index 37365a8a738..add50b8753a 100644 --- a/src/com/android/settings/datetime/timezone/RegionZonePicker.java +++ b/src/com/android/settings/datetime/timezone/RegionZonePicker.java @@ -18,7 +18,10 @@ package com.android.settings.datetime.timezone; import android.content.Intent; import android.icu.text.Collator; +import android.icu.text.LocaleDisplayNames; import android.icu.util.TimeZone; +import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.Log; @@ -43,8 +46,10 @@ public class RegionZonePicker extends BaseTimeZoneInfoPicker { public static final String EXTRA_REGION_ID = "com.android.settings.datetime.timezone.region_id"; + private @Nullable String mRegionName; + public RegionZonePicker() { - super(R.string.date_time_select_zone, R.string.search_settings, true, false); + super(R.string.date_time_set_timezone_title, R.string.search_settings, true, false); } @Override @@ -52,6 +57,21 @@ public class RegionZonePicker extends BaseTimeZoneInfoPicker { return MetricsProto.MetricsEvent.SETTINGS_ZONE_PICKER_TIME_ZONE; } + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final LocaleDisplayNames localeDisplayNames = LocaleDisplayNames.getInstance(getLocale()); + final String regionId = + getArguments() == null ? null : getArguments().getString(EXTRA_REGION_ID); + mRegionName = regionId == null ? null : localeDisplayNames.regionDisplayName(regionId); + } + + @Override + protected @Nullable CharSequence getHeaderText() { + return mRegionName; + } + /** * Add the extra region id into the result. */ @@ -67,6 +87,7 @@ public class RegionZonePicker extends BaseTimeZoneInfoPicker { if (getArguments() == null) { Log.e(TAG, "getArguments() == null"); getActivity().finish(); + return Collections.emptyList(); } String regionId = getArguments().getString(EXTRA_REGION_ID); @@ -75,6 +96,7 @@ public class RegionZonePicker extends BaseTimeZoneInfoPicker { if (filteredCountryTimeZones == null) { Log.e(TAG, "region id is not valid: " + regionId); getActivity().finish(); + return Collections.emptyList(); } // It could be a timely operations if there are many time zones. A region in time zone data diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapterTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapterTest.java index c85c598491a..a2406466321 100644 --- a/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapterTest.java +++ b/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZoneAdapterTest.java @@ -62,7 +62,7 @@ public class BaseTimeZoneAdapterTest { observer.await(); assertThat(adapter.getItemCount()).isEqualTo(items.length); for (int i = 0; i < items.length; i++) { - assertThat(adapter.getItem(i)).isEqualTo(items[i]); + assertThat(adapter.getDataItem(i)).isEqualTo(items[i]); } } @@ -90,7 +90,8 @@ public class BaseTimeZoneAdapterTest { private static class TestTimeZoneAdapter extends BaseTimeZoneAdapter { public TestTimeZoneAdapter(List items) { - super(items, position -> {}, Locale.US, false); + super(items, position -> {}, Locale.US, false /* showItemSummary */, + null /* headerText */); } } diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPickerTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPickerTest.java index 3491b03b32d..ef80968cbd7 100644 --- a/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPickerTest.java +++ b/tests/robotests/src/com/android/settings/datetime/timezone/BaseTimeZoneInfoPickerTest.java @@ -61,13 +61,13 @@ public class BaseTimeZoneInfoPickerTest { BaseTimeZoneAdapter adapter = picker.createAdapter(mock(TimeZoneData.class)); Truth.assertThat(adapter.getItemCount()).isEqualTo(2); - BaseTimeZoneAdapter.AdapterItem item1 = adapter.getItem(0); + BaseTimeZoneAdapter.AdapterItem item1 = adapter.getDataItem(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); + BaseTimeZoneAdapter.AdapterItem item2 = adapter.getDataItem(1); Truth.assertThat(item2.getTitle().toString()).isEqualTo("New York"); Truth.assertThat(item2.getSummary().toString()).isEqualTo("Eastern Time (GMT-05:00)"); Truth.assertThat(item2.getCurrentTime()) diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/FixedOffsetPickerTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/FixedOffsetPickerTest.java index 007d3c508e0..9d650cc45af 100644 --- a/tests/robotests/src/com/android/settings/datetime/timezone/FixedOffsetPickerTest.java +++ b/tests/robotests/src/com/android/settings/datetime/timezone/FixedOffsetPickerTest.java @@ -65,10 +65,10 @@ public class FixedOffsetPickerTest { TestFixedOffsetPicker picker = new TestFixedOffsetPicker(); BaseTimeZoneAdapter adapter = picker.createAdapter(new TimeZoneData(mFinder)); assertThat(adapter.getItemCount()).isEqualTo(12 + 1 + 14); // 27 GMT offsets from -12 to +14 - AdapterItem utc = adapter.getItem(0); + AdapterItem utc = adapter.getDataItem(0); assertThat(utc.getTitle().toString()).isEqualTo("Coordinated Universal Time"); assertThat(utc.getSummary().toString()).isEqualTo("GMT+00:00"); - AdapterItem gmtMinus12 = adapter.getItem(1); + AdapterItem gmtMinus12 = adapter.getDataItem(1); assertThat(gmtMinus12.getTitle().toString()).isEqualTo("GMT-12:00"); assertThat(gmtMinus12.getSummary().toString()).isEmpty(); } diff --git a/tests/robotests/src/com/android/settings/datetime/timezone/RegionSearchPickerTest.java b/tests/robotests/src/com/android/settings/datetime/timezone/RegionSearchPickerTest.java index 8da9cbf1c9b..02a3122cb9e 100644 --- a/tests/robotests/src/com/android/settings/datetime/timezone/RegionSearchPickerTest.java +++ b/tests/robotests/src/com/android/settings/datetime/timezone/RegionSearchPickerTest.java @@ -66,7 +66,7 @@ public class RegionSearchPickerTest { RegionSearchPicker picker = new RegionSearchPicker(); BaseTimeZoneAdapter adapter = picker.createAdapter(new TimeZoneData(finder)); assertEquals(1, adapter.getItemCount()); - AdapterItem item = adapter.getItem(0); + AdapterItem item = adapter.getDataItem(0); assertEquals("United States", item.getTitle().toString()); assertThat(Arrays.asList(item.getSearchKeys())).contains("United States"); } @@ -86,8 +86,8 @@ public class RegionSearchPickerTest { RegionSearchPicker picker = new RegionSearchPicker(); BaseTimeZoneAdapter adapter = picker.createAdapter(new TimeZoneData(finder)); // Prepare and bind a new ItemViewHolder with United States - ItemViewHolder viewHolder = adapter.onCreateViewHolder( - new LinearLayout(RuntimeEnvironment.application), 0); + ItemViewHolder viewHolder = (ItemViewHolder) adapter.onCreateViewHolder( + new LinearLayout(RuntimeEnvironment.application), BaseTimeZoneAdapter.TYPE_ITEM); adapter.onBindViewHolder(viewHolder, 0); assertEquals(1, adapter.getItemCount());