BaseRecipientAdapterpublic class BaseRecipientAdapter extends android.widget.BaseAdapter implements AccountSpecifier, android.widget.Filterable, PhotoManager.PhotoManagerCallbackAdapter for showing a recipient list. |
Fields Summary |
---|
private static final String | TAG | private static final boolean | DEBUG | private static final int | DEFAULT_PREFERRED_MAX_RESULT_COUNTThe preferred number of results to be retrieved. This number may be
exceeded if there are several directories configured, because we will use
the same limit for all directories. | static final int | ALLOWANCE_FOR_DUPLICATESThe number of extra entries requested to allow for duplicates. Duplicates
are removed from the overall result. | static final String | PRIMARY_ACCOUNT_NAME | static final String | PRIMARY_ACCOUNT_TYPE | private static final int | MESSAGE_SEARCH_PENDING_DELAYThe "Waiting for more contacts" message will be displayed if search is not complete
within this many milliseconds. | private static final int | MESSAGE_SEARCH_PENDINGUsed to prepare "Waiting for more contacts" message. | public static final int | QUERY_TYPE_EMAIL | public static final int | QUERY_TYPE_PHONE | private final Queries.Query | mQueryMode | private final int | mQueryType | private final android.content.Context | mContext | private final android.content.ContentResolver | mContentResolver | private android.accounts.Account | mAccount | protected final int | mPreferredMaxResultCount | private DropdownChipLayouter | mDropdownChipLayouter | private LinkedHashMap | mEntryMap{@link #mEntries} is responsible for showing every result for this Adapter. To
construct it, we use {@link #mEntryMap}, {@link #mNonAggregatedEntries}, and
{@link #mExistingDestinations}.
First, each destination (an email address or a phone number) with a valid contactId is
inserted into {@link #mEntryMap} and grouped by the contactId. Destinations without valid
contactId (possible if they aren't in local storage) are stored in
{@link #mNonAggregatedEntries}.
Duplicates are removed using {@link #mExistingDestinations}.
After having all results from Cursor objects, all destinations in mEntryMap are copied to
{@link #mEntries}. If the number of destinations is not enough (i.e. less than
{@link #mPreferredMaxResultCount}), destinations in mNonAggregatedEntries are also used.
These variables are only used in UI thread, thus should not be touched in
performFiltering() methods. | private List | mNonAggregatedEntries | private Set | mExistingDestinations | private List | mEntriesNote: use {@link #updateEntries(List)} to update this variable. | private List | mTempEntries | private int | mRemainingDirectoryCountThe number of directories this adapter is waiting for results. | protected CharSequence | mCurrentConstraintUsed to ignore asynchronous queries with a different constraint, which may happen when
users type characters quickly. | private PhotoManager | mPhotoManagerPerforms all photo querying as well as caching for repeated lookups. | private final DelayedMessageHandler | mDelayedMessageHandler | private EntriesUpdatedObserver | mEntriesUpdatedObserver |
Constructors Summary |
---|
public BaseRecipientAdapter(android.content.Context context)Constructor for email queries.
this(context, DEFAULT_PREFERRED_MAX_RESULT_COUNT, QUERY_TYPE_EMAIL);
| public BaseRecipientAdapter(android.content.Context context, int preferredMaxResultCount)
this(context, preferredMaxResultCount, QUERY_TYPE_EMAIL);
| public BaseRecipientAdapter(int queryMode, android.content.Context context)
this(context, DEFAULT_PREFERRED_MAX_RESULT_COUNT, queryMode);
| public BaseRecipientAdapter(int queryMode, android.content.Context context, int preferredMaxResultCount)
this(context, preferredMaxResultCount, queryMode);
| public BaseRecipientAdapter(android.content.Context context, int preferredMaxResultCount, int queryMode)
mContext = context;
mContentResolver = context.getContentResolver();
mPreferredMaxResultCount = preferredMaxResultCount;
mPhotoManager = new DefaultPhotoManager(mContentResolver);
mQueryType = queryMode;
if (queryMode == QUERY_TYPE_EMAIL) {
mQueryMode = Queries.EMAIL;
} else if (queryMode == QUERY_TYPE_PHONE) {
mQueryMode = Queries.PHONE;
} else {
mQueryMode = Queries.EMAIL;
Log.e(TAG, "Unsupported query type: " + queryMode);
}
|
Methods Summary |
---|
protected void | cacheCurrentEntries()
mTempEntries = mEntries;
| protected void | clearTempEntries()
mTempEntries = null;
| protected java.util.List | constructEntryList()Returns the actual list to use for this Adapter. Derived classes
should override this method if overriding how the adapter stores and collates
data.
return constructEntryList(mEntryMap, mNonAggregatedEntries);
| private java.util.List | constructEntryList(java.util.LinkedHashMap entryMap, java.util.List nonAggregatedEntries)Constructs an actual list for this Adapter using {@link #mEntryMap}. Also tries to
fetch a cached photo for each contact entry (other than separators), or request another
thread to get one from directories.
final List<RecipientEntry> entries = new ArrayList<RecipientEntry>();
int validEntryCount = 0;
for (Map.Entry<Long, List<RecipientEntry>> mapEntry : entryMap.entrySet()) {
final List<RecipientEntry> entryList = mapEntry.getValue();
final int size = entryList.size();
for (int i = 0; i < size; i++) {
RecipientEntry entry = entryList.get(i);
entries.add(entry);
mPhotoManager.populatePhotoBytesAsync(entry, this);
validEntryCount++;
}
if (validEntryCount > mPreferredMaxResultCount) {
break;
}
}
if (validEntryCount <= mPreferredMaxResultCount) {
for (RecipientEntry entry : nonAggregatedEntries) {
if (validEntryCount > mPreferredMaxResultCount) {
break;
}
entries.add(entry);
mPhotoManager.populatePhotoBytesAsync(entry, this);
validEntryCount++;
}
}
return entries;
| private android.database.Cursor | doQuery(java.lang.CharSequence constraint, int limit, java.lang.Long directoryId)
final Uri.Builder builder = mQueryMode.getContentFilterUri().buildUpon()
.appendPath(constraint.toString())
.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
String.valueOf(limit + ALLOWANCE_FOR_DUPLICATES));
if (directoryId != null) {
builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
String.valueOf(directoryId));
}
if (mAccount != null) {
builder.appendQueryParameter(PRIMARY_ACCOUNT_NAME, mAccount.name);
builder.appendQueryParameter(PRIMARY_ACCOUNT_TYPE, mAccount.type);
}
final long start = System.currentTimeMillis();
final Cursor cursor = mContentResolver.query(
builder.build(), mQueryMode.getProjection(), null, null, null);
final long end = System.currentTimeMillis();
if (DEBUG) {
Log.d(TAG, "Time for autocomplete (query: " + constraint
+ ", directoryId: " + directoryId + ", num_of_results: "
+ (cursor != null ? cursor.getCount() : "null") + "): "
+ (end - start) + " ms");
}
return cursor;
| protected void | fetchPhoto(RecipientEntry entry, PhotoManager.PhotoManagerCallback cb)
mPhotoManager.populatePhotoBytesAsync(entry, cb);
| public boolean | forceShowAddress()If true, forces using the {@link com.android.ex.chips.SingleRecipientArrayAdapter}
instead of {@link com.android.ex.chips.RecipientAlternatesAdapter} when
clicking on a chip. Default implementation returns {@code false}.
return false;
| public android.accounts.Account | getAccount()
return mAccount;
| public android.content.Context | getContext()
return mContext;
| public int | getCount()
final List<RecipientEntry> entries = getEntries();
return entries != null ? entries.size() : 0;
| public DropdownChipLayouter | getDropdownChipLayouter()
return mDropdownChipLayouter;
| protected java.util.List | getEntries()
return mTempEntries != null ? mTempEntries : mEntries;
| public android.widget.Filter | getFilter()Will be called from {@link AutoCompleteTextView} to prepare auto-complete list.
return new DefaultFilter();
| public RecipientEntry | getItem(int position)
return getEntries().get(position);
| public long | getItemId(int position)
return position;
| public int | getItemViewType(int position)
return getEntries().get(position).getEntryType();
| public void | getMatchingRecipients(java.util.ArrayList inAddresses, RecipientAlternatesAdapter.RecipientMatchCallback callback)Used to replace email addresses with chips. Default behavior
queries the ContactsProvider for contact information about the contact.
Derived classes should override this method if they wish to use a
new data source.
RecipientAlternatesAdapter.getMatchingRecipients(
getContext(), this, inAddresses, getAccount(), callback);
| public java.util.Map | getMatchingRecipients(java.util.Set addresses)An extension to {@link RecipientAlternatesAdapter#getMatchingRecipients} that allows
additional sources of contacts to be considered as matching recipients.
return null;
| public PhotoManager | getPhotoManager()
return mPhotoManager;
| public int | getQueryType()
return mQueryType;
| public android.view.View | getView(int position, android.view.View convertView, android.view.ViewGroup parent)
final RecipientEntry entry = getEntries().get(position);
final String constraint = mCurrentConstraint == null ? null :
mCurrentConstraint.toString();
return mDropdownChipLayouter.bindView(convertView, parent, entry, position,
AdapterType.BASE_RECIPIENT, constraint);
| public int | getViewTypeCount()
return RecipientEntry.ENTRY_TYPE_SIZE;
| public boolean | isEnabled(int position)
return getEntries().get(position).isSelectable();
| public void | onPhotoBytesAsyncLoadFailed()
// Default implementation does nothing
| public void | onPhotoBytesAsynchronouslyPopulated()
notifyDataSetChanged();
| public void | onPhotoBytesPopulated()
// Default implementation does nothing
| protected void | putOneEntry(com.android.ex.chips.BaseRecipientAdapter$TemporaryEntry entry, boolean isAggregatedEntry)Called whenever {@link com.android.ex.chips.BaseRecipientAdapter.DirectoryFilter}
wants to add an additional entry to the results. Derived classes should override
this method if they are not using the default data structures provided by
{@link com.android.ex.chips.BaseRecipientAdapter} and are instead using their
own data structures to store and collate data.
putOneEntry(entry, isAggregatedEntry,
mEntryMap, mNonAggregatedEntries, mExistingDestinations);
| private static void | putOneEntry(com.android.ex.chips.BaseRecipientAdapter$TemporaryEntry entry, boolean isAggregatedEntry, java.util.LinkedHashMap entryMap, java.util.List nonAggregatedEntries, java.util.Set existingDestinations)
if (existingDestinations.contains(entry.destination)) {
return;
}
existingDestinations.add(entry.destination);
if (!isAggregatedEntry) {
nonAggregatedEntries.add(RecipientEntry.constructTopLevelEntry(
entry.displayName,
entry.displayNameSource,
entry.destination, entry.destinationType, entry.destinationLabel,
entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString,
true, entry.lookupKey));
} else if (entryMap.containsKey(entry.contactId)) {
// We already have a section for the person.
final List<RecipientEntry> entryList = entryMap.get(entry.contactId);
entryList.add(RecipientEntry.constructSecondLevelEntry(
entry.displayName,
entry.displayNameSource,
entry.destination, entry.destinationType, entry.destinationLabel,
entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString,
true, entry.lookupKey));
} else {
final List<RecipientEntry> entryList = new ArrayList<RecipientEntry>();
entryList.add(RecipientEntry.constructTopLevelEntry(
entry.displayName,
entry.displayNameSource,
entry.destination, entry.destinationType, entry.destinationLabel,
entry.contactId, entry.directoryId, entry.dataId, entry.thumbnailUriString,
true, entry.lookupKey));
entryMap.put(entry.contactId, entryList);
}
| public void | registerUpdateObserver(com.android.ex.chips.BaseRecipientAdapter$EntriesUpdatedObserver observer)
mEntriesUpdatedObserver = observer;
| protected java.util.List | searchOtherDirectories(java.util.Set existingDestinations)
// After having local results, check the size of results. If the results are
// not enough, we search remote directories, which will take longer time.
final int limit = mPreferredMaxResultCount - existingDestinations.size();
if (limit > 0) {
if (DEBUG) {
Log.d(TAG, "More entries should be needed (current: "
+ existingDestinations.size()
+ ", remaining limit: " + limit + ") ");
}
final Cursor directoryCursor = mContentResolver.query(
DirectoryListQuery.URI, DirectoryListQuery.PROJECTION,
null, null, null);
return setupOtherDirectories(mContext, directoryCursor, mAccount);
} else {
// We don't need to search other directories.
return null;
}
| public void | setAccount(android.accounts.Account account)Set the account when known. Causes the search to prioritize contacts from that account.
mAccount = account;
| public void | setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter)
mDropdownChipLayouter = dropdownChipLayouter;
mDropdownChipLayouter.setQuery(mQueryMode);
| public void | setPhotoManager(PhotoManager photoManager)Enables overriding the default photo manager that is used.
mPhotoManager = photoManager;
| public static java.util.List | setupOtherDirectories(android.content.Context context, android.database.Cursor directoryCursor, android.accounts.Account account)
final PackageManager packageManager = context.getPackageManager();
final List<DirectorySearchParams> paramsList = new ArrayList<DirectorySearchParams>();
DirectorySearchParams preferredDirectory = null;
while (directoryCursor.moveToNext()) {
final long id = directoryCursor.getLong(DirectoryListQuery.ID);
// Skip the local invisible directory, because the default directory already includes
// all local results.
if (id == Directory.LOCAL_INVISIBLE) {
continue;
}
final DirectorySearchParams params = new DirectorySearchParams();
final String packageName = directoryCursor.getString(DirectoryListQuery.PACKAGE_NAME);
final int resourceId = directoryCursor.getInt(DirectoryListQuery.TYPE_RESOURCE_ID);
params.directoryId = id;
params.displayName = directoryCursor.getString(DirectoryListQuery.DISPLAY_NAME);
params.accountName = directoryCursor.getString(DirectoryListQuery.ACCOUNT_NAME);
params.accountType = directoryCursor.getString(DirectoryListQuery.ACCOUNT_TYPE);
if (packageName != null && resourceId != 0) {
try {
final Resources resources =
packageManager.getResourcesForApplication(packageName);
params.directoryType = resources.getString(resourceId);
if (params.directoryType == null) {
Log.e(TAG, "Cannot resolve directory name: "
+ resourceId + "@" + packageName);
}
} catch (NameNotFoundException e) {
Log.e(TAG, "Cannot resolve directory name: "
+ resourceId + "@" + packageName, e);
}
}
// If an account has been provided and we found a directory that
// corresponds to that account, place that directory second, directly
// underneath the local contacts.
if (account != null && account.name.equals(params.accountName) &&
account.type.equals(params.accountType)) {
preferredDirectory = params;
} else {
paramsList.add(params);
}
}
if (preferredDirectory != null) {
paramsList.add(1, preferredDirectory);
}
return paramsList;
| protected void | startSearchOtherDirectories(java.lang.CharSequence constraint, java.util.List paramsList, int limit)Starts search in other directories using {@link Filter}. Results will be handled in
{@link DirectoryFilter}.
final int count = paramsList.size();
// Note: skipping the default partition (index 0), which has already been loaded
for (int i = 1; i < count; i++) {
final DirectorySearchParams params = paramsList.get(i);
params.constraint = constraint;
if (params.filter == null) {
params.filter = new DirectoryFilter(params);
}
params.filter.setLimit(limit);
params.filter.filter(constraint);
}
// Directory search started. We may show "waiting" message if directory results are slow
// enough.
mRemainingDirectoryCount = count - 1;
mDelayedMessageHandler.sendDelayedLoadMessage();
| protected void | updateEntries(java.util.List newEntries)Resets {@link #mEntries} and notify the event to its parent ListView.
mEntries = newEntries;
mEntriesUpdatedObserver.onChanged(newEntries);
notifyDataSetChanged();
|
|