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