• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.text.Normalizer;
37 import java.util.ArrayList;
38 import java.util.List;
39 import java.util.Locale;
40 import java.util.regex.Pattern;
41 
42 /**
43  * Used with {@class BaseTimeZonePicker}. It renders text in each item into list view. A list of
44  * {@class AdapterItem} must be provided when an instance is created.
45  */
46 public class BaseTimeZoneAdapter<T extends BaseTimeZoneAdapter.AdapterItem>
47         extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
48     @VisibleForTesting
49     static final int TYPE_HEADER = 0;
50     @VisibleForTesting
51     static final int TYPE_ITEM = 1;
52 
53     private static final Pattern PATTERN_REMOVE_DIACRITICS = Pattern.compile(
54             "\\p{InCombiningDiacriticalMarks}+");
55 
56     private final List<T> mOriginalItems;
57     private final OnListItemClickListener<T> mOnListItemClickListener;
58     private final Locale mLocale;
59     private final boolean mShowItemSummary;
60     private final boolean mShowHeader;
61     private final CharSequence mHeaderText;
62 
63     private List<T> mItems;
64     private ArrayFilter mFilter;
65 
66     /**
67      * @param headerText the text shown in the header, or null to show no header.
68      */
BaseTimeZoneAdapter(List<T> items, OnListItemClickListener<T> onListItemClickListener, Locale locale, boolean showItemSummary, @Nullable CharSequence headerText)69     public BaseTimeZoneAdapter(List<T> items, OnListItemClickListener<T> onListItemClickListener,
70             Locale locale, boolean showItemSummary, @Nullable CharSequence headerText) {
71         mOriginalItems = items;
72         mItems = items;
73         mOnListItemClickListener = onListItemClickListener;
74         mLocale = locale;
75         mShowItemSummary = showItemSummary;
76         mShowHeader = headerText != null;
77         mHeaderText = headerText;
78         setHasStableIds(true);
79     }
80 
81     @NonNull
82     @Override
onCreateViewHolder(@onNull ViewGroup parent, int viewType)83     public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
84         LayoutInflater inflater = LayoutInflater.from(parent.getContext());
85         switch (viewType) {
86             case TYPE_HEADER: {
87                 final View view = inflater.inflate(
88                         R.layout.time_zone_search_header,
89                         parent, false);
90                 return new HeaderViewHolder(view);
91             }
92             case TYPE_ITEM: {
93                 final View view = inflater.inflate(R.layout.time_zone_search_item, parent, false);
94                 return new ItemViewHolder(view, mOnListItemClickListener);
95             }
96             default:
97                 throw new IllegalArgumentException("Unexpected viewType: " + viewType);
98         }
99     }
100 
101     @Override
onBindViewHolder(@onNull RecyclerView.ViewHolder holder, int position)102     public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
103         if (holder instanceof HeaderViewHolder) {
104             ((HeaderViewHolder) holder).setText(mHeaderText);
105         } else if (holder instanceof ItemViewHolder) {
106             ItemViewHolder<T> itemViewHolder = (ItemViewHolder<T>) holder;
107             itemViewHolder.setAdapterItem(getDataItem(position));
108             itemViewHolder.mSummaryFrame.setVisibility(mShowItemSummary ? View.VISIBLE : View.GONE);
109         }
110     }
111 
112     @Override
getItemId(int position)113     public long getItemId(int position) {
114         // Data item can't have negative id
115         return isPositionHeader(position) ? -1 : getDataItem(position).getItemId();
116     }
117 
118     @Override
getItemCount()119     public int getItemCount() {
120         return mItems.size() + getHeaderCount();
121     }
122 
123     @Override
getItemViewType(int position)124     public int getItemViewType(int position) {
125         return isPositionHeader(position) ? TYPE_HEADER : TYPE_ITEM;
126     }
127 
128     /*
129      * Avoid being overridden by making the method final, since constructor shouldn't invoke
130      * overridable method.
131      */
132     @Override
setHasStableIds(boolean hasStableIds)133     public final void setHasStableIds(boolean hasStableIds) {
134         super.setHasStableIds(hasStableIds);
135     }
136 
getHeaderCount()137     private int getHeaderCount() {
138         return mShowHeader ? 1 : 0;
139     }
140 
isPositionHeader(int position)141     private boolean isPositionHeader(int position) {
142         return mShowHeader && position == 0;
143     }
144 
145     @NonNull
getFilter()146     public ArrayFilter getFilter() {
147         if (mFilter == null) {
148             mFilter = new ArrayFilter();
149         }
150         return mFilter;
151     }
152 
153     /**
154      * @throws IndexOutOfBoundsException if the view type at the position is a header
155      */
156     @VisibleForTesting
getDataItem(int position)157     public T getDataItem(int position) {
158         return mItems.get(position - getHeaderCount());
159     }
160 
161     public interface AdapterItem {
getTitle()162         CharSequence getTitle();
163 
getSummary()164         CharSequence getSummary();
165 
getIconText()166         String getIconText();
167 
getCurrentTime()168         String getCurrentTime();
169 
170         /**
171          * @return unique non-negative number
172          */
getItemId()173         long getItemId();
174 
getSearchKeys()175         String[] getSearchKeys();
176     }
177 
178     private static class HeaderViewHolder extends RecyclerView.ViewHolder {
179         private final TextView mTextView;
180 
HeaderViewHolder(View itemView)181         public HeaderViewHolder(View itemView) {
182             super(itemView);
183             mTextView = itemView.findViewById(android.R.id.title);
184         }
185 
setText(CharSequence text)186         public void setText(CharSequence text) {
187             mTextView.setText(text);
188         }
189     }
190 
191     /**
192      * Removes diacritics (e.g. accents) from a string
193      */
removeDiacritics(final String str)194     private static String removeDiacritics(final String str) {
195         if (str == null || str.isEmpty()) {
196             return str;
197         }
198         // decomposes the original characters into a base character and a diacritic sign
199         final String decomposed = Normalizer.normalize(str, Normalizer.Form.NFKD);
200         // replaces the diacritic signs with empty strings
201         return PATTERN_REMOVE_DIACRITICS.matcher(decomposed).replaceAll("");
202     }
203 
204     @VisibleForTesting
205     public static class ItemViewHolder<T extends BaseTimeZoneAdapter.AdapterItem>
206             extends RecyclerView.ViewHolder implements View.OnClickListener {
207 
208         final OnListItemClickListener<T> mOnListItemClickListener;
209         final View mSummaryFrame;
210         final TextView mTitleView;
211         final TextView mIconTextView;
212         final TextView mSummaryView;
213         final TextView mTimeView;
214         private T mItem;
215 
ItemViewHolder(View itemView, OnListItemClickListener<T> onListItemClickListener)216         public ItemViewHolder(View itemView, OnListItemClickListener<T> onListItemClickListener) {
217             super(itemView);
218             itemView.setOnClickListener(this);
219             mSummaryFrame = itemView.findViewById(R.id.summary_frame);
220             mTitleView = itemView.findViewById(android.R.id.title);
221             mIconTextView = itemView.findViewById(R.id.icon_text);
222             mSummaryView = itemView.findViewById(android.R.id.summary);
223             mTimeView = itemView.findViewById(R.id.current_time);
224             mOnListItemClickListener = onListItemClickListener;
225         }
226 
setAdapterItem(T item)227         public void setAdapterItem(T item) {
228             mItem = item;
229             mTitleView.setText(item.getTitle());
230             mIconTextView.setText(item.getIconText());
231             mSummaryView.setText(item.getSummary());
232             mTimeView.setText(item.getCurrentTime());
233         }
234 
235         @Override
onClick(View v)236         public void onClick(View v) {
237             mOnListItemClickListener.onListItemClick(mItem);
238         }
239     }
240 
241     /**
242      * <p>An array filter constrains the content of the array adapter with
243      * a prefix. Each item that does not start with the supplied prefix
244      * is removed from the list.</p>
245      *
246      * The filtering operation is not optimized, due to small data size (~260 regions),
247      * require additional pre-processing. Potentially, a trie structure can be used to match
248      * prefixes of the search keys.
249      */
250     @VisibleForTesting
251     public class ArrayFilter extends Filter {
252 
253         private BreakIterator mBreakIterator = BreakIterator.getWordInstance(mLocale);
254 
255         @WorkerThread
256         @Override
performFiltering(CharSequence prefix)257         protected FilterResults performFiltering(CharSequence prefix) {
258             final List<T> newItems;
259             if (TextUtils.isEmpty(prefix)) {
260                 newItems = mOriginalItems;
261             } else {
262                 final String prefixString = removeDiacritics(
263                         prefix.toString().toLowerCase(mLocale));
264                 newItems = new ArrayList<>();
265 
266                 for (T item : mOriginalItems) {
267                     outer:
268                     for (String searchKey : item.getSearchKeys()) {
269                         searchKey = removeDiacritics(searchKey.toLowerCase(mLocale));
270                         // First match against the whole, non-splitted value
271                         if (searchKey.startsWith(prefixString)) {
272                             newItems.add(item);
273                             break outer;
274                         } else {
275                             mBreakIterator.setText(searchKey);
276                             for (int wordStart = 0, wordLimit = mBreakIterator.next();
277                                     wordLimit != BreakIterator.DONE;
278                                     wordStart = wordLimit,
279                                             wordLimit = mBreakIterator.next()) {
280                                 if (mBreakIterator.getRuleStatus() != BreakIterator.WORD_NONE
281                                         && searchKey.startsWith(prefixString, wordStart)) {
282                                     newItems.add(item);
283                                     break outer;
284                                 }
285                             }
286                         }
287                     }
288                 }
289             }
290 
291             final FilterResults results = new FilterResults();
292             results.values = newItems;
293             results.count = newItems.size();
294 
295             return results;
296         }
297 
298         @VisibleForTesting
299         @Override
publishResults(CharSequence constraint, FilterResults results)300         public void publishResults(CharSequence constraint, FilterResults results) {
301             mItems = (List<T>) results.values;
302             notifyDataSetChanged();
303         }
304     }
305 }
306