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