1/* 2 * Copyright (C) 2010 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 */ 16package com.android.contacts.common.list; 17 18import android.content.ContentUris; 19import android.content.Context; 20import android.content.CursorLoader; 21import android.database.Cursor; 22import android.net.Uri; 23import android.net.Uri.Builder; 24import android.provider.ContactsContract; 25import android.provider.ContactsContract.CommonDataKinds.Callable; 26import android.provider.ContactsContract.CommonDataKinds.Phone; 27import android.provider.ContactsContract.CommonDataKinds.SipAddress; 28import android.provider.ContactsContract.Contacts; 29import android.provider.ContactsContract.Data; 30import android.provider.ContactsContract.Directory; 31import android.telephony.PhoneNumberUtils; 32import android.text.TextUtils; 33import android.util.Log; 34import android.view.View; 35import android.view.ViewGroup; 36 37import com.android.contacts.common.GeoUtil; 38import com.android.contacts.common.R; 39import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; 40import com.android.contacts.common.extensions.ExtendedPhoneDirectoriesManager; 41import com.android.contacts.common.extensions.ExtensionsFactory; 42import com.android.contacts.common.preference.ContactsPreferences; 43import com.android.contacts.common.util.Constants; 44 45import java.util.ArrayList; 46import java.util.List; 47 48/** 49 * A cursor adapter for the {@link Phone#CONTENT_ITEM_TYPE} and 50 * {@link SipAddress#CONTENT_ITEM_TYPE}. 51 * 52 * By default this adapter just handles phone numbers. When {@link #setUseCallableUri(boolean)} is 53 * called with "true", this adapter starts handling SIP addresses too, by using {@link Callable} 54 * API instead of {@link Phone}. 55 */ 56public class PhoneNumberListAdapter extends ContactEntryListAdapter { 57 58 private static final String TAG = PhoneNumberListAdapter.class.getSimpleName(); 59 60 // A list of extended directories to add to the directories from the database 61 private final List<DirectoryPartition> mExtendedDirectories; 62 63 // Extended directories will have ID's that are higher than any of the id's from the database. 64 // Thi sis so that we can identify them and set them up properly. If no extended directories 65 // exist, this will be Long.MAX_VALUE 66 private long mFirstExtendedDirectoryId = Long.MAX_VALUE; 67 68 public static class PhoneQuery { 69 public static final String[] PROJECTION_PRIMARY = new String[] { 70 Phone._ID, // 0 71 Phone.TYPE, // 1 72 Phone.LABEL, // 2 73 Phone.NUMBER, // 3 74 Phone.CONTACT_ID, // 4 75 Phone.LOOKUP_KEY, // 5 76 Phone.PHOTO_ID, // 6 77 Phone.DISPLAY_NAME_PRIMARY, // 7 78 Phone.PHOTO_THUMBNAIL_URI, // 8 79 }; 80 81 public static final String[] PROJECTION_ALTERNATIVE = new String[] { 82 Phone._ID, // 0 83 Phone.TYPE, // 1 84 Phone.LABEL, // 2 85 Phone.NUMBER, // 3 86 Phone.CONTACT_ID, // 4 87 Phone.LOOKUP_KEY, // 5 88 Phone.PHOTO_ID, // 6 89 Phone.DISPLAY_NAME_ALTERNATIVE, // 7 90 Phone.PHOTO_THUMBNAIL_URI, // 8 91 }; 92 93 public static final int PHONE_ID = 0; 94 public static final int PHONE_TYPE = 1; 95 public static final int PHONE_LABEL = 2; 96 public static final int PHONE_NUMBER = 3; 97 public static final int CONTACT_ID = 4; 98 public static final int LOOKUP_KEY = 5; 99 public static final int PHOTO_ID = 6; 100 public static final int DISPLAY_NAME = 7; 101 public static final int PHOTO_URI = 8; 102 } 103 104 private static final String IGNORE_NUMBER_TOO_LONG_CLAUSE = 105 "length(" + Phone.NUMBER + ") < 1000"; 106 107 private final CharSequence mUnknownNameText; 108 private final String mCountryIso; 109 110 private ContactListItemView.PhotoPosition mPhotoPosition; 111 112 private boolean mUseCallableUri; 113 114 public PhoneNumberListAdapter(Context context) { 115 super(context); 116 setDefaultFilterHeaderText(R.string.list_filter_phones); 117 mUnknownNameText = context.getText(android.R.string.unknownName); 118 mCountryIso = GeoUtil.getCurrentCountryIso(context); 119 120 final ExtendedPhoneDirectoriesManager manager 121 = ExtensionsFactory.getExtendedPhoneDirectoriesManager(); 122 if (manager != null) { 123 mExtendedDirectories = manager.getExtendedDirectories(mContext); 124 } else { 125 // Empty list to avoid sticky NPE's 126 mExtendedDirectories = new ArrayList<DirectoryPartition>(); 127 } 128 } 129 130 protected CharSequence getUnknownNameText() { 131 return mUnknownNameText; 132 } 133 134 @Override 135 public void configureLoader(CursorLoader loader, long directoryId) { 136 String query = getQueryString(); 137 if (query == null) { 138 query = ""; 139 } 140 if (isExtendedDirectory(directoryId)) { 141 final DirectoryPartition directory = getExtendedDirectoryFromId(directoryId); 142 final String contentUri = directory.getContentUri(); 143 if (contentUri == null) { 144 throw new IllegalStateException("Extended directory must have a content URL: " 145 + directory); 146 } 147 final Builder builder = Uri.parse(contentUri).buildUpon(); 148 builder.appendPath(query); 149 builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, 150 String.valueOf(getDirectoryResultLimit(directory))); 151 loader.setUri(builder.build()); 152 loader.setProjection(PhoneQuery.PROJECTION_PRIMARY); 153 } else { 154 final boolean isRemoteDirectoryQuery = isRemoteDirectory(directoryId); 155 final Builder builder; 156 if (isSearchMode()) { 157 final Uri baseUri; 158 if (isRemoteDirectoryQuery) { 159 baseUri = Phone.CONTENT_FILTER_URI; 160 } else if (mUseCallableUri) { 161 baseUri = Callable.CONTENT_FILTER_URI; 162 } else { 163 baseUri = Phone.CONTENT_FILTER_URI; 164 } 165 builder = baseUri.buildUpon(); 166 builder.appendPath(query); // Builder will encode the query 167 builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, 168 String.valueOf(directoryId)); 169 if (isRemoteDirectoryQuery) { 170 builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, 171 String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId)))); 172 } 173 } else { 174 final Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI; 175 builder = baseUri.buildUpon().appendQueryParameter( 176 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)); 177 if (isSectionHeaderDisplayEnabled()) { 178 builder.appendQueryParameter(Phone.EXTRA_ADDRESS_BOOK_INDEX, "true"); 179 } 180 applyFilter(loader, builder, directoryId, getFilter()); 181 } 182 183 // Ignore invalid phone numbers that are too long. These can potentially cause freezes 184 // in the UI and there is no reason to display them. 185 final String prevSelection = loader.getSelection(); 186 final String newSelection; 187 if (!TextUtils.isEmpty(prevSelection)) { 188 newSelection = prevSelection + " AND " + IGNORE_NUMBER_TOO_LONG_CLAUSE; 189 } else { 190 newSelection = IGNORE_NUMBER_TOO_LONG_CLAUSE; 191 } 192 loader.setSelection(newSelection); 193 194 // Remove duplicates when it is possible. 195 builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true"); 196 loader.setUri(builder.build()); 197 198 // TODO a projection that includes the search snippet 199 if (getContactNameDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) { 200 loader.setProjection(PhoneQuery.PROJECTION_PRIMARY); 201 } else { 202 loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE); 203 } 204 205 if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) { 206 loader.setSortOrder(Phone.SORT_KEY_PRIMARY); 207 } else { 208 loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE); 209 } 210 } 211 } 212 213 protected boolean isExtendedDirectory(long directoryId) { 214 return directoryId >= mFirstExtendedDirectoryId; 215 } 216 217 private DirectoryPartition getExtendedDirectoryFromId(long directoryId) { 218 final int directoryIndex = (int) (directoryId - mFirstExtendedDirectoryId); 219 return mExtendedDirectories.get(directoryIndex); 220 } 221 222 /** 223 * Configure {@code loader} and {@code uriBuilder} according to {@code directoryId} and {@code 224 * filter}. 225 */ 226 private void applyFilter(CursorLoader loader, Uri.Builder uriBuilder, long directoryId, 227 ContactListFilter filter) { 228 if (filter == null || directoryId != Directory.DEFAULT) { 229 return; 230 } 231 232 final StringBuilder selection = new StringBuilder(); 233 final List<String> selectionArgs = new ArrayList<String>(); 234 235 switch (filter.filterType) { 236 case ContactListFilter.FILTER_TYPE_CUSTOM: { 237 selection.append(Contacts.IN_VISIBLE_GROUP + "=1"); 238 selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1"); 239 break; 240 } 241 case ContactListFilter.FILTER_TYPE_ACCOUNT: { 242 filter.addAccountQueryParameterToUrl(uriBuilder); 243 break; 244 } 245 case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: 246 case ContactListFilter.FILTER_TYPE_DEFAULT: 247 break; // No selection needed. 248 case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: 249 break; // This adapter is always "phone only", so no selection needed either. 250 default: 251 Log.w(TAG, "Unsupported filter type came " + 252 "(type: " + filter.filterType + ", toString: " + filter + ")" + 253 " showing all contacts."); 254 // No selection. 255 break; 256 } 257 loader.setSelection(selection.toString()); 258 loader.setSelectionArgs(selectionArgs.toArray(new String[0])); 259 } 260 261 @Override 262 public String getContactDisplayName(int position) { 263 return ((Cursor) getItem(position)).getString(PhoneQuery.DISPLAY_NAME); 264 } 265 266 public String getPhoneNumber(int position) { 267 final Cursor item = (Cursor)getItem(position); 268 return item != null ? item.getString(PhoneQuery.PHONE_NUMBER) : null; 269 } 270 271 /** 272 * Builds a {@link Data#CONTENT_URI} for the given cursor position. 273 * 274 * @return Uri for the data. may be null if the cursor is not ready. 275 */ 276 public Uri getDataUri(int position) { 277 final int partitionIndex = getPartitionForPosition(position); 278 final Cursor item = (Cursor)getItem(position); 279 return item != null ? getDataUri(partitionIndex, item) : null; 280 } 281 282 public Uri getDataUri(int partitionIndex, Cursor cursor) { 283 final long directoryId = 284 ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId(); 285 if (!isRemoteDirectory(directoryId)) { 286 final long phoneId = cursor.getLong(PhoneQuery.PHONE_ID); 287 return ContentUris.withAppendedId(Data.CONTENT_URI, phoneId); 288 } 289 return null; 290 } 291 292 @Override 293 protected ContactListItemView newView( 294 Context context, int partition, Cursor cursor, int position, ViewGroup parent) { 295 ContactListItemView view = super.newView(context, partition, cursor, position, parent); 296 view.setUnknownNameText(mUnknownNameText); 297 view.setQuickContactEnabled(isQuickContactEnabled()); 298 view.setPhotoPosition(mPhotoPosition); 299 return view; 300 } 301 302 protected void setHighlight(ContactListItemView view, Cursor cursor) { 303 view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null); 304 } 305 306 // Override default, which would return number of phone numbers, so we 307 // instead return number of contacts. 308 @Override 309 protected int getResultCount(Cursor cursor) { 310 if (cursor == null) { 311 return 0; 312 } 313 cursor.moveToPosition(-1); 314 long curContactId = -1; 315 int numContacts = 0; 316 while(cursor.moveToNext()) { 317 final long contactId = cursor.getLong(PhoneQuery.CONTACT_ID); 318 if (contactId != curContactId) { 319 curContactId = contactId; 320 ++numContacts; 321 } 322 } 323 return numContacts; 324 } 325 326 @Override 327 protected void bindView(View itemView, int partition, Cursor cursor, int position) { 328 super.bindView(itemView, partition, cursor, position); 329 ContactListItemView view = (ContactListItemView)itemView; 330 331 setHighlight(view, cursor); 332 333 // Look at elements before and after this position, checking if contact IDs are same. 334 // If they have one same contact ID, it means they can be grouped. 335 // 336 // In one group, only the first entry will show its photo and its name, and the other 337 // entries in the group show just their data (e.g. phone number, email address). 338 cursor.moveToPosition(position); 339 boolean isFirstEntry = true; 340 boolean showBottomDivider = true; 341 final long currentContactId = cursor.getLong(PhoneQuery.CONTACT_ID); 342 if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) { 343 final long previousContactId = cursor.getLong(PhoneQuery.CONTACT_ID); 344 if (currentContactId == previousContactId) { 345 isFirstEntry = false; 346 } 347 } 348 cursor.moveToPosition(position); 349 if (cursor.moveToNext() && !cursor.isAfterLast()) { 350 final long nextContactId = cursor.getLong(PhoneQuery.CONTACT_ID); 351 if (currentContactId == nextContactId) { 352 // The following entry should be in the same group, which means we don't want a 353 // divider between them. 354 // TODO: we want a different divider than the divider between groups. Just hiding 355 // this divider won't be enough. 356 showBottomDivider = false; 357 } 358 } 359 cursor.moveToPosition(position); 360 361 bindViewId(view, cursor, PhoneQuery.PHONE_ID); 362 363 bindSectionHeaderAndDivider(view, position); 364 if (isFirstEntry) { 365 bindName(view, cursor); 366 if (isQuickContactEnabled()) { 367 bindQuickContact(view, partition, cursor, PhoneQuery.PHOTO_ID, 368 PhoneQuery.PHOTO_URI, PhoneQuery.CONTACT_ID, 369 PhoneQuery.LOOKUP_KEY, PhoneQuery.DISPLAY_NAME); 370 } else { 371 if (getDisplayPhotos()) { 372 bindPhoto(view, partition, cursor); 373 } 374 } 375 } else { 376 unbindName(view); 377 378 view.removePhotoView(true, false); 379 } 380 381 final DirectoryPartition directory = (DirectoryPartition) getPartition(partition); 382 bindPhoneNumber(view, cursor, directory.isDisplayNumber()); 383 } 384 385 protected void bindPhoneNumber(ContactListItemView view, Cursor cursor, boolean displayNumber) { 386 CharSequence label = null; 387 if (displayNumber && !cursor.isNull(PhoneQuery.PHONE_TYPE)) { 388 final int type = cursor.getInt(PhoneQuery.PHONE_TYPE); 389 final String customLabel = cursor.getString(PhoneQuery.PHONE_LABEL); 390 391 // TODO cache 392 label = Phone.getTypeLabel(getContext().getResources(), type, customLabel); 393 } 394 view.setLabel(label); 395 final String text; 396 if (displayNumber) { 397 text = cursor.getString(PhoneQuery.PHONE_NUMBER); 398 } else { 399 // Display phone label. If that's null, display geocoded location for the number 400 final String phoneLabel = cursor.getString(PhoneQuery.PHONE_LABEL); 401 if (phoneLabel != null) { 402 text = phoneLabel; 403 } else { 404 final String phoneNumber = cursor.getString(PhoneQuery.PHONE_NUMBER); 405 text = GeoUtil.getGeocodedLocationFor(mContext, phoneNumber); 406 } 407 } 408 view.setPhoneNumber(text, mCountryIso); 409 } 410 411 protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) { 412 if (isSectionHeaderDisplayEnabled()) { 413 Placement placement = getItemPlacementInSection(position); 414 view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null); 415 } else { 416 view.setSectionHeader(null); 417 } 418 } 419 420 protected void bindName(final ContactListItemView view, Cursor cursor) { 421 view.showDisplayName(cursor, PhoneQuery.DISPLAY_NAME, getContactNameDisplayOrder()); 422 // Note: we don't show phonetic names any more (see issue 5265330) 423 } 424 425 protected void unbindName(final ContactListItemView view) { 426 view.hideDisplayName(); 427 } 428 429 protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) { 430 if (!isPhotoSupported(partitionIndex)) { 431 view.removePhotoView(); 432 return; 433 } 434 435 long photoId = 0; 436 if (!cursor.isNull(PhoneQuery.PHOTO_ID)) { 437 photoId = cursor.getLong(PhoneQuery.PHOTO_ID); 438 } 439 440 if (photoId != 0) { 441 getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false, 442 getCircularPhotos(), null); 443 } else { 444 final String photoUriString = cursor.getString(PhoneQuery.PHOTO_URI); 445 final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString); 446 447 DefaultImageRequest request = null; 448 if (photoUri == null) { 449 final String displayName = cursor.getString(PhoneQuery.DISPLAY_NAME); 450 final String lookupKey = cursor.getString(PhoneQuery.LOOKUP_KEY); 451 request = new DefaultImageRequest(displayName, lookupKey, getCircularPhotos()); 452 } 453 getPhotoLoader().loadDirectoryPhoto(view.getPhotoView(), photoUri, false, 454 getCircularPhotos(), request); 455 } 456 } 457 458 public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) { 459 mPhotoPosition = photoPosition; 460 } 461 462 public ContactListItemView.PhotoPosition getPhotoPosition() { 463 return mPhotoPosition; 464 } 465 466 public void setUseCallableUri(boolean useCallableUri) { 467 mUseCallableUri = useCallableUri; 468 } 469 470 public boolean usesCallableUri() { 471 return mUseCallableUri; 472 } 473 474 /** 475 * Override base implementation to inject extended directories between local & remote 476 * directories. This is done in the following steps: 477 * 1. Call base implementation to add directories from the cursor. 478 * 2. Iterate all base directories and establish the following information: 479 * a. The highest directory id so that we can assign unused id's to the extended directories. 480 * b. The index of the last non-remote directory. This is where we will insert extended 481 * directories. 482 * 3. Iterate the extended directories and for each one, assign an ID and insert it in the 483 * proper location. 484 */ 485 @Override 486 public void changeDirectories(Cursor cursor) { 487 super.changeDirectories(cursor); 488 if (getDirectorySearchMode() == DirectoryListLoader.SEARCH_MODE_NONE) { 489 return; 490 } 491 final int numExtendedDirectories = mExtendedDirectories.size(); 492 if (getPartitionCount() == cursor.getCount() + numExtendedDirectories) { 493 // already added all directories; 494 return; 495 } 496 // 497 mFirstExtendedDirectoryId = Long.MAX_VALUE; 498 if (numExtendedDirectories > 0) { 499 // The Directory.LOCAL_INVISIBLE is not in the cursor but we can't reuse it's 500 // "special" ID. 501 long maxId = Directory.LOCAL_INVISIBLE; 502 int insertIndex = 0; 503 for (int i = 0, n = getPartitionCount(); i < n; i++) { 504 final DirectoryPartition partition = (DirectoryPartition) getPartition(i); 505 final long id = partition.getDirectoryId(); 506 if (id > maxId) { 507 maxId = id; 508 } 509 if (!isRemoteDirectory(id)) { 510 // assuming remote directories come after local, we will end up with the index 511 // where we should insert extended directories. This also works if there are no 512 // remote directories at all. 513 insertIndex = i + 1; 514 } 515 } 516 // Extended directories ID's cannot collide with base directories 517 mFirstExtendedDirectoryId = maxId + 1; 518 for (int i = 0; i < numExtendedDirectories; i++) { 519 final long id = mFirstExtendedDirectoryId + i; 520 final DirectoryPartition directory = mExtendedDirectories.get(i); 521 if (getPartitionByDirectoryId(id) == -1) { 522 addPartition(insertIndex, directory); 523 directory.setDirectoryId(id); 524 } 525 } 526 } 527 } 528 529 protected Uri getContactUri(int partitionIndex, Cursor cursor, 530 int contactIdColumn, int lookUpKeyColumn) { 531 final DirectoryPartition directory = (DirectoryPartition) getPartition(partitionIndex); 532 final long directoryId = directory.getDirectoryId(); 533 if (!isExtendedDirectory(directoryId)) { 534 return super.getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn); 535 } 536 return Contacts.CONTENT_LOOKUP_URI.buildUpon() 537 .appendPath(Constants.LOOKUP_URI_ENCODED) 538 .appendQueryParameter(Directory.DISPLAY_NAME, directory.getLabel()) 539 .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, 540 String.valueOf(directoryId)) 541 .encodedFragment(cursor.getString(lookUpKeyColumn)) 542 .build(); 543 } 544} 545