/* * 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.widget.Filter; 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.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 { 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)); } @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 Pair getFilteredResultsCursor(final Context context, 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(); Cursor resultCursor; 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(); // TODO: Separating enterprise result from personal result (b/26021888) resultCursor = new MergeCursor( new Cursor[]{personalFilterEmailsCursor, enterpriseFilterEmailsCursor, personalFilterPhonesCursor, enterpriseFilterPhonesCursor}); } else { resultCursor = new MergeCursor( new Cursor[]{personalFilterEmailsCursor, personalFilterPhonesCursor}); } return Pair.create( resultCursor, false /* the merged cursor is not sorted */ ); } else { final Cursor personalFilterDestinationCursor = ContactUtil .filterDestination(getContext(), searchText).performSynchronousQuery(); Cursor resultCursor; boolean sorted; if (OsUtil.isAtLeastN()) { // Including enterprise result starting from N. final Cursor enterpriseFilterDestinationCursor = ContactUtil .filterDestinationEnterprise(getContext(), searchText) .performSynchronousQuery(); // TODO: Separating enterprise result from personal result (b/26021888) resultCursor = new MergeCursor(new Cursor[]{personalFilterDestinationCursor, enterpriseFilterDestinationCursor}); sorted = false; } else { resultCursor = personalFilterDestinationCursor; sorted = true; } return Pair.create(resultCursor, sorted); } } @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 Pair filteredResults = getFilteredResultsCursor(getContext(), searchText); final Cursor cursor = filteredResults.first; final boolean sorted = filteredResults.second; if (cursor != null) { try { final List 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)); } HashSet 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); } entries.add(ContactUtil.createRecipientEntryForPhoneQuery(cursor, isFirstLevel)); } if (!sorted) { Collections.sort(entries, mComparator); } results.values = entries; results.count = 1; } finally { cursor.close(); } } return results; } @Override protected void publishResults(final CharSequence constraint, final FilterResults results) { mCurrentConstraint = constraint; clearTempEntries(); if (results.values != null) { @SuppressWarnings("unchecked") final List entries = (List) results.values; updateEntries(entries); } else { updateEntries(Collections.emptyList()); } } private class RecipientEntryComparator implements Comparator { 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; } } } } /** * 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 inAddresses, final RecipientMatchCallback callback) { final int addressesSize = Math.min( RecipientAlternatesAdapter.MAX_LOOKUPS, inAddresses.size()); final HashSet addresses = new HashSet(); 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 recipientEntries = new HashMap(); // 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); } }