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