summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/ui/contact/ContactRecipientAdapter.java
blob: 240f28101d2e24d8c308c507087146a11c3f7b40 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
/*
 * 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<Cursor, Boolean> 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<Cursor, Boolean> filteredResults = getFilteredResultsCursor(getContext(),
                    searchText);
            final Cursor cursor = filteredResults.first;
            final boolean sorted = filteredResults.second;
            if (cursor != null) {
                try {
                    final List<RecipientEntry> entries = new ArrayList<RecipientEntry>();

                    // 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<Long> existingContactIds = new HashSet<Long>();
                    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<RecipientEntry> entries = (List<RecipientEntry>) results.values;
                updateEntries(entries);
            } 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;
                }
            }
        }
    }

    /**
     * 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);
    }
}