1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.datetime.timezone;
18 
19 import android.icu.text.BreakIterator;
20 import android.text.TextUtils;
21 import android.view.LayoutInflater;
22 import android.view.View;
23 import android.view.ViewGroup;
24 import android.widget.Filter;
25 import android.widget.TextView;
26 
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 import androidx.annotation.VisibleForTesting;
30 import androidx.annotation.WorkerThread;
31 import androidx.recyclerview.widget.RecyclerView;
32 
33 import com.android.settings.R;
34 import com.android.settings.datetime.timezone.BaseTimeZonePicker.OnListItemClickListener;
35 
36 import java.util.ArrayList;
37 import java.util.List;
38 import java.util.Locale;
39 
40 /**
41  * Used with {@class BaseTimeZonePicker}. It renders text in each item into list view. A list of
42  * {@class AdapterItem} must be provided when an instance is created.
43  */
44 public class BaseTimeZoneAdapter<T extends BaseTimeZoneAdapter.AdapterItem>
45         extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
46     @VisibleForTesting
47     static final int TYPE_HEADER = 0;
48     @VisibleForTesting
49     static final int TYPE_ITEM = 1;
50 
51     private final List<T> mOriginalItems;
52     private final OnListItemClickListener<T> mOnListItemClickListener;
53     private final Locale mLocale;
54     private final boolean mShowItemSummary;
55     private final boolean mShowHeader;
56     private final CharSequence mHeaderText;
57 
58     private List<T> mItems;
59     private ArrayFilter mFilter;
60 
61     /**
62      * @param headerText the text shown in the header, or null to show no header.
63      */
BaseTimeZoneAdapter(List<T> items, OnListItemClickListener<T> onListItemClickListener, Locale locale, boolean showItemSummary, @Nullable CharSequence headerText)64     public BaseTimeZoneAdapter(List<T> items, OnListItemClickListener<T> onListItemClickListener,
65             Locale locale, boolean showItemSummary, @Nullable CharSequence headerText) {
66         mOriginalItems = items;
67         mItems = items;
68         mOnListItemClickListener = onListItemClickListener;
69         mLocale = locale;
70         mShowItemSummary = showItemSummary;
71         mShowHeader = headerText != null;
72         mHeaderText = headerText;
73         setHasStableIds(true);
74     }
75 
76     @NonNull
77     @Override
onCreateViewHolder(@onNull ViewGroup parent, int viewType)78     public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
79         LayoutInflater inflater = LayoutInflater.from(parent.getContext());
80         switch (viewType) {
81             case TYPE_HEADER: {
82                 final View view = inflater.inflate(
83                         R.layout.time_zone_search_header,
84                         parent, false);
85                 return new HeaderViewHolder(view);
86             }
87             case TYPE_ITEM: {
88                 final View view = inflater.inflate(R.layout.time_zone_search_item, parent, false);
89                 return new ItemViewHolder(view, mOnListItemClickListener);
90             }
91             default:
92                 throw new IllegalArgumentException("Unexpected viewType: " + viewType);
93         }
94     }
95 
96     @Override
onBindViewHolder(@onNull RecyclerView.ViewHolder holder, int position)97     public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
98         if (holder instanceof HeaderViewHolder) {
99             ((HeaderViewHolder) holder).setText(mHeaderText);
100         } else if (holder instanceof ItemViewHolder) {
101             ItemViewHolder<T> itemViewHolder = (ItemViewHolder<T>) holder;
102             itemViewHolder.setAdapterItem(getDataItem(position));
103             itemViewHolder.mSummaryFrame.setVisibility(mShowItemSummary ? View.VISIBLE : View.GONE);
104         }
105     }
106 
107     @Override
getItemId(int position)108     public long getItemId(int position) {
109         // Data item can't have negative id
110         return isPositionHeader(position) ? -1 : getDataItem(position).getItemId();
111     }
112 
113     @Override
getItemCount()114     public int getItemCount() {
115         return mItems.size() + getHeaderCount();
116     }
117 
118     @Override
getItemViewType(int position)119     public int getItemViewType(int position) {
120         return isPositionHeader(position) ? TYPE_HEADER : TYPE_ITEM;
121     }
122 
123     /*
124      * Avoid being overridden by making the method final, since constructor shouldn't invoke
125      * overridable method.
126      */
127     @Override
setHasStableIds(boolean hasStableIds)128     public final void setHasStableIds(boolean hasStableIds) {
129         super.setHasStableIds(hasStableIds);
130     }
131 
getHeaderCount()132     private int getHeaderCount() {
133         return mShowHeader ? 1 : 0;
134     }
135 
isPositionHeader(int position)136     private boolean isPositionHeader(int position) {
137         return mShowHeader && position == 0;
138     }
139 
140     @NonNull
getFilter()141     public ArrayFilter getFilter() {
142         if (mFilter == null) {
143             mFilter = new ArrayFilter();
144         }
145         return mFilter;
146     }
147 
148     /**
149      * @throws IndexOutOfBoundsException if the view type at the position is a header
150      */
151     @VisibleForTesting
getDataItem(int position)152     public T getDataItem(int position) {
153         return mItems.get(position - getHeaderCount());
154     }
155 
156     public interface AdapterItem {
getTitle()157         CharSequence getTitle();
158 
getSummary()159         CharSequence getSummary();
160 
getIconText()161         String getIconText();
162 
getCurrentTime()163         String getCurrentTime();
164 
165         /**
166          * @return unique non-negative number
167          */
getItemId()168         long getItemId();
169 
getSearchKeys()170         String[] getSearchKeys();
171     }
172 
173     private static class HeaderViewHolder extends RecyclerView.ViewHolder {
174         private final TextView mTextView;
175 
HeaderViewHolder(View itemView)176         public HeaderViewHolder(View itemView) {
177             super(itemView);
178             mTextView = itemView.findViewById(android.R.id.title);
179         }
180 
setText(CharSequence text)181         public void setText(CharSequence text) {
182             mTextView.setText(text);
183         }
184     }
185 
186     @VisibleForTesting
187     public static class ItemViewHolder<T extends BaseTimeZoneAdapter.AdapterItem>
188             extends RecyclerView.ViewHolder implements View.OnClickListener {
189 
190         final OnListItemClickListener<T> mOnListItemClickListener;
191         final View mSummaryFrame;
192         final TextView mTitleView;
193         final TextView mIconTextView;
194         final TextView mSummaryView;
195         final TextView mTimeView;
196         private T mItem;
197 
ItemViewHolder(View itemView, OnListItemClickListener<T> onListItemClickListener)198         public ItemViewHolder(View itemView, OnListItemClickListener<T> onListItemClickListener) {
199             super(itemView);
200             itemView.setOnClickListener(this);
201             mSummaryFrame = itemView.findViewById(R.id.summary_frame);
202             mTitleView = itemView.findViewById(android.R.id.title);
203             mIconTextView = itemView.findViewById(R.id.icon_text);
204             mSummaryView = itemView.findViewById(android.R.id.summary);
205             mTimeView = itemView.findViewById(R.id.current_time);
206             mOnListItemClickListener = onListItemClickListener;
207         }
208 
setAdapterItem(T item)209         public void setAdapterItem(T item) {
210             mItem = item;
211             mTitleView.setText(item.getTitle());
212             mIconTextView.setText(item.getIconText());
213             mSummaryView.setText(item.getSummary());
214             mTimeView.setText(item.getCurrentTime());
215         }
216 
217         @Override
onClick(View v)218         public void onClick(View v) {
219             mOnListItemClickListener.onListItemClick(mItem);
220         }
221     }
222 
223     /**
224      * <p>An array filter constrains the content of the array adapter with
225      * a prefix. Each item that does not start with the supplied prefix
226      * is removed from the list.</p>
227      *
228      * The filtering operation is not optimized, due to small data size (~260 regions),
229      * require additional pre-processing. Potentially, a trie structure can be used to match
230      * prefixes of the search keys.
231      */
232     @VisibleForTesting
233     public class ArrayFilter extends Filter {
234 
235         private BreakIterator mBreakIterator = BreakIterator.getWordInstance(mLocale);
236 
237         @WorkerThread
238         @Override
performFiltering(CharSequence prefix)239         protected FilterResults performFiltering(CharSequence prefix) {
240             final List<T> newItems;
241             if (TextUtils.isEmpty(prefix)) {
242                 newItems = mOriginalItems;
243             } else {
244                 final String prefixString = prefix.toString().toLowerCase(mLocale);
245                 newItems = new ArrayList<>();
246 
247                 for (T item : mOriginalItems) {
248                     outer:
249                     for (String searchKey : item.getSearchKeys()) {
250                         searchKey = searchKey.toLowerCase(mLocale);
251                         // First match against the whole, non-splitted value
252                         if (searchKey.startsWith(prefixString)) {
253                             newItems.add(item);
254                             break outer;
255                         } else {
256                             mBreakIterator.setText(searchKey);
257                             for (int wordStart = 0, wordLimit = mBreakIterator.next();
258                                     wordLimit != BreakIterator.DONE;
259                                     wordStart = wordLimit,
260                                             wordLimit = mBreakIterator.next()) {
261                                 if (mBreakIterator.getRuleStatus() != BreakIterator.WORD_NONE
262                                         && searchKey.startsWith(prefixString, wordStart)) {
263                                     newItems.add(item);
264                                     break outer;
265                                 }
266                             }
267                         }
268                     }
269                 }
270             }
271 
272             final FilterResults results = new FilterResults();
273             results.values = newItems;
274             results.count = newItems.size();
275 
276             return results;
277         }
278 
279         @VisibleForTesting
280         @Override
publishResults(CharSequence constraint, FilterResults results)281         public void publishResults(CharSequence constraint, FilterResults results) {
282             mItems = (List<T>) results.values;
283             notifyDataSetChanged();
284         }
285     }
286 }
287