This change allows sms app to show search result of different directories in different partitions. Currently, only personal default and enterprise default directory are searched. BUG=26021888 BUG=28016344 (cherry picked from commit 59666b32c192bd28807ccd2f9153c796668bdf13) Change-Id: I733ceaae806d13661861126ce61c34108ac7e662
457 lines
19 KiB
Java
457 lines
19 KiB
Java
/*
|
|
* Copyright (C) 2015 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package com.android.messaging.ui.contact;
|
|
|
|
import android.content.Context;
|
|
import android.database.Cursor;
|
|
import android.database.MergeCursor;
|
|
import android.support.v4.util.Pair;
|
|
import android.text.TextUtils;
|
|
import android.text.util.Rfc822Token;
|
|
import android.text.util.Rfc822Tokenizer;
|
|
import android.view.LayoutInflater;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.widget.Filter;
|
|
import android.widget.TextView;
|
|
|
|
import com.android.ex.chips.BaseRecipientAdapter;
|
|
import com.android.ex.chips.RecipientAlternatesAdapter;
|
|
import com.android.ex.chips.RecipientAlternatesAdapter.RecipientMatchCallback;
|
|
import com.android.ex.chips.RecipientEntry;
|
|
import com.android.messaging.R;
|
|
import com.android.messaging.util.Assert;
|
|
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
|
|
import com.android.messaging.util.BugleGservices;
|
|
import com.android.messaging.util.BugleGservicesKeys;
|
|
import com.android.messaging.util.ContactRecipientEntryUtils;
|
|
import com.android.messaging.util.ContactUtil;
|
|
import com.android.messaging.util.OsUtil;
|
|
import com.android.messaging.util.PhoneUtils;
|
|
|
|
import java.text.Collator;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Comparator;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* An extension on the base {@link BaseRecipientAdapter} that uses data layer from Bugle,
|
|
* such as the ContactRecipientPhotoManager that uses our own MediaResourceManager, and
|
|
* contact lookup that relies on ContactUtil. It provides data source and filtering ability
|
|
* for {@link ContactRecipientAutoCompleteView}
|
|
*/
|
|
public final class ContactRecipientAdapter extends BaseRecipientAdapter {
|
|
private static final int WORD_DIRECTORY_HEADER_POS_NONE = -1;
|
|
/**
|
|
* Stores the index of work directory header.
|
|
*/
|
|
private int mWorkDirectoryHeaderPos = WORD_DIRECTORY_HEADER_POS_NONE;
|
|
private final LayoutInflater mInflater;
|
|
|
|
/**
|
|
* Type of directory entry.
|
|
*/
|
|
private static final int ENTRY_TYPE_DIRECTORY = RecipientEntry.ENTRY_TYPE_SIZE;
|
|
|
|
public ContactRecipientAdapter(final Context context,
|
|
final ContactListItemView.HostInterface clivHost) {
|
|
this(context, Integer.MAX_VALUE, QUERY_TYPE_PHONE, clivHost);
|
|
}
|
|
|
|
public ContactRecipientAdapter(final Context context, final int preferredMaxResultCount,
|
|
final int queryMode, final ContactListItemView.HostInterface clivHost) {
|
|
super(context, preferredMaxResultCount, queryMode);
|
|
setPhotoManager(new ContactRecipientPhotoManager(context, clivHost));
|
|
mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
}
|
|
|
|
@Override
|
|
public boolean forceShowAddress() {
|
|
// We should always use the SingleRecipientAddressAdapter
|
|
// And never use the RecipientAlternatesAdapter
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public Filter getFilter() {
|
|
return new ContactFilter();
|
|
}
|
|
|
|
/**
|
|
* A Filter for a RecipientEditTextView that queries Bugle's ContactUtil for auto-complete
|
|
* results.
|
|
*/
|
|
public class ContactFilter extends Filter {
|
|
|
|
// Used to sort filtered contacts when it has combined results from email and phone.
|
|
private final RecipientEntryComparator mComparator = new RecipientEntryComparator();
|
|
|
|
/**
|
|
* Returns a cursor containing the filtered results in contacts given the search text,
|
|
* and a boolean indicating whether the results are sorted.
|
|
*
|
|
* The queries are synchronously performed since this is not run on the main thread.
|
|
*
|
|
* Some locales (e.g. JPN) expect email addresses to be auto-completed for MMS.
|
|
* If this is the case, perform two queries on phone number followed by email and
|
|
* return the merged results.
|
|
*/
|
|
@DoesNotRunOnMainThread
|
|
private CursorResult getFilteredResultsCursor(final String searchText) {
|
|
Assert.isNotMainThread();
|
|
if (BugleGservices.get().getBoolean(
|
|
BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS,
|
|
BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS_DEFAULT)) {
|
|
|
|
final Cursor personalFilterPhonesCursor = ContactUtil
|
|
.filterPhones(getContext(), searchText).performSynchronousQuery();
|
|
final Cursor personalFilterEmailsCursor = ContactUtil
|
|
.filterEmails(getContext(), searchText).performSynchronousQuery();
|
|
final Cursor personalCursor = new MergeCursor(
|
|
new Cursor[]{personalFilterEmailsCursor, personalFilterPhonesCursor});
|
|
final CursorResult cursorResult =
|
|
new CursorResult(personalCursor, false /* sorted */);
|
|
if (OsUtil.isAtLeastN()) {
|
|
// Including enterprise result starting from N.
|
|
final Cursor enterpriseFilterPhonesCursor = ContactUtil.filterPhonesEnterprise(
|
|
getContext(), searchText).performSynchronousQuery();
|
|
final Cursor enterpriseFilterEmailsCursor = ContactUtil.filterEmailsEnterprise(
|
|
getContext(), searchText).performSynchronousQuery();
|
|
final Cursor enterpriseCursor = new MergeCursor(
|
|
new Cursor[]{enterpriseFilterEmailsCursor,
|
|
enterpriseFilterPhonesCursor});
|
|
cursorResult.enterpriseCursor = enterpriseCursor;
|
|
}
|
|
return cursorResult;
|
|
} else {
|
|
final Cursor personalFilterDestinationCursor = ContactUtil
|
|
.filterDestination(getContext(), searchText).performSynchronousQuery();
|
|
final CursorResult cursorResult = new CursorResult(personalFilterDestinationCursor,
|
|
true);
|
|
if (OsUtil.isAtLeastN()) {
|
|
// Including enterprise result starting from N.
|
|
final Cursor enterpriseFilterDestinationCursor = ContactUtil
|
|
.filterDestinationEnterprise(getContext(), searchText)
|
|
.performSynchronousQuery();
|
|
cursorResult.enterpriseCursor = enterpriseFilterDestinationCursor;
|
|
}
|
|
return cursorResult;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected FilterResults performFiltering(final CharSequence constraint) {
|
|
Assert.isNotMainThread();
|
|
final FilterResults results = new FilterResults();
|
|
|
|
// No query, return empty results.
|
|
if (TextUtils.isEmpty(constraint)) {
|
|
clearTempEntries();
|
|
return results;
|
|
}
|
|
|
|
final String searchText = constraint.toString();
|
|
|
|
// Query for auto-complete results, since performFiltering() is not done on the
|
|
// main thread, perform the cursor loader queries directly.
|
|
|
|
final CursorResult cursorResult = getFilteredResultsCursor(searchText);
|
|
final List<RecipientEntry> entries = new ArrayList<>();
|
|
|
|
// First check if the constraint is a valid SMS destination. If so, add the
|
|
// destination as a suggestion item to the drop down.
|
|
if (PhoneUtils.isValidSmsMmsDestination(searchText)) {
|
|
entries.add(ContactRecipientEntryUtils
|
|
.constructSendToDestinationEntry(searchText));
|
|
}
|
|
|
|
// Only show work directory header if more than one result in work directory.
|
|
int workDirectoryHeaderPos = WORD_DIRECTORY_HEADER_POS_NONE;
|
|
if (cursorResult.enterpriseCursor != null
|
|
&& cursorResult.enterpriseCursor.getCount() > 0) {
|
|
if (cursorResult.personalCursor != null) {
|
|
workDirectoryHeaderPos = entries.size();
|
|
workDirectoryHeaderPos += cursorResult.personalCursor.getCount();
|
|
}
|
|
}
|
|
|
|
final Cursor[] cursors = new Cursor[]{cursorResult.personalCursor,
|
|
cursorResult.enterpriseCursor};
|
|
for (Cursor cursor : cursors) {
|
|
if (cursor != null) {
|
|
try {
|
|
final List<RecipientEntry> tempEntries = new ArrayList<>();
|
|
HashSet<Long> existingContactIds = new HashSet<>();
|
|
while (cursor.moveToNext()) {
|
|
// Make sure there's only one first-level contact (i.e. contact for
|
|
// which we show the avatar picture and name) for every contact id.
|
|
final long contactId = cursor.getLong(ContactUtil.INDEX_CONTACT_ID);
|
|
final boolean isFirstLevel = !existingContactIds.contains(contactId);
|
|
if (isFirstLevel) {
|
|
existingContactIds.add(contactId);
|
|
}
|
|
tempEntries.add(ContactUtil.createRecipientEntryForPhoneQuery(cursor,
|
|
isFirstLevel));
|
|
}
|
|
|
|
if (!cursorResult.isSorted) {
|
|
Collections.sort(tempEntries, mComparator);
|
|
}
|
|
entries.addAll(tempEntries);
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
}
|
|
results.values = new ContactReceipientFilterResult(entries, workDirectoryHeaderPos);
|
|
results.count = 1;
|
|
return results;
|
|
}
|
|
|
|
@Override
|
|
protected void publishResults(final CharSequence constraint, final FilterResults results) {
|
|
mCurrentConstraint = constraint;
|
|
clearTempEntries();
|
|
|
|
final ContactReceipientFilterResult contactReceipientFilterResult
|
|
= (ContactReceipientFilterResult) results.values;
|
|
if (contactReceipientFilterResult != null) {
|
|
mWorkDirectoryHeaderPos = contactReceipientFilterResult.workDirectoryPos;
|
|
if (contactReceipientFilterResult.recipientEntries != null) {
|
|
updateEntries(contactReceipientFilterResult.recipientEntries);
|
|
} else {
|
|
updateEntries(Collections.<RecipientEntry>emptyList());
|
|
}
|
|
}
|
|
}
|
|
|
|
private class RecipientEntryComparator implements Comparator<RecipientEntry> {
|
|
|
|
private final Collator mCollator;
|
|
|
|
public RecipientEntryComparator() {
|
|
mCollator = Collator.getInstance(Locale.getDefault());
|
|
mCollator.setStrength(Collator.PRIMARY);
|
|
}
|
|
|
|
/**
|
|
* Compare two RecipientEntry's, first by locale-aware display name comparison, then by
|
|
* contact id comparison, finally by first-level-ness comparison.
|
|
*/
|
|
@Override
|
|
public int compare(RecipientEntry lhs, RecipientEntry rhs) {
|
|
// Send-to-destinations always appear before everything else.
|
|
final boolean sendToLhs = ContactRecipientEntryUtils
|
|
.isSendToDestinationContact(lhs);
|
|
final boolean sendToRhs = ContactRecipientEntryUtils
|
|
.isSendToDestinationContact(lhs);
|
|
if (sendToLhs != sendToRhs) {
|
|
if (sendToLhs) {
|
|
return -1;
|
|
} else if (sendToRhs) {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
final int displayNameCompare = mCollator.compare(lhs.getDisplayName(),
|
|
rhs.getDisplayName());
|
|
if (displayNameCompare != 0) {
|
|
return displayNameCompare;
|
|
}
|
|
|
|
// Long.compare could accomplish the following three lines, but this is only
|
|
// available in API 19+
|
|
final long lhsContactId = lhs.getContactId();
|
|
final long rhsContactId = rhs.getContactId();
|
|
final int contactCompare = lhsContactId < rhsContactId ? -1 :
|
|
(lhsContactId == rhsContactId ? 0 : 1);
|
|
if (contactCompare != 0) {
|
|
return contactCompare;
|
|
}
|
|
|
|
// These are the same contact. Make sure first-level contacts always
|
|
// appear at the front.
|
|
if (lhs.isFirstLevel()) {
|
|
return -1;
|
|
} else if (rhs.isFirstLevel()) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
private class CursorResult {
|
|
|
|
public final Cursor personalCursor;
|
|
|
|
public Cursor enterpriseCursor;
|
|
|
|
public final boolean isSorted;
|
|
|
|
public CursorResult(Cursor personalCursor, boolean isSorted) {
|
|
this.personalCursor = personalCursor;
|
|
this.isSorted = isSorted;
|
|
}
|
|
}
|
|
|
|
private class ContactReceipientFilterResult {
|
|
/**
|
|
* Recipient entries in all directories.
|
|
*/
|
|
public final List<RecipientEntry> recipientEntries;
|
|
|
|
/**
|
|
* Index of row that showing work directory header.
|
|
*/
|
|
public final int workDirectoryPos;
|
|
|
|
public ContactReceipientFilterResult(List<RecipientEntry> recipientEntries,
|
|
int workDirectoryPos) {
|
|
this.recipientEntries = recipientEntries;
|
|
this.workDirectoryPos = workDirectoryPos;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when we need to substitute temporary recipient chips with better alternatives.
|
|
* For example, if a list of comma-delimited phone numbers are pasted into the edit box,
|
|
* we want to be able to look up in the ContactUtil for exact matches and get contact
|
|
* details such as name and photo thumbnail for the contact to display a better chip.
|
|
*/
|
|
@Override
|
|
public void getMatchingRecipients(final ArrayList<String> inAddresses,
|
|
final RecipientMatchCallback callback) {
|
|
final int addressesSize = Math.min(
|
|
RecipientAlternatesAdapter.MAX_LOOKUPS, inAddresses.size());
|
|
final HashSet<String> addresses = new HashSet<String>();
|
|
for (int i = 0; i < addressesSize; i++) {
|
|
final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(inAddresses.get(i).toLowerCase());
|
|
addresses.add(tokens.length > 0 ? tokens[0].getAddress() : inAddresses.get(i));
|
|
}
|
|
|
|
final Map<String, RecipientEntry> recipientEntries =
|
|
new HashMap<String, RecipientEntry>();
|
|
// query for each address
|
|
for (final String address : addresses) {
|
|
final Cursor cursor = ContactUtil.lookupDestination(getContext(), address)
|
|
.performSynchronousQuery();
|
|
if (cursor != null) {
|
|
try {
|
|
if (cursor.moveToNext()) {
|
|
// There may be multiple matches to the same number, always take the
|
|
// first match.
|
|
// TODO: May need to consider if there's an existing conversation
|
|
// that matches this particular contact and prioritize that contact.
|
|
final RecipientEntry entry =
|
|
ContactUtil.createRecipientEntryForPhoneQuery(cursor, true);
|
|
recipientEntries.put(address, entry);
|
|
}
|
|
|
|
} finally {
|
|
cursor.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
// report matches
|
|
callback.matchesFound(recipientEntries);
|
|
}
|
|
|
|
/**
|
|
* We handle directory header here and then delegate the work of creating recipient views to
|
|
* the {@link BaseRecipientAdapter}. Please notice that we need to fix the position
|
|
* before passing to {@link BaseRecipientAdapter} because it is not aware of the existence of
|
|
* directory headers.
|
|
*/
|
|
@Override
|
|
public View getView(int position, View convertView, ViewGroup parent) {
|
|
TextView textView;
|
|
if (isDirectoryEntry(position)) {
|
|
if (convertView == null) {
|
|
textView = (TextView) mInflater.inflate(R.layout.work_directory_header, parent,
|
|
false);
|
|
} else {
|
|
textView = (TextView) convertView;
|
|
}
|
|
return textView;
|
|
}
|
|
return super.getView(fixPosition(position), convertView, parent);
|
|
}
|
|
|
|
@Override
|
|
public RecipientEntry getItem(int position) {
|
|
if (isDirectoryEntry(position)) {
|
|
return null;
|
|
}
|
|
return super.getItem(fixPosition(position));
|
|
}
|
|
|
|
@Override
|
|
public int getViewTypeCount() {
|
|
return RecipientEntry.ENTRY_TYPE_SIZE + 1;
|
|
}
|
|
|
|
@Override
|
|
public int getItemViewType(int position) {
|
|
if (isDirectoryEntry(position)) {
|
|
return ENTRY_TYPE_DIRECTORY;
|
|
}
|
|
return super.getItemViewType(fixPosition(position));
|
|
}
|
|
|
|
@Override
|
|
public boolean isEnabled(int position) {
|
|
if (isDirectoryEntry(position)) {
|
|
return false;
|
|
}
|
|
return super.isEnabled(fixPosition(position));
|
|
}
|
|
|
|
@Override
|
|
public int getCount() {
|
|
return super.getCount() + ((hasWorkDirectoryHeader()) ? 1 : 0);
|
|
}
|
|
|
|
private boolean isDirectoryEntry(int position) {
|
|
return position == mWorkDirectoryHeaderPos;
|
|
}
|
|
|
|
/**
|
|
* @return the position of items without counting directory headers.
|
|
*/
|
|
private int fixPosition(int position) {
|
|
if (hasWorkDirectoryHeader()) {
|
|
Assert.isTrue(position != mWorkDirectoryHeaderPos);
|
|
if (position > mWorkDirectoryHeaderPos) {
|
|
return position - 1;
|
|
}
|
|
}
|
|
return position;
|
|
}
|
|
|
|
private boolean hasWorkDirectoryHeader() {
|
|
return mWorkDirectoryHeaderPos != WORD_DIRECTORY_HEADER_POS_NONE;
|
|
}
|
|
|
|
}
|