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.text.TextUtils; 33 import android.util.Log; 34 import android.view.View; 35 import android.view.ViewGroup; 36 37 import com.android.contacts.common.R; 38 39 import java.util.ArrayList; 40 import java.util.List; 41 42 /** 43 * A cursor adapter for the {@link Phone#CONTENT_ITEM_TYPE} and 44 * {@link SipAddress#CONTENT_ITEM_TYPE}. 45 * 46 * By default this adapter just handles phone numbers. When {@link #setUseCallableUri(boolean)} is 47 * called with "true", this adapter starts handling SIP addresses too, by using {@link Callable} 48 * API instead of {@link Phone}. 49 */ 50 public class PhoneNumberListAdapter extends ContactEntryListAdapter { 51 private static final String TAG = PhoneNumberListAdapter.class.getSimpleName(); 52 53 protected static class PhoneQuery { 54 private static final String[] PROJECTION_PRIMARY = new String[] { 55 Phone._ID, // 0 56 Phone.TYPE, // 1 57 Phone.LABEL, // 2 58 Phone.NUMBER, // 3 59 Phone.CONTACT_ID, // 4 60 Phone.LOOKUP_KEY, // 5 61 Phone.PHOTO_ID, // 6 62 Phone.DISPLAY_NAME_PRIMARY, // 7 63 }; 64 65 private static final String[] PROJECTION_ALTERNATIVE = 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_ALTERNATIVE, // 7 74 }; 75 76 public static final int PHONE_ID = 0; 77 public static final int PHONE_TYPE = 1; 78 public static final int PHONE_LABEL = 2; 79 public static final int PHONE_NUMBER = 3; 80 public static final int PHONE_CONTACT_ID = 4; 81 public static final int PHONE_LOOKUP_KEY = 5; 82 public static final int PHONE_PHOTO_ID = 6; 83 public static final int PHONE_DISPLAY_NAME = 7; 84 } 85 86 private final CharSequence mUnknownNameText; 87 88 private ContactListItemView.PhotoPosition mPhotoPosition; 89 90 private boolean mUseCallableUri; 91 92 public PhoneNumberListAdapter(Context context) { 93 super(context); 94 setDefaultFilterHeaderText(R.string.list_filter_phones); 95 mUnknownNameText = context.getText(android.R.string.unknownName); 96 } 97 98 protected CharSequence getUnknownNameText() { 99 return mUnknownNameText; 100 } 101 102 @Override 103 public void configureLoader(CursorLoader loader, long directoryId) { 104 if (directoryId != Directory.DEFAULT) { 105 Log.w(TAG, "PhoneNumberListAdapter is not ready for non-default directory ID (" 106 + "directoryId: " + directoryId + ")"); 107 } 108 109 final Builder builder; 110 if (isSearchMode()) { 111 final Uri baseUri = 112 mUseCallableUri ? Callable.CONTENT_FILTER_URI : Phone.CONTENT_FILTER_URI; 113 builder = baseUri.buildUpon(); 114 final String query = getQueryString(); 115 if (TextUtils.isEmpty(query)) { 116 builder.appendPath(""); 117 } else { 118 builder.appendPath(query); // Builder will encode the query 119 } 120 builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, 121 String.valueOf(directoryId)); 122 } else { 123 final Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI; 124 builder = baseUri.buildUpon().appendQueryParameter( 125 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)); 126 if (isSectionHeaderDisplayEnabled()) { 127 builder.appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true"); 128 } 129 applyFilter(loader, builder, directoryId, getFilter()); 130 } 131 132 // Remove duplicates when it is possible. 133 builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true"); 134 loader.setUri(builder.build()); 135 136 // TODO a projection that includes the search snippet 137 if (getContactNameDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) { 138 loader.setProjection(PhoneQuery.PROJECTION_PRIMARY); 139 } else { 140 loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE); 141 } 142 143 if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) { 144 loader.setSortOrder(Phone.SORT_KEY_PRIMARY); 145 } else { 146 loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE); 147 } 148 } 149 150 /** 151 * Configure {@code loader} and {@code uriBuilder} according to {@code directoryId} and {@code 152 * filter}. 153 */ 154 private void applyFilter(CursorLoader loader, Uri.Builder uriBuilder, long directoryId, 155 ContactListFilter filter) { 156 if (filter == null || directoryId != Directory.DEFAULT) { 157 return; 158 } 159 160 final StringBuilder selection = new StringBuilder(); 161 final List<String> selectionArgs = new ArrayList<String>(); 162 163 switch (filter.filterType) { 164 case ContactListFilter.FILTER_TYPE_CUSTOM: { 165 selection.append(Contacts.IN_VISIBLE_GROUP + "=1"); 166 selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1"); 167 break; 168 } 169 case ContactListFilter.FILTER_TYPE_ACCOUNT: { 170 filter.addAccountQueryParameterToUrl(uriBuilder); 171 break; 172 } 173 case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: 174 case ContactListFilter.FILTER_TYPE_DEFAULT: 175 break; // No selection needed. 176 case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: 177 break; // This adapter is always "phone only", so no selection needed either. 178 default: 179 Log.w(TAG, "Unsupported filter type came " + 180 "(type: " + filter.filterType + ", toString: " + filter + ")" + 181 " showing all contacts."); 182 // No selection. 183 break; 184 } 185 loader.setSelection(selection.toString()); 186 loader.setSelectionArgs(selectionArgs.toArray(new String[0])); 187 } 188 189 @Override 190 public String getContactDisplayName(int position) { 191 return ((Cursor) getItem(position)).getString(PhoneQuery.PHONE_DISPLAY_NAME); 192 } 193 194 /** 195 * Builds a {@link Data#CONTENT_URI} for the given cursor position. 196 * 197 * @return Uri for the data. may be null if the cursor is not ready. 198 */ 199 public Uri getDataUri(int position) { 200 Cursor cursor = ((Cursor)getItem(position)); 201 if (cursor != null) { 202 long id = cursor.getLong(PhoneQuery.PHONE_ID); 203 return ContentUris.withAppendedId(Data.CONTENT_URI, id); 204 } else { 205 Log.w(TAG, "Cursor was null in getDataUri() call. Returning null instead."); 206 return null; 207 } 208 } 209 210 @Override 211 protected View newView(Context context, int partition, Cursor cursor, int position, 212 ViewGroup parent) { 213 final ContactListItemView view = new ContactListItemView(context, null); 214 view.setUnknownNameText(mUnknownNameText); 215 view.setQuickContactEnabled(isQuickContactEnabled()); 216 view.setPhotoPosition(mPhotoPosition); 217 return view; 218 } 219 220 @Override 221 protected void bindView(View itemView, int partition, Cursor cursor, int position) { 222 ContactListItemView view = (ContactListItemView)itemView; 223 224 view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null); 225 226 // Look at elements before and after this position, checking if contact IDs are same. 227 // If they have one same contact ID, it means they can be grouped. 228 // 229 // In one group, only the first entry will show its photo and its name, and the other 230 // entries in the group show just their data (e.g. phone number, email address). 231 cursor.moveToPosition(position); 232 boolean isFirstEntry = true; 233 boolean showBottomDivider = true; 234 final long currentContactId = cursor.getLong(PhoneQuery.PHONE_CONTACT_ID); 235 if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) { 236 final long previousContactId = cursor.getLong(PhoneQuery.PHONE_CONTACT_ID); 237 if (currentContactId == previousContactId) { 238 isFirstEntry = false; 239 } 240 } 241 cursor.moveToPosition(position); 242 if (cursor.moveToNext() && !cursor.isAfterLast()) { 243 final long nextContactId = cursor.getLong(PhoneQuery.PHONE_CONTACT_ID); 244 if (currentContactId == nextContactId) { 245 // The following entry should be in the same group, which means we don't want a 246 // divider between them. 247 // TODO: we want a different divider than the divider between groups. Just hiding 248 // this divider won't be enough. 249 showBottomDivider = false; 250 } 251 } 252 cursor.moveToPosition(position); 253 254 bindSectionHeaderAndDivider(view, position); 255 if (isFirstEntry) { 256 bindName(view, cursor); 257 if (isQuickContactEnabled()) { 258 // No need for photo uri here, because we can not have directory results. If we 259 // ever do, we need to add photo uri to the query 260 bindQuickContact(view, partition, cursor, PhoneQuery.PHONE_PHOTO_ID, -1, 261 PhoneQuery.PHONE_CONTACT_ID, PhoneQuery.PHONE_LOOKUP_KEY); 262 } else { 263 bindPhoto(view, cursor); 264 } 265 } else { 266 unbindName(view); 267 268 view.removePhotoView(true, false); 269 } 270 bindPhoneNumber(view, cursor); 271 view.setDividerVisible(showBottomDivider); 272 } 273 274 protected void bindPhoneNumber(ContactListItemView view, Cursor cursor) { 275 CharSequence label = null; 276 if (!cursor.isNull(PhoneQuery.PHONE_TYPE)) { 277 final int type = cursor.getInt(PhoneQuery.PHONE_TYPE); 278 final String customLabel = cursor.getString(PhoneQuery.PHONE_LABEL); 279 280 // TODO cache 281 label = Phone.getTypeLabel(getContext().getResources(), type, customLabel); 282 } 283 view.setLabel(label); 284 view.showData(cursor, PhoneQuery.PHONE_NUMBER); 285 } 286 287 protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) { 288 if (isSectionHeaderDisplayEnabled()) { 289 Placement placement = getItemPlacementInSection(position); 290 view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null); 291 view.setDividerVisible(!placement.lastInSection); 292 } else { 293 view.setSectionHeader(null); 294 view.setDividerVisible(true); 295 } 296 } 297 298 protected void bindName(final ContactListItemView view, Cursor cursor) { 299 view.showDisplayName(cursor, PhoneQuery.PHONE_DISPLAY_NAME, getContactNameDisplayOrder()); 300 // Note: we don't show phonetic names any more (see issue 5265330) 301 } 302 303 protected void unbindName(final ContactListItemView view) { 304 view.hideDisplayName(); 305 } 306 307 protected void bindPhoto(final ContactListItemView view, Cursor cursor) { 308 long photoId = 0; 309 if (!cursor.isNull(PhoneQuery.PHONE_PHOTO_ID)) { 310 photoId = cursor.getLong(PhoneQuery.PHONE_PHOTO_ID); 311 } 312 313 getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false); 314 } 315 316 public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) { 317 mPhotoPosition = photoPosition; 318 } 319 320 public ContactListItemView.PhotoPosition getPhotoPosition() { 321 return mPhotoPosition; 322 } 323 324 public void setUseCallableUri(boolean useCallableUri) { 325 mUseCallableUri = useCallableUri; 326 } 327 328 public boolean usesCallableUri() { 329 return mUseCallableUri; 330 } 331 } 332