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