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