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