1 // Copyright 2018 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.components.browser_ui.contacts_picker; 6 7 import android.content.ContentResolver; 8 import android.content.Context; 9 import android.text.TextUtils; 10 import android.view.LayoutInflater; 11 import android.view.View; 12 import android.view.ViewGroup; 13 14 import androidx.annotation.CallSuper; 15 import androidx.annotation.IntDef; 16 import androidx.annotation.Nullable; 17 import androidx.annotation.VisibleForTesting; 18 import androidx.recyclerview.widget.RecyclerView; 19 import androidx.recyclerview.widget.RecyclerView.Adapter; 20 21 import org.chromium.base.task.AsyncTask; 22 23 import java.lang.annotation.Retention; 24 import java.lang.annotation.RetentionPolicy; 25 import java.util.ArrayList; 26 import java.util.List; 27 import java.util.Locale; 28 29 /** 30 * A data adapter for the Contacts Picker. 31 * 32 * This class is abstract and embedders must specialize it to provide access to the active 33 * user's contact information. 34 */ 35 public abstract class PickerAdapter extends Adapter<RecyclerView.ViewHolder> 36 implements ContactsFetcherWorkerTask.ContactsRetrievedCallback, 37 TopView.ChipToggledCallback { 38 /** 39 * A ViewHolder for the top-most view in the RecyclerView. The view it contains has a 40 * checkbox and some multi-line text that goes with it, so clicks on either text line 41 * should be treated as clicks for the checkbox (hence the onclick forwarding). 42 */ 43 class TopViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { 44 TopView mItemView; 45 TopViewHolder(TopView itemView)46 public TopViewHolder(TopView itemView) { 47 super(itemView); 48 mItemView = itemView; 49 mItemView.setOnClickListener(this); 50 } 51 52 @Override onClick(View view)53 public void onClick(View view) { 54 // TODO(finnur): Make the explanation text non-clickable. 55 mItemView.toggle(); 56 } 57 } 58 59 /** 60 * The types of filters supported. 61 */ 62 @Retention(RetentionPolicy.SOURCE) 63 @IntDef({FilterType.NAMES, FilterType.EMAILS, FilterType.TELEPHONES, FilterType.ADDRESSES, 64 FilterType.ICONS}) 65 public @interface FilterType { 66 int NAMES = 0; 67 int EMAILS = 1; 68 int TELEPHONES = 2; 69 int ADDRESSES = 3; 70 int ICONS = 4; 71 } 72 73 /** 74 * The types of views supported. 75 */ 76 @Retention(RetentionPolicy.SOURCE) 77 @IntDef({ViewType.SELECT_ALL_CHECKBOX, ViewType.CONTACT_DETAILS}) 78 private @interface ViewType { 79 int SELECT_ALL_CHECKBOX = 0; 80 int CONTACT_DETAILS = 1; 81 } 82 83 // The current context to use. 84 private Context mContext; 85 86 // The category view to use to show the contacts. 87 private PickerCategoryView mCategoryView; 88 89 // The view at the top of the RecyclerView (disclaimer and select all functionality). 90 private TopView mTopView; 91 92 // The origin the data will be shared with, formatted for display with the scheme omitted. 93 private String mFormattedOrigin; 94 95 // The content resolver to query data from. 96 private ContentResolver mContentResolver; 97 98 // The full list of all registered contacts on the device. 99 private ArrayList<ContactDetails> mContactDetails; 100 101 // The email address of the owner of the device. 102 @Nullable 103 private String mOwnerEmail; 104 105 // The async worker task to use for fetching the contact details. 106 private ContactsFetcherWorkerTask mWorkerTask; 107 108 // Whether the user has switched to search mode. 109 private boolean mSearchMode; 110 111 // A list of search result indices into the larger data set. 112 private ArrayList<Integer> mSearchResults; 113 114 // Whether to include addresses in the returned results. 115 private static boolean sIncludeAddresses; 116 117 // Whether to include names in the returned results. 118 private static boolean sIncludeNames; 119 120 // Whether to include emails in the returned results. 121 private static boolean sIncludeEmails; 122 123 // Whether to include telephone numbers in the returned results. 124 private static boolean sIncludeTelephones; 125 126 // Whether to include icons in the returned results. 127 private static boolean sIncludeIcons; 128 129 // A list of contacts to use for testing (instead of querying Android). 130 private static ArrayList<ContactDetails> sTestContacts; 131 132 // An owner email to use when testing. 133 private static String sTestOwnerEmail; 134 135 /** 136 * The PickerAdapter constructor. 137 * @param categoryView The category view to use to show the contacts. 138 * @param context The current context. 139 * @param formattedOrigin The origin the data will be shared with. 140 */ 141 @CallSuper init(PickerCategoryView categoryView, Context context, String formattedOrigin)142 public void init(PickerCategoryView categoryView, Context context, String formattedOrigin) { 143 mContext = context; 144 mCategoryView = categoryView; 145 mContentResolver = context.getContentResolver(); 146 mFormattedOrigin = formattedOrigin; 147 sIncludeAddresses = true; 148 sIncludeNames = true; 149 sIncludeEmails = true; 150 sIncludeTelephones = true; 151 sIncludeIcons = true; 152 153 if (getAllContacts() == null && sTestContacts == null) { 154 mWorkerTask = new ContactsFetcherWorkerTask(context, this, mCategoryView.includeNames, 155 mCategoryView.includeEmails, mCategoryView.includeTel, 156 mCategoryView.includeAddresses); 157 mWorkerTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 158 } else { 159 contactsRetrieved(sTestContacts); 160 } 161 } 162 163 /** 164 * Set whether the user has switched to search mode. 165 * @param searchMode True when we are in search mode. 166 */ setSearchMode(boolean searchMode)167 public void setSearchMode(boolean searchMode) { 168 mSearchMode = searchMode; 169 notifyDataSetChanged(); 170 } 171 172 /** 173 * Sets the search query (filter) for the contact list. Filtering is by display name. 174 * @param query The search term to use. 175 */ setSearchString(String query)176 public void setSearchString(String query) { 177 if (query.equals("")) { 178 if (mSearchResults == null) return; 179 mSearchResults.clear(); 180 mSearchResults = null; 181 } else { 182 mSearchResults = new ArrayList<Integer>(); 183 Integer count = 0; 184 String query_lower = query.toLowerCase(Locale.getDefault()); 185 for (ContactDetails contact : mContactDetails) { 186 if (contact.getDisplayName().toLowerCase(Locale.getDefault()).contains(query_lower) 187 || contact.getContactDetailsAsString(includesAddresses(), includesEmails(), 188 includesTelephones()) 189 .toLowerCase(Locale.getDefault()) 190 .contains(query_lower)) { 191 mSearchResults.add(count); 192 } 193 count++; 194 } 195 } 196 notifyDataSetChanged(); 197 } 198 199 /** 200 * Fetches all known contacts. 201 * @return The contact list as an array. 202 */ getAllContacts()203 public ArrayList<ContactDetails> getAllContacts() { 204 return mContactDetails; 205 } 206 getOwnerEmail()207 protected String getOwnerEmail() { 208 return mOwnerEmail; 209 } 210 update()211 protected void update() { 212 if (mTopView != null) mTopView.updateContactCount(mContactDetails.size()); 213 notifyDataSetChanged(); 214 } 215 216 // Abstract methods: 217 218 /** 219 * Called to get the email for the current user. 220 * The default is null, but some embedder-specific specializations may override this method to 221 * facilitate showing the owner's contact card at the top of the picker. 222 * @return the email address of the current user/owner. 223 */ 224 @Nullable findOwnerEmail()225 protected abstract String findOwnerEmail(); 226 227 /** 228 * Called to add an entry which represents the current user to the given list. 229 * As with {@link #findOwnerEmail}, embedders may override this to make sure the current user's 230 * contact card is shown, or may no-op. 231 * @param contacts the list which is missing an entry for the active user, and to which such an 232 * entry should be pre-pended. 233 */ addOwnerInfoToContacts(ArrayList<ContactDetails> contacts)234 protected abstract void addOwnerInfoToContacts(ArrayList<ContactDetails> contacts); 235 236 // ContactsFetcherWorkerTask.ContactsRetrievedCallback: 237 238 @Override contactsRetrieved(ArrayList<ContactDetails> contacts)239 public void contactsRetrieved(ArrayList<ContactDetails> contacts) { 240 mOwnerEmail = sTestOwnerEmail != null ? sTestOwnerEmail : findOwnerEmail(); 241 242 if (!processOwnerInfo(contacts, mOwnerEmail)) addOwnerInfoToContacts(contacts); 243 mContactDetails = contacts; 244 update(); 245 } 246 247 // RecyclerView.Adapter: 248 249 @Override getItemViewType(int position)250 public int getItemViewType(int position) { 251 if (position == 0 && !mSearchMode) return ViewType.SELECT_ALL_CHECKBOX; 252 return ViewType.CONTACT_DETAILS; 253 } 254 255 @Override onCreateViewHolder(ViewGroup parent, int viewType)256 public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 257 switch (viewType) { 258 case ViewType.SELECT_ALL_CHECKBOX: { 259 mTopView = (TopView) LayoutInflater.from(parent.getContext()) 260 .inflate(R.layout.top_view, parent, false); 261 mTopView.setSiteString(mFormattedOrigin); 262 mTopView.registerSelectAllCallback(mCategoryView); 263 mTopView.registerChipToggledCallback(this); 264 mTopView.updateCheckboxVisibility(mCategoryView.multiSelectionAllowed()); 265 mTopView.updateChipVisibility(mCategoryView.includeNames, 266 mCategoryView.includeAddresses, mCategoryView.includeEmails, 267 mCategoryView.includeTel, mCategoryView.includeIcons); 268 mCategoryView.setTopView(mTopView); 269 if (mContactDetails != null) mTopView.updateContactCount(mContactDetails.size()); 270 return new TopViewHolder(mTopView); 271 } 272 case ViewType.CONTACT_DETAILS: { 273 ContactView itemView = (ContactView) LayoutInflater.from(parent.getContext()) 274 .inflate(R.layout.contact_view, parent, false); 275 itemView.setCategoryView(mCategoryView); 276 return new ContactViewHolder(itemView, mCategoryView, mContentResolver); 277 } 278 } 279 return null; 280 } 281 282 @Override onBindViewHolder(RecyclerView.ViewHolder holder, int position)283 public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { 284 switch (holder.getItemViewType()) { 285 case ViewType.SELECT_ALL_CHECKBOX: 286 // There's no need to bind the Select All view. 287 return; 288 case ViewType.CONTACT_DETAILS: 289 ContactViewHolder contactHolder = (ContactViewHolder) holder; 290 ContactDetails contact; 291 if (!mSearchMode || mSearchResults == null) { 292 // Subtract one because the first view is the Select All checkbox when not in 293 // search mode. 294 contact = mContactDetails.get(position - (mSearchMode ? 0 : 1)); 295 } else { 296 Integer index = mSearchResults.get(position); 297 contact = mContactDetails.get(index); 298 } 299 300 contactHolder.setContactDetails(contact); 301 } 302 } 303 304 @Override 305 // This will return how many items the RecyclerView should show, which can be a subset of 306 // contacts when in search mode. This function also includes the Select All checkbox (which is 307 // not a contact, obviously). To get the total number of contacts use getAllContacts().size() 308 // instead. getItemCount()309 public int getItemCount() { 310 if (mSearchResults != null) return mSearchResults.size(); 311 if (mContactDetails == null || mContactDetails.size() == 0) return 0; 312 // Add one entry to account for the Select All checkbox, when not searching. 313 return mContactDetails.size() + (mSearchMode ? 0 : 1); 314 } 315 316 // TopView.ChipToggledCallback: 317 318 @Override onChipToggled(@ilterType int chip)319 public void onChipToggled(@FilterType int chip) { 320 switch (chip) { 321 case FilterType.NAMES: 322 sIncludeNames = !sIncludeNames; 323 break; 324 case FilterType.ADDRESSES: 325 sIncludeAddresses = !sIncludeAddresses; 326 break; 327 case FilterType.EMAILS: 328 sIncludeEmails = !sIncludeEmails; 329 break; 330 case FilterType.TELEPHONES: 331 sIncludeTelephones = !sIncludeTelephones; 332 break; 333 case FilterType.ICONS: 334 sIncludeIcons = !sIncludeIcons; 335 break; 336 default: 337 assert false; 338 } 339 340 notifyDataSetChanged(); 341 } 342 343 /** 344 * Returns true unless the adapter is filtering out addresses. 345 */ includesAddresses()346 public static boolean includesAddresses() { 347 return sIncludeAddresses; 348 } 349 350 /** 351 * Returns true unless the adapter is filtering out names. 352 */ includesNames()353 public static boolean includesNames() { 354 return sIncludeNames; 355 } 356 357 /** 358 * Returns true unless the adapter is filtering out emails. 359 */ includesEmails()360 public static boolean includesEmails() { 361 return sIncludeEmails; 362 } 363 364 /** 365 * Returns true unless the adapter is filtering out telephone numbers. 366 */ includesTelephones()367 public static boolean includesTelephones() { 368 return sIncludeTelephones; 369 } 370 371 /** 372 * Returns true unless the adapter is filtering out icons. 373 */ includesIcons()374 public static boolean includesIcons() { 375 return sIncludeIcons; 376 } 377 378 /** 379 * Sets a list of contacts to use as data for the dialog, and the owner email. For testing use 380 * only. 381 */ 382 @VisibleForTesting setTestContactsAndOwner( ArrayList<ContactDetails> contacts, String ownerEmail)383 public static void setTestContactsAndOwner( 384 ArrayList<ContactDetails> contacts, String ownerEmail) { 385 sTestContacts = contacts; 386 sTestOwnerEmail = ownerEmail; 387 } 388 389 /** 390 * Attempts to figure out if the owner of the device is listed in the available contact details. 391 * If so move it to the top of the list. If not found, returns false. 392 * @return Returns true if processing is complete, false if waiting on asynchronous fetching of 393 * missing data for the owner info. 394 */ processOwnerInfo(ArrayList<ContactDetails> contacts, String ownerEmail)395 private static boolean processOwnerInfo(ArrayList<ContactDetails> contacts, String ownerEmail) { 396 if (ownerEmail == null) { 397 return true; 398 } 399 400 ArrayList<Integer> matches = new ArrayList<Integer>(); 401 for (int i = 0; i < contacts.size(); ++i) { 402 List<String> emails = contacts.get(i).getEmails(); 403 for (int y = 0; y < emails.size(); ++y) { 404 if (TextUtils.equals(emails.get(y), ownerEmail)) { 405 matches.add(i); 406 break; 407 } 408 } 409 } 410 411 if (matches.size() == 0) { 412 // No match was found, return false so that a record can be synthesized. 413 return false; 414 } 415 416 // Move the contacts that match owner email to the top of the list. 417 for (int i = 0; i < matches.size(); ++i) { 418 int match = matches.get(i); 419 ContactDetails contact = contacts.get(match); 420 contact.setIsSelf(true); 421 contacts.remove(match); 422 contacts.add(i, contact); 423 } 424 return true; 425 } 426 } 427