Home | History | Annotate | Download | only in contact
      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  */
     16 package com.android.messaging.ui.contact;
     17 
     18 import android.content.Context;
     19 import android.database.Cursor;
     20 import android.database.MergeCursor;
     21 import android.support.v4.util.Pair;
     22 import android.text.TextUtils;
     23 import android.text.util.Rfc822Token;
     24 import android.text.util.Rfc822Tokenizer;
     25 import android.view.LayoutInflater;
     26 import android.view.View;
     27 import android.view.ViewGroup;
     28 import android.widget.Filter;
     29 import android.widget.TextView;
     30 
     31 import com.android.ex.chips.BaseRecipientAdapter;
     32 import com.android.ex.chips.RecipientAlternatesAdapter;
     33 import com.android.ex.chips.RecipientAlternatesAdapter.RecipientMatchCallback;
     34 import com.android.ex.chips.RecipientEntry;
     35 import com.android.messaging.R;
     36 import com.android.messaging.util.Assert;
     37 import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
     38 import com.android.messaging.util.BugleGservices;
     39 import com.android.messaging.util.BugleGservicesKeys;
     40 import com.android.messaging.util.ContactRecipientEntryUtils;
     41 import com.android.messaging.util.ContactUtil;
     42 import com.android.messaging.util.OsUtil;
     43 import com.android.messaging.util.PhoneUtils;
     44 
     45 import java.text.Collator;
     46 import java.util.ArrayList;
     47 import java.util.Collections;
     48 import java.util.Comparator;
     49 import java.util.HashMap;
     50 import java.util.HashSet;
     51 import java.util.List;
     52 import java.util.Locale;
     53 import java.util.Map;
     54 
     55 /**
     56  * An extension on the base {@link BaseRecipientAdapter} that uses data layer from Bugle,
     57  * such as the ContactRecipientPhotoManager that uses our own MediaResourceManager, and
     58  * contact lookup that relies on ContactUtil. It provides data source and filtering ability
     59  * for {@link ContactRecipientAutoCompleteView}
     60  */
     61 public final class ContactRecipientAdapter extends BaseRecipientAdapter {
     62     private static final int WORD_DIRECTORY_HEADER_POS_NONE = -1;
     63     /**
     64      * Stores the index of work directory header.
     65      */
     66     private int mWorkDirectoryHeaderPos = WORD_DIRECTORY_HEADER_POS_NONE;
     67     private final LayoutInflater mInflater;
     68 
     69     /**
     70      * Type of directory entry.
     71      */
     72     private static final int ENTRY_TYPE_DIRECTORY = RecipientEntry.ENTRY_TYPE_SIZE;
     73 
     74     public ContactRecipientAdapter(final Context context,
     75             final ContactListItemView.HostInterface clivHost) {
     76         this(context, Integer.MAX_VALUE, QUERY_TYPE_PHONE, clivHost);
     77     }
     78 
     79     public ContactRecipientAdapter(final Context context, final int preferredMaxResultCount,
     80             final int queryMode, final ContactListItemView.HostInterface clivHost) {
     81         super(context, preferredMaxResultCount, queryMode);
     82         setPhotoManager(new ContactRecipientPhotoManager(context, clivHost));
     83         mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
     84     }
     85 
     86     @Override
     87     public boolean forceShowAddress() {
     88         // We should always use the SingleRecipientAddressAdapter
     89         // And never use the RecipientAlternatesAdapter
     90         return true;
     91     }
     92 
     93     @Override
     94     public Filter getFilter() {
     95         return new ContactFilter();
     96     }
     97 
     98     /**
     99      * A Filter for a RecipientEditTextView that queries Bugle's ContactUtil for auto-complete
    100      * results.
    101      */
    102     public class ContactFilter extends Filter {
    103 
    104         // Used to sort filtered contacts when it has combined results from email and phone.
    105         private final RecipientEntryComparator mComparator = new RecipientEntryComparator();
    106 
    107         /**
    108          * Returns a cursor containing the filtered results in contacts given the search text,
    109          * and a boolean indicating whether the results are sorted.
    110          *
    111          * The queries are synchronously performed since this is not run on the main thread.
    112          *
    113          * Some locales (e.g. JPN) expect email addresses to be auto-completed for MMS.
    114          * If this is the case, perform two queries on phone number followed by email and
    115          * return the merged results.
    116          */
    117         @DoesNotRunOnMainThread
    118         private CursorResult getFilteredResultsCursor(final String searchText) {
    119             Assert.isNotMainThread();
    120             if (BugleGservices.get().getBoolean(
    121                     BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS,
    122                     BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS_DEFAULT)) {
    123 
    124                 final Cursor personalFilterPhonesCursor = ContactUtil
    125                         .filterPhones(getContext(), searchText).performSynchronousQuery();
    126                 final Cursor personalFilterEmailsCursor = ContactUtil
    127                         .filterEmails(getContext(), searchText).performSynchronousQuery();
    128                 final Cursor personalCursor = new MergeCursor(
    129                         new Cursor[]{personalFilterEmailsCursor, personalFilterPhonesCursor});
    130                 final CursorResult cursorResult =
    131                         new CursorResult(personalCursor, false /* sorted */);
    132                 if (OsUtil.isAtLeastN()) {
    133                     // Including enterprise result starting from N.
    134                     final Cursor enterpriseFilterPhonesCursor = ContactUtil.filterPhonesEnterprise(
    135                             getContext(), searchText).performSynchronousQuery();
    136                     final Cursor enterpriseFilterEmailsCursor = ContactUtil.filterEmailsEnterprise(
    137                             getContext(), searchText).performSynchronousQuery();
    138                     final Cursor enterpriseCursor = new MergeCursor(
    139                             new Cursor[]{enterpriseFilterEmailsCursor,
    140                                     enterpriseFilterPhonesCursor});
    141                     cursorResult.enterpriseCursor = enterpriseCursor;
    142                 }
    143                 return cursorResult;
    144             } else {
    145                 final Cursor personalFilterDestinationCursor = ContactUtil
    146                         .filterDestination(getContext(), searchText).performSynchronousQuery();
    147                 final CursorResult cursorResult = new CursorResult(personalFilterDestinationCursor,
    148                         true);
    149                 if (OsUtil.isAtLeastN()) {
    150                     // Including enterprise result starting from N.
    151                     final Cursor enterpriseFilterDestinationCursor = ContactUtil
    152                             .filterDestinationEnterprise(getContext(), searchText)
    153                             .performSynchronousQuery();
    154                     cursorResult.enterpriseCursor = enterpriseFilterDestinationCursor;
    155                 }
    156                 return cursorResult;
    157             }
    158         }
    159 
    160         @Override
    161         protected FilterResults performFiltering(final CharSequence constraint) {
    162             Assert.isNotMainThread();
    163             final FilterResults results = new FilterResults();
    164 
    165             // No query, return empty results.
    166             if (TextUtils.isEmpty(constraint)) {
    167                 clearTempEntries();
    168                 return results;
    169             }
    170 
    171             final String searchText = constraint.toString();
    172 
    173             // Query for auto-complete results, since performFiltering() is not done on the
    174             // main thread, perform the cursor loader queries directly.
    175 
    176             final CursorResult cursorResult = getFilteredResultsCursor(searchText);
    177             final List<RecipientEntry> entries = new ArrayList<>();
    178 
    179             // First check if the constraint is a valid SMS destination. If so, add the
    180             // destination as a suggestion item to the drop down.
    181             if (PhoneUtils.isValidSmsMmsDestination(searchText)) {
    182                 entries.add(ContactRecipientEntryUtils
    183                         .constructSendToDestinationEntry(searchText));
    184             }
    185 
    186             // Only show work directory header if more than one result in work directory.
    187             int workDirectoryHeaderPos = WORD_DIRECTORY_HEADER_POS_NONE;
    188             if (cursorResult.enterpriseCursor != null
    189                     && cursorResult.enterpriseCursor.getCount() > 0) {
    190                 if (cursorResult.personalCursor != null) {
    191                     workDirectoryHeaderPos = entries.size();
    192                     workDirectoryHeaderPos += cursorResult.personalCursor.getCount();
    193                 }
    194             }
    195 
    196             final Cursor[] cursors = new Cursor[]{cursorResult.personalCursor,
    197                     cursorResult.enterpriseCursor};
    198             for (Cursor cursor : cursors) {
    199                 if (cursor != null) {
    200                     try {
    201                         final List<RecipientEntry> tempEntries = new ArrayList<>();
    202                         HashSet<Long> existingContactIds = new HashSet<>();
    203                         while (cursor.moveToNext()) {
    204                             // Make sure there's only one first-level contact (i.e. contact for
    205                             // which we show the avatar picture and name) for every contact id.
    206                             final long contactId = cursor.getLong(ContactUtil.INDEX_CONTACT_ID);
    207                             final boolean isFirstLevel = !existingContactIds.contains(contactId);
    208                             if (isFirstLevel) {
    209                                 existingContactIds.add(contactId);
    210                             }
    211                             tempEntries.add(ContactUtil.createRecipientEntryForPhoneQuery(cursor,
    212                                     isFirstLevel));
    213                         }
    214 
    215                         if (!cursorResult.isSorted) {
    216                             Collections.sort(tempEntries, mComparator);
    217                         }
    218                         entries.addAll(tempEntries);
    219                     } finally {
    220                         cursor.close();
    221                     }
    222                 }
    223             }
    224             results.values = new ContactReceipientFilterResult(entries, workDirectoryHeaderPos);
    225             results.count = 1;
    226             return results;
    227         }
    228 
    229         @Override
    230         protected void publishResults(final CharSequence constraint, final FilterResults results) {
    231             mCurrentConstraint = constraint;
    232             clearTempEntries();
    233 
    234             final ContactReceipientFilterResult contactReceipientFilterResult
    235                     = (ContactReceipientFilterResult) results.values;
    236             if (contactReceipientFilterResult != null) {
    237                 mWorkDirectoryHeaderPos = contactReceipientFilterResult.workDirectoryPos;
    238                 if (contactReceipientFilterResult.recipientEntries != null) {
    239                     updateEntries(contactReceipientFilterResult.recipientEntries);
    240                 } else {
    241                     updateEntries(Collections.<RecipientEntry>emptyList());
    242                 }
    243             }
    244         }
    245 
    246         private class RecipientEntryComparator implements Comparator<RecipientEntry> {
    247 
    248             private final Collator mCollator;
    249 
    250             public RecipientEntryComparator() {
    251                 mCollator = Collator.getInstance(Locale.getDefault());
    252                 mCollator.setStrength(Collator.PRIMARY);
    253             }
    254 
    255             /**
    256              * Compare two RecipientEntry's, first by locale-aware display name comparison, then by
    257              * contact id comparison, finally by first-level-ness comparison.
    258              */
    259             @Override
    260             public int compare(RecipientEntry lhs, RecipientEntry rhs) {
    261                 // Send-to-destinations always appear before everything else.
    262                 final boolean sendToLhs = ContactRecipientEntryUtils
    263                         .isSendToDestinationContact(lhs);
    264                 final boolean sendToRhs = ContactRecipientEntryUtils
    265                         .isSendToDestinationContact(lhs);
    266                 if (sendToLhs != sendToRhs) {
    267                     if (sendToLhs) {
    268                         return -1;
    269                     } else if (sendToRhs) {
    270                         return 1;
    271                     }
    272                 }
    273 
    274                 final int displayNameCompare = mCollator.compare(lhs.getDisplayName(),
    275                         rhs.getDisplayName());
    276                 if (displayNameCompare != 0) {
    277                     return displayNameCompare;
    278                 }
    279 
    280                 // Long.compare could accomplish the following three lines, but this is only
    281                 // available in API 19+
    282                 final long lhsContactId = lhs.getContactId();
    283                 final long rhsContactId = rhs.getContactId();
    284                 final int contactCompare = lhsContactId < rhsContactId ? -1 :
    285                         (lhsContactId == rhsContactId ? 0 : 1);
    286                 if (contactCompare != 0) {
    287                     return contactCompare;
    288                 }
    289 
    290                 // These are the same contact. Make sure first-level contacts always
    291                 // appear at the front.
    292                 if (lhs.isFirstLevel()) {
    293                     return -1;
    294                 } else if (rhs.isFirstLevel()) {
    295                     return 1;
    296                 } else {
    297                     return 0;
    298                 }
    299             }
    300         }
    301 
    302         private class CursorResult {
    303 
    304             public final Cursor personalCursor;
    305 
    306             public Cursor enterpriseCursor;
    307 
    308             public final boolean isSorted;
    309 
    310             public CursorResult(Cursor personalCursor, boolean isSorted) {
    311                 this.personalCursor = personalCursor;
    312                 this.isSorted = isSorted;
    313             }
    314         }
    315 
    316         private class ContactReceipientFilterResult {
    317             /**
    318              * Recipient entries in all directories.
    319              */
    320             public final List<RecipientEntry> recipientEntries;
    321 
    322             /**
    323              * Index of row that showing work directory header.
    324              */
    325             public final int workDirectoryPos;
    326 
    327             public ContactReceipientFilterResult(List<RecipientEntry> recipientEntries,
    328                     int workDirectoryPos) {
    329                 this.recipientEntries = recipientEntries;
    330                 this.workDirectoryPos = workDirectoryPos;
    331             }
    332         }
    333     }
    334 
    335     /**
    336      * Called when we need to substitute temporary recipient chips with better alternatives.
    337      * For example, if a list of comma-delimited phone numbers are pasted into the edit box,
    338      * we want to be able to look up in the ContactUtil for exact matches and get contact
    339      * details such as name and photo thumbnail for the contact to display a better chip.
    340      */
    341     @Override
    342     public void getMatchingRecipients(final ArrayList<String> inAddresses,
    343             final RecipientMatchCallback callback) {
    344         final int addressesSize = Math.min(
    345                 RecipientAlternatesAdapter.MAX_LOOKUPS, inAddresses.size());
    346         final HashSet<String> addresses = new HashSet<String>();
    347         for (int i = 0; i < addressesSize; i++) {
    348             final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(inAddresses.get(i).toLowerCase());
    349             addresses.add(tokens.length > 0 ? tokens[0].getAddress() : inAddresses.get(i));
    350         }
    351 
    352         final Map<String, RecipientEntry> recipientEntries =
    353                 new HashMap<String, RecipientEntry>();
    354         // query for each address
    355         for (final String address : addresses) {
    356             final Cursor cursor = ContactUtil.lookupDestination(getContext(), address)
    357                     .performSynchronousQuery();
    358             if (cursor != null) {
    359                 try {
    360                     if (cursor.moveToNext()) {
    361                         // There may be multiple matches to the same number, always take the
    362                         // first match.
    363                         // TODO: May need to consider if there's an existing conversation
    364                         // that matches this particular contact and prioritize that contact.
    365                         final RecipientEntry entry =
    366                                 ContactUtil.createRecipientEntryForPhoneQuery(cursor, true);
    367                         recipientEntries.put(address, entry);
    368                     }
    369 
    370                 } finally {
    371                     cursor.close();
    372                 }
    373             }
    374         }
    375 
    376         // report matches
    377         callback.matchesFound(recipientEntries);
    378     }
    379 
    380     /**
    381      * We handle directory header here and then delegate the work of creating recipient views to
    382      * the {@link BaseRecipientAdapter}. Please notice that we need to fix the position
    383      * before passing to {@link BaseRecipientAdapter} because it is not aware of the existence of
    384      * directory headers.
    385      */
    386     @Override
    387     public View getView(int position, View convertView, ViewGroup parent) {
    388         TextView textView;
    389         if (isDirectoryEntry(position)) {
    390             if (convertView == null) {
    391                 textView = (TextView) mInflater.inflate(R.layout.work_directory_header, parent,
    392                         false);
    393             } else {
    394                 textView = (TextView) convertView;
    395             }
    396             return textView;
    397         }
    398         return super.getView(fixPosition(position), convertView, parent);
    399     }
    400 
    401     @Override
    402     public RecipientEntry getItem(int position) {
    403         if (isDirectoryEntry(position)) {
    404             return null;
    405         }
    406         return super.getItem(fixPosition(position));
    407     }
    408 
    409     @Override
    410     public int getViewTypeCount() {
    411         return RecipientEntry.ENTRY_TYPE_SIZE + 1;
    412     }
    413 
    414     @Override
    415     public int getItemViewType(int position) {
    416         if (isDirectoryEntry(position)) {
    417             return ENTRY_TYPE_DIRECTORY;
    418         }
    419         return super.getItemViewType(fixPosition(position));
    420     }
    421 
    422     @Override
    423     public boolean isEnabled(int position) {
    424         if (isDirectoryEntry(position)) {
    425             return false;
    426         }
    427         return super.isEnabled(fixPosition(position));
    428     }
    429 
    430     @Override
    431     public int getCount() {
    432         return super.getCount() + ((hasWorkDirectoryHeader()) ? 1 : 0);
    433     }
    434 
    435     private boolean isDirectoryEntry(int position) {
    436         return position == mWorkDirectoryHeaderPos;
    437     }
    438 
    439     /**
    440      * @return the position of items without counting directory headers.
    441      */
    442     private int fixPosition(int position) {
    443         if (hasWorkDirectoryHeader()) {
    444             Assert.isTrue(position != mWorkDirectoryHeaderPos);
    445             if (position > mWorkDirectoryHeaderPos) {
    446                 return position - 1;
    447             }
    448         }
    449         return position;
    450     }
    451 
    452     private boolean hasWorkDirectoryHeader() {
    453         return mWorkDirectoryHeaderPos != WORD_DIRECTORY_HEADER_POS_NONE;
    454     }
    455 
    456 }
    457