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