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