BaseEmailAddressAdapter.java revision 1e35ed124efed58b9ef6e70059865cbd145bbce8
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 17package com.android.common.contacts; 18 19import com.android.common.widget.CompositeCursorAdapter; 20 21import android.accounts.Account; 22import android.content.ContentResolver; 23import android.content.Context; 24import android.content.pm.PackageManager; 25import android.content.pm.PackageManager.NameNotFoundException; 26import android.content.res.Resources; 27import android.database.Cursor; 28import android.database.MatrixCursor; 29import android.net.Uri; 30import android.provider.ContactsContract; 31import android.provider.ContactsContract.CommonDataKinds.Email; 32import android.provider.ContactsContract.Contacts; 33import android.text.TextUtils; 34import android.text.util.Rfc822Token; 35import android.util.Log; 36import android.view.View; 37import android.view.ViewGroup; 38import android.widget.Filter; 39import android.widget.Filterable; 40 41import java.util.ArrayList; 42import java.util.List; 43 44/** 45 * A base class for email address autocomplete adapters. It uses 46 * {@link Email#CONTENT_FILTER_URI} to search for data rows by email address 47 * and/or contact name. It also searches registered {@link Directory}'s. 48 */ 49public abstract class BaseEmailAddressAdapter extends CompositeCursorAdapter implements Filterable { 50 51 private static final String TAG = "BaseEmailAddressAdapter"; 52 53 // TODO: revert to references to the Directory class as soon as the 54 // issue with the dependency on SDK 8 is resolved 55 56 // This is Directory.LOCAL_INVISIBLE 57 private static final long DIRECTORY_LOCAL_INVISIBLE = 1; 58 59 // This is ContactsContract.DIRECTORY_PARAM_KEY 60 private static final String DIRECTORY_PARAM_KEY = "directory"; 61 62 // This is ContactsContract.LIMIT_PARAM_KEY 63 private static final String LIMIT_PARAM_KEY = "limit"; 64 65 /** 66 * The preferred number of results to be retrieved. This number may be 67 * exceeded if there are several directories configured, because we will 68 * use the same limit for all directories. 69 */ 70 private static final int DEFAULT_PREFERRED_MAX_RESULT_COUNT = 10; 71 72 /** 73 * Model object for a {@link Directory} row. There is a partition in the 74 * {@link CompositeCursorAdapter} for every directory (except 75 * {@link Directory#LOCAL_INVISIBLE}. 76 */ 77 public final static class DirectoryPartition extends CompositeCursorAdapter.Partition { 78 public long directoryId; 79 public String directoryType; 80 public String displayName; 81 public String accountName; 82 public String accountType; 83 public boolean loading; 84 public CharSequence constraint; 85 public DirectoryPartitionFilter filter; 86 87 public DirectoryPartition() { 88 super(false, false); 89 } 90 } 91 92 private static class EmailQuery { 93 public static final String[] PROJECTION = { 94 Contacts.DISPLAY_NAME, // 0 95 Email.DATA // 1 96 }; 97 98 public static final int NAME = 0; 99 public static final int ADDRESS = 1; 100 } 101 102 private static class DirectoryListQuery { 103 104 // TODO: revert to references to the Directory class as soon as the 105 // issue with the dependency on SDK 8 is resolved 106 public static final Uri URI = 107 Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories"); 108 private static final String DIRECTORY_ID = "_id"; 109 private static final String DIRECTORY_ACCOUNT_NAME = "accountName"; 110 private static final String DIRECTORY_ACCOUNT_TYPE = "accountType"; 111 private static final String DIRECTORY_DISPLAY_NAME = "displayName"; 112 private static final String DIRECTORY_PACKAGE_NAME = "packageName"; 113 private static final String DIRECTORY_TYPE_RESOURCE_ID = "typeResourceId"; 114 115 public static final String[] PROJECTION = { 116 DIRECTORY_ID, // 0 117 DIRECTORY_ACCOUNT_NAME, // 1 118 DIRECTORY_ACCOUNT_TYPE, // 2 119 DIRECTORY_DISPLAY_NAME, // 3 120 DIRECTORY_PACKAGE_NAME, // 4 121 DIRECTORY_TYPE_RESOURCE_ID, // 5 122 }; 123 124 public static final int ID = 0; 125 public static final int ACCOUNT_NAME = 1; 126 public static final int ACCOUNT_TYPE = 2; 127 public static final int DISPLAY_NAME = 3; 128 public static final int PACKAGE_NAME = 4; 129 public static final int TYPE_RESOURCE_ID = 5; 130 } 131 132 /** 133 * A fake column name that indicates a "Searching..." item in the list. 134 */ 135 private static final String SEARCHING_CURSOR_MARKER = "searching"; 136 137 /** 138 * An asynchronous filter used for loading two data sets: email rows from the local 139 * contact provider and the list of {@link Directory}'s. 140 */ 141 private final class DefaultPartitionFilter extends Filter { 142 143 @Override 144 protected FilterResults performFiltering(CharSequence constraint) { 145 Cursor directoryCursor = null; 146 if (!mDirectoriesLoaded) { 147 directoryCursor = mContentResolver.query( 148 DirectoryListQuery.URI, DirectoryListQuery.PROJECTION, null, null, null); 149 mDirectoriesLoaded = true; 150 } 151 152 FilterResults results = new FilterResults(); 153 Cursor cursor = null; 154 if (!TextUtils.isEmpty(constraint)) { 155 Uri uri = Email.CONTENT_FILTER_URI.buildUpon() 156 .appendPath(constraint.toString()) 157 .appendQueryParameter(LIMIT_PARAM_KEY, 158 String.valueOf(mPreferredMaxResultCount)) 159 .build(); 160 cursor = mContentResolver.query(uri, EmailQuery.PROJECTION, null, null, null); 161 results.count = cursor.getCount(); 162 } 163 results.values = new Cursor[] { directoryCursor, cursor }; 164 return results; 165 } 166 167 @Override 168 protected void publishResults(CharSequence constraint, FilterResults results) { 169 if (results.values != null) { 170 Cursor[] cursors = (Cursor[]) results.values; 171 onDirectoryLoadFinished(constraint, cursors[0], cursors[1]); 172 } 173 results.count = getCount(); 174 } 175 176 @Override 177 public CharSequence convertResultToString(Object resultValue) { 178 return makeDisplayString((Cursor) resultValue); 179 } 180 } 181 182 /** 183 * An asynchronous filter that performs search in a particular directory. 184 */ 185 private final class DirectoryPartitionFilter extends Filter { 186 private final int mPartitionIndex; 187 private final long mDirectoryId; 188 private int mLimit; 189 190 public DirectoryPartitionFilter(int partitionIndex, long directoryId) { 191 this.mPartitionIndex = partitionIndex; 192 this.mDirectoryId = directoryId; 193 } 194 195 public synchronized void setLimit(int limit) { 196 this.mLimit = limit; 197 } 198 199 public synchronized int getLimit() { 200 return this.mLimit; 201 } 202 203 @Override 204 protected FilterResults performFiltering(CharSequence constraint) { 205 FilterResults results = new FilterResults(); 206 if (!TextUtils.isEmpty(constraint)) { 207 Uri uri = Email.CONTENT_FILTER_URI.buildUpon() 208 .appendPath(constraint.toString()) 209 .appendQueryParameter(DIRECTORY_PARAM_KEY, String.valueOf(mDirectoryId)) 210 .appendQueryParameter(LIMIT_PARAM_KEY, String.valueOf(getLimit())) 211 .build(); 212 Cursor cursor = mContentResolver.query( 213 uri, EmailQuery.PROJECTION, null, null, null); 214 results.values = cursor; 215 } 216 return results; 217 } 218 219 @Override 220 protected void publishResults(CharSequence constraint, FilterResults results) { 221 Cursor cursor = (Cursor) results.values; 222 onPartitionLoadFinished(constraint, mPartitionIndex, cursor); 223 results.count = getCount(); 224 } 225 } 226 227 protected final ContentResolver mContentResolver; 228 private boolean mDirectoriesLoaded; 229 private Account mAccount; 230 private int mPreferredMaxResultCount; 231 232 public BaseEmailAddressAdapter(Context context) { 233 this(context, DEFAULT_PREFERRED_MAX_RESULT_COUNT); 234 } 235 236 public BaseEmailAddressAdapter(Context context, int preferredMaxResultCount) { 237 super(context); 238 mContentResolver = context.getContentResolver(); 239 mPreferredMaxResultCount = preferredMaxResultCount; 240 } 241 242 /** 243 * Set the account when known. Causes the search to prioritize contacts from 244 * that account. 245 */ 246 public void setAccount(Account account) { 247 mAccount = account; 248 } 249 250 /** 251 * Override to create a view for line item in the autocomplete suggestion list UI. 252 */ 253 protected abstract View inflateItemView(ViewGroup parent); 254 255 /** 256 * Override to populate the autocomplete suggestion line item UI with data. 257 */ 258 protected abstract void bindView(View view, String directoryType, String directoryName, 259 String displayName, String emailAddress); 260 261 /** 262 * Override to create a view for a "Searching directory" line item, which is 263 * displayed temporarily while the corresponding filter is running. 264 */ 265 protected abstract View inflateItemViewLoading(ViewGroup parent); 266 267 /** 268 * Override to populate the "Searching directory" line item UI with data. 269 */ 270 protected abstract void bindViewLoading(View view, String directoryType, String directoryName); 271 272 @Override 273 protected int getItemViewType(int partitionIndex, int position) { 274 DirectoryPartition partition = (DirectoryPartition)getPartition(partitionIndex); 275 return partition.loading ? 1 : 0; 276 } 277 278 @Override 279 protected View newView(Context context, int partitionIndex, Cursor cursor, 280 int position, ViewGroup parent) { 281 DirectoryPartition partition = (DirectoryPartition)getPartition(partitionIndex); 282 if (partition.loading) { 283 return inflateItemViewLoading(parent); 284 } else { 285 return inflateItemView(parent); 286 } 287 } 288 289 @Override 290 protected void bindView(View v, int partition, Cursor cursor, int position) { 291 DirectoryPartition directoryPartition = (DirectoryPartition)getPartition(partition); 292 String directoryType = directoryPartition.directoryType; 293 String directoryName = directoryPartition.displayName; 294 if (directoryPartition.loading) { 295 bindViewLoading(v, directoryType, directoryName); 296 } else { 297 String displayName = cursor.getString(EmailQuery.NAME); 298 String emailAddress = cursor.getString(EmailQuery.ADDRESS); 299 if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, emailAddress)) { 300 displayName = emailAddress; 301 emailAddress = null; 302 } 303 bindView(v, directoryType, directoryName, displayName, emailAddress); 304 } 305 } 306 307 @Override 308 public boolean areAllItemsEnabled() { 309 return false; 310 } 311 312 @Override 313 protected boolean isEnabled(int partitionIndex, int position) { 314 // The "Searching..." item should not be selectable 315 return !((DirectoryPartition)getPartition(partitionIndex)).loading; 316 } 317 318 @Override 319 public Filter getFilter() { 320 return new DefaultPartitionFilter(); 321 } 322 323 /** 324 * Handles the result of the initial call, which brings back the list of 325 * directories as well as the search results for the local directories. 326 */ 327 protected void onDirectoryLoadFinished( 328 CharSequence constraint, Cursor directoryCursor, Cursor defaultPartitionCursor) { 329 if (directoryCursor != null) { 330 PackageManager packageManager = getContext().getPackageManager(); 331 DirectoryPartition preferredDirectory = null; 332 List<DirectoryPartition> directories = new ArrayList<DirectoryPartition>(); 333 while (directoryCursor.moveToNext()) { 334 long id = directoryCursor.getLong(DirectoryListQuery.ID); 335 336 // Skip the local invisible directory, because the default directory 337 // already includes all local results. 338 if (id == DIRECTORY_LOCAL_INVISIBLE) { 339 continue; 340 } 341 342 DirectoryPartition partition = new DirectoryPartition(); 343 partition.directoryId = id; 344 partition.displayName = directoryCursor.getString(DirectoryListQuery.DISPLAY_NAME); 345 partition.accountName = directoryCursor.getString(DirectoryListQuery.ACCOUNT_NAME); 346 partition.accountType = directoryCursor.getString(DirectoryListQuery.ACCOUNT_TYPE); 347 String packageName = directoryCursor.getString(DirectoryListQuery.PACKAGE_NAME); 348 int resourceId = directoryCursor.getInt(DirectoryListQuery.TYPE_RESOURCE_ID); 349 if (packageName != null && resourceId != 0) { 350 try { 351 Resources resources = 352 packageManager.getResourcesForApplication(packageName); 353 partition.directoryType = resources.getString(resourceId); 354 if (partition.directoryType == null) { 355 Log.e(TAG, "Cannot resolve directory name: " 356 + resourceId + "@" + packageName); 357 } 358 } catch (NameNotFoundException e) { 359 Log.e(TAG, "Cannot resolve directory name: " 360 + resourceId + "@" + packageName, e); 361 } 362 } 363 364 // If an account has been provided and we found a directory that 365 // corresponds to that account, place that directory second, directly 366 // underneath the local contacts. 367 if (mAccount != null && mAccount.name.equals(partition.accountName) && 368 mAccount.type.equals(partition.accountType)) { 369 preferredDirectory = partition; 370 } else { 371 directories.add(partition); 372 } 373 } 374 375 if (preferredDirectory != null) { 376 directories.add(1, preferredDirectory); 377 } 378 379 for (DirectoryPartition partition : directories) { 380 addPartition(partition); 381 } 382 } 383 384 int count = getPartitionCount(); 385 int limit = 0; 386 387 // Since we will be changing several partitions at once, hold the data change 388 // notifications 389 setNotificationsEnabled(false); 390 try { 391 // The filter has loaded results for the default partition too. 392 if (defaultPartitionCursor != null && getPartitionCount() > 0) { 393 changeCursor(0, defaultPartitionCursor); 394 } 395 396 int defaultPartitionCount = (defaultPartitionCursor == null ? 0 397 : defaultPartitionCursor.getCount()); 398 399 limit = mPreferredMaxResultCount - defaultPartitionCount; 400 401 // Show non-default directories as "loading" 402 // Note: skipping the default partition (index 0), which has already been loaded 403 for (int i = 1; i < count; i++) { 404 DirectoryPartition partition = (DirectoryPartition) getPartition(i); 405 partition.constraint = constraint; 406 407 if (limit > 0) { 408 if (!partition.loading) { 409 partition.loading = true; 410 changeCursor(i, createLoadingCursor()); 411 } 412 } else { 413 partition.loading = false; 414 changeCursor(i, null); 415 } 416 } 417 } finally { 418 setNotificationsEnabled(true); 419 } 420 421 // Start search in other directories 422 // Note: skipping the default partition (index 0), which has already been loaded 423 for (int i = 1; i < count; i++) { 424 DirectoryPartition partition = (DirectoryPartition) getPartition(i); 425 if (partition.loading) { 426 if (partition.filter == null) { 427 partition.filter = new DirectoryPartitionFilter(i, partition.directoryId); 428 } 429 partition.filter.setLimit(limit); 430 partition.filter.filter(constraint); 431 } else { 432 if (partition.filter != null) { 433 // Cancel any previous loading request 434 partition.filter.filter(null); 435 } 436 } 437 } 438 } 439 440 /** 441 * Creates a dummy cursor to represent the "Searching directory..." item. 442 */ 443 private Cursor createLoadingCursor() { 444 MatrixCursor cursor = new MatrixCursor(new String[]{SEARCHING_CURSOR_MARKER}); 445 cursor.addRow(new Object[]{""}); 446 return cursor; 447 } 448 449 public void onPartitionLoadFinished( 450 CharSequence constraint, int partitionIndex, Cursor cursor) { 451 if (partitionIndex < getPartitionCount()) { 452 DirectoryPartition partition = (DirectoryPartition) getPartition(partitionIndex); 453 // Check if the received result matches the current constraint 454 // If not - the user must have continued typing after the request 455 // was issued 456 if (partition.loading && TextUtils.equals(constraint, partition.constraint)) { 457 partition.loading = false; 458 changeCursor(partitionIndex, cursor); 459 } else { 460 // We got the result for an unexpected query (the user is still typing) 461 // Just ignore this result 462 if (cursor != null) { 463 cursor.close(); 464 } 465 } 466 } else if (cursor != null) { 467 cursor.close(); 468 } 469 } 470 471 private final String makeDisplayString(Cursor cursor) { 472 if (cursor.getColumnName(0).equals(SEARCHING_CURSOR_MARKER)) { 473 return ""; 474 } 475 476 String displayName = cursor.getString(EmailQuery.NAME); 477 String emailAddress = cursor.getString(EmailQuery.ADDRESS); 478 if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, emailAddress)) { 479 return emailAddress; 480 } else { 481 return new Rfc822Token(displayName, emailAddress, null).toString(); 482 } 483 } 484} 485