ContactRecipientAdapter.java revision d3b009ae55651f1e60950342468e3c37fdeb0796
1/*
2 * Copyright (C) 2015 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 */
16package com.android.messaging.ui.contact;
17
18import android.content.Context;
19import android.database.Cursor;
20import android.database.MergeCursor;
21import android.support.v4.util.Pair;
22import android.text.TextUtils;
23import android.text.util.Rfc822Token;
24import android.text.util.Rfc822Tokenizer;
25import android.widget.Filter;
26
27import com.android.ex.chips.BaseRecipientAdapter;
28import com.android.ex.chips.RecipientAlternatesAdapter;
29import com.android.ex.chips.RecipientAlternatesAdapter.RecipientMatchCallback;
30import com.android.ex.chips.RecipientEntry;
31import com.android.messaging.util.Assert;
32import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
33import com.android.messaging.util.BugleGservices;
34import com.android.messaging.util.BugleGservicesKeys;
35import com.android.messaging.util.ContactRecipientEntryUtils;
36import com.android.messaging.util.ContactUtil;
37import com.android.messaging.util.PhoneUtils;
38
39import java.text.Collator;
40import java.util.ArrayList;
41import java.util.Collections;
42import java.util.Comparator;
43import java.util.HashMap;
44import java.util.HashSet;
45import java.util.List;
46import java.util.Locale;
47import java.util.Map;
48
49/**
50 * An extension on the base {@link BaseRecipientAdapter} that uses data layer from Bugle,
51 * such as the ContactRecipientPhotoManager that uses our own MediaResourceManager, and
52 * contact lookup that relies on ContactUtil. It provides data source and filtering ability
53 * for {@link ContactRecipientAutoCompleteView}
54 */
55public final class ContactRecipientAdapter extends BaseRecipientAdapter {
56    public ContactRecipientAdapter(final Context context,
57            final ContactListItemView.HostInterface clivHost) {
58        this(context, Integer.MAX_VALUE, QUERY_TYPE_PHONE, clivHost);
59    }
60
61    public ContactRecipientAdapter(final Context context, final int preferredMaxResultCount,
62            final int queryMode, final ContactListItemView.HostInterface clivHost) {
63        super(context, preferredMaxResultCount, queryMode);
64        setPhotoManager(new ContactRecipientPhotoManager(context, clivHost));
65    }
66
67    @Override
68    public boolean forceShowAddress() {
69        // We should always use the SingleRecipientAddressAdapter
70        // And never use the RecipientAlternatesAdapter
71        return true;
72    }
73
74    @Override
75    public Filter getFilter() {
76        return new ContactFilter();
77    }
78
79    /**
80     * A Filter for a RecipientEditTextView that queries Bugle's ContactUtil for auto-complete
81     * results.
82     */
83    public class ContactFilter extends Filter {
84        // Used to sort filtered contacts when it has combined results from email and phone.
85        private final RecipientEntryComparator mComparator = new RecipientEntryComparator();
86
87        /**
88         * Returns a cursor containing the filtered results in contacts given the search text,
89         * and a boolean indicating whether the results are sorted.
90         *
91         * The queries are synchronously performed since this is not run on the main thread.
92         *
93         * Some locales (e.g. JPN) expect email addresses to be auto-completed for MMS.
94         * If this is the case, perform two queries on phone number followed by email and
95         * return the merged results.
96         */
97        @DoesNotRunOnMainThread
98        private Pair<Cursor, Boolean> getFilteredResultsCursor(final Context context,
99                final String searchText) {
100            Assert.isNotMainThread();
101            if (BugleGservices.get().getBoolean(
102                    BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS,
103                    BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS_DEFAULT)) {
104                return Pair.create((Cursor) new MergeCursor(new Cursor[] {
105                        ContactUtil.filterPhones(getContext(), searchText)
106                                .performSynchronousQuery(),
107                        ContactUtil.filterEmails(getContext(), searchText)
108                                .performSynchronousQuery()
109                }), false /* the merged cursor is not sorted */);
110            } else {
111                return Pair.create(ContactUtil.filterDestination(getContext(), searchText)
112                        .performSynchronousQuery(), true);
113            }
114        }
115
116        @Override
117        protected FilterResults performFiltering(final CharSequence constraint) {
118            Assert.isNotMainThread();
119            final FilterResults results = new FilterResults();
120
121            // No query, return empty results.
122            if (TextUtils.isEmpty(constraint)) {
123                clearTempEntries();
124                return results;
125            }
126
127            final String searchText = constraint.toString();
128
129            // Query for auto-complete results, since performFiltering() is not done on the
130            // main thread, perform the cursor loader queries directly.
131            final Pair<Cursor, Boolean> filteredResults = getFilteredResultsCursor(getContext(),
132                    searchText);
133            final Cursor cursor = filteredResults.first;
134            final boolean sorted = filteredResults.second;
135            if (cursor != null) {
136                try {
137                    final List<RecipientEntry> entries = new ArrayList<RecipientEntry>();
138
139                    // First check if the constraint is a valid SMS destination. If so, add the
140                    // destination as a suggestion item to the drop down.
141                    if (PhoneUtils.isValidSmsMmsDestination(searchText)) {
142                        entries.add(ContactRecipientEntryUtils
143                                .constructSendToDestinationEntry(searchText));
144                    }
145
146                    HashSet<Long> existingContactIds = new HashSet<Long>();
147                    while (cursor.moveToNext()) {
148                        // Make sure there's only one first-level contact (i.e. contact for which
149                        // we show the avatar picture and name) for every contact id.
150                        final long contactId = cursor.getLong(ContactUtil.INDEX_CONTACT_ID);
151                        final boolean isFirstLevel = !existingContactIds.contains(contactId);
152                        if (isFirstLevel) {
153                            existingContactIds.add(contactId);
154                        }
155                        entries.add(ContactUtil.createRecipientEntryForPhoneQuery(cursor,
156                                isFirstLevel));
157                    }
158
159                    if (!sorted) {
160                        Collections.sort(entries, mComparator);
161                    }
162                    results.values = entries;
163                    results.count = 1;
164
165                } finally {
166                    cursor.close();
167                }
168            }
169            return results;
170        }
171
172        @Override
173        protected void publishResults(final CharSequence constraint, final FilterResults results) {
174            mCurrentConstraint = constraint;
175            clearTempEntries();
176
177            if (results.values != null) {
178                @SuppressWarnings("unchecked")
179                final List<RecipientEntry> entries = (List<RecipientEntry>) results.values;
180                updateEntries(entries);
181            } else {
182                updateEntries(Collections.<RecipientEntry>emptyList());
183            }
184        }
185
186        private class RecipientEntryComparator implements Comparator<RecipientEntry> {
187            private final Collator mCollator;
188
189            public RecipientEntryComparator() {
190                mCollator = Collator.getInstance(Locale.getDefault());
191                mCollator.setStrength(Collator.PRIMARY);
192            }
193
194            /**
195             * Compare two RecipientEntry's, first by locale-aware display name comparison, then by
196             * contact id comparison, finally by first-level-ness comparison.
197             */
198            @Override
199            public int compare(RecipientEntry lhs, RecipientEntry rhs) {
200                // Send-to-destinations always appear before everything else.
201                final boolean sendToLhs = ContactRecipientEntryUtils
202                        .isSendToDestinationContact(lhs);
203                final boolean sendToRhs = ContactRecipientEntryUtils
204                        .isSendToDestinationContact(lhs);
205                if (sendToLhs != sendToRhs) {
206                    if (sendToLhs) {
207                        return -1;
208                    } else if (sendToRhs) {
209                        return 1;
210                    }
211                }
212
213                final int displayNameCompare = mCollator.compare(lhs.getDisplayName(),
214                        rhs.getDisplayName());
215                if (displayNameCompare != 0) {
216                    return displayNameCompare;
217                }
218
219                // Long.compare could accomplish the following three lines, but this is only
220                // available in API 19+
221                final long lhsContactId = lhs.getContactId();
222                final long rhsContactId = rhs.getContactId();
223                final int contactCompare = lhsContactId < rhsContactId ? -1 :
224                        (lhsContactId == rhsContactId ? 0 : 1);
225                if (contactCompare != 0) {
226                    return contactCompare;
227                }
228
229                // These are the same contact. Make sure first-level contacts always
230                // appear at the front.
231                if (lhs.isFirstLevel()) {
232                    return -1;
233                } else if (rhs.isFirstLevel()) {
234                    return 1;
235                } else {
236                    return 0;
237                }
238            }
239        }
240    }
241
242    /**
243     * Called when we need to substitute temporary recipient chips with better alternatives.
244     * For example, if a list of comma-delimited phone numbers are pasted into the edit box,
245     * we want to be able to look up in the ContactUtil for exact matches and get contact
246     * details such as name and photo thumbnail for the contact to display a better chip.
247     */
248    @Override
249    public void getMatchingRecipients(final ArrayList<String> inAddresses,
250            final RecipientMatchCallback callback) {
251        final int addressesSize = Math.min(
252                RecipientAlternatesAdapter.MAX_LOOKUPS, inAddresses.size());
253        final HashSet<String> addresses = new HashSet<String>();
254        for (int i = 0; i < addressesSize; i++) {
255            final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(inAddresses.get(i).toLowerCase());
256            addresses.add(tokens.length > 0 ? tokens[0].getAddress() : inAddresses.get(i));
257        }
258
259        final Map<String, RecipientEntry> recipientEntries =
260                new HashMap<String, RecipientEntry>();
261        // query for each address
262        for (final String address : addresses) {
263            final Cursor cursor = ContactUtil.lookupDestination(getContext(), address)
264                    .performSynchronousQuery();
265            if (cursor != null) {
266                try {
267                    if (cursor.moveToNext()) {
268                        // There may be multiple matches to the same number, always take the
269                        // first match.
270                        // TODO: May need to consider if there's an existing conversation
271                        // that matches this particular contact and prioritize that contact.
272                        final RecipientEntry entry =
273                                ContactUtil.createRecipientEntryForPhoneQuery(cursor, true);
274                        recipientEntries.put(address, entry);
275                    }
276
277                } finally {
278                    cursor.close();
279                }
280            }
281        }
282
283        // report matches
284        callback.matchesFound(recipientEntries);
285    }
286}
287