summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/ui/contact/ContactRecipientAdapter.java
blob: 1d91241860ecbc2ae22eee1bc9cb357c543e4eb5 (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
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
/*
 * 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;
    }

}