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.graphics.Rect;
     21 import android.os.AsyncTask;
     22 import android.support.v7.appcompat.R;
     23 import android.text.Editable;
     24 import android.text.TextPaint;
     25 import android.text.TextWatcher;
     26 import android.text.util.Rfc822Tokenizer;
     27 import android.util.AttributeSet;
     28 import android.view.ContextThemeWrapper;
     29 import android.view.KeyEvent;
     30 import android.view.inputmethod.EditorInfo;
     31 import android.widget.TextView;
     32 
     33 import com.android.ex.chips.RecipientEditTextView;
     34 import com.android.ex.chips.RecipientEntry;
     35 import com.android.ex.chips.recipientchip.DrawableRecipientChip;
     36 import com.android.messaging.datamodel.data.ParticipantData;
     37 import com.android.messaging.util.ContactRecipientEntryUtils;
     38 import com.android.messaging.util.ContactUtil;
     39 import com.android.messaging.util.PhoneUtils;
     40 
     41 import java.util.ArrayList;
     42 import java.util.HashSet;
     43 import java.util.Set;
     44 import java.util.concurrent.Executor;
     45 import java.util.concurrent.Executors;
     46 
     47 /**
     48  * An extension for {@link RecipientEditTextView} which shows a list of Materialized contact chips.
     49  * It uses Bugle's ContactUtil to perform contact lookup, and is able to return the list of
     50  * recipients in the form of a ParticipantData list.
     51  */
     52 public class ContactRecipientAutoCompleteView extends RecipientEditTextView {
     53     public interface ContactChipsChangeListener {
     54         void onContactChipsChanged(int oldCount, int newCount);
     55         void onInvalidContactChipsPruned(int prunedCount);
     56         void onEntryComplete();
     57     }
     58 
     59     private final int mTextHeight;
     60     private ContactChipsChangeListener mChipsChangeListener;
     61 
     62     /**
     63      * Watches changes in contact chips to determine possible state transitions.
     64      */
     65     private class ContactChipsWatcher implements TextWatcher {
     66         /**
     67          * Tracks the old chips count before text changes. Note that we currently don't compare
     68          * the entire chip sets but just the cheaper-to-do before and after counts, because
     69          * the chips view don't allow for replacing chips.
     70          */
     71         private int mLastChipsCount = 0;
     72 
     73         @Override
     74         public void onTextChanged(final CharSequence s, final int start, final int before,
     75                 final int count) {
     76         }
     77 
     78         @Override
     79         public void beforeTextChanged(final CharSequence s, final int start, final int count,
     80                 final int after) {
     81             // We don't take mLastChipsCount from here but from the last afterTextChanged() run.
     82             // The reason is because at this point, any chip spans to be removed is already removed
     83             // from s in the chips text view.
     84         }
     85 
     86         @Override
     87         public void afterTextChanged(final Editable s) {
     88             final int currentChipsCount = s.getSpans(0, s.length(),
     89                     DrawableRecipientChip.class).length;
     90             if (currentChipsCount != mLastChipsCount) {
     91                 // When a sanitizing task is running, we don't want to notify any chips count
     92                 // change, but we do want to track the last chip count.
     93                 if (mChipsChangeListener != null && mCurrentSanitizeTask == null) {
     94                     mChipsChangeListener.onContactChipsChanged(mLastChipsCount, currentChipsCount);
     95                 }
     96                 mLastChipsCount = currentChipsCount;
     97             }
     98         }
     99     }
    100 
    101     private static final String TEXT_HEIGHT_SAMPLE = "a";
    102 
    103     public ContactRecipientAutoCompleteView(final Context context, final AttributeSet attrs) {
    104         super(new ContextThemeWrapper(context, R.style.ColorAccentGrayOverrideStyle), attrs);
    105 
    106         // Get the height of the text, given the currently set font face and size.
    107         final Rect textBounds = new Rect(0, 0, 0, 0);
    108         final TextPaint paint = getPaint();
    109         paint.getTextBounds(TEXT_HEIGHT_SAMPLE, 0, TEXT_HEIGHT_SAMPLE.length(), textBounds);
    110         mTextHeight = textBounds.height();
    111 
    112         setTokenizer(new Rfc822Tokenizer());
    113         addTextChangedListener(new ContactChipsWatcher());
    114         setOnFocusListShrinkRecipients(false);
    115 
    116         setBackground(context.getResources().getDrawable(
    117                 R.drawable.abc_textfield_search_default_mtrl_alpha));
    118     }
    119 
    120     public void setContactChipsListener(final ContactChipsChangeListener listener) {
    121         mChipsChangeListener = listener;
    122     }
    123 
    124     /**
    125      * A tuple of chips which AsyncContactChipSanitizeTask reports as progress to have the
    126      * chip actually replaced/removed on the UI thread.
    127      */
    128     private class ChipReplacementTuple {
    129         public final DrawableRecipientChip removedChip;
    130         public final RecipientEntry replacedChipEntry;
    131 
    132         public ChipReplacementTuple(final DrawableRecipientChip removedChip,
    133                 final RecipientEntry replacedChipEntry) {
    134             this.removedChip = removedChip;
    135             this.replacedChipEntry = replacedChipEntry;
    136         }
    137     }
    138 
    139     /**
    140      * An AsyncTask that cleans up contact chips on every chips commit (i.e. get or create a new
    141      * conversation with the given chips).
    142      */
    143     private class AsyncContactChipSanitizeTask extends
    144             AsyncTask<Void, ChipReplacementTuple, Integer> {
    145 
    146         @Override
    147         protected Integer doInBackground(final Void... params) {
    148             final DrawableRecipientChip[] recips = getText()
    149                     .getSpans(0, getText().length(), DrawableRecipientChip.class);
    150             int invalidChipsRemoved = 0;
    151             for (final DrawableRecipientChip recipient : recips) {
    152                 final RecipientEntry entry = recipient.getEntry();
    153                 if (entry != null) {
    154                     if (entry.isValid()) {
    155                         if (RecipientEntry.isCreatedRecipient(entry.getContactId()) ||
    156                                 ContactRecipientEntryUtils.isSendToDestinationContact(entry)) {
    157                             // This is a generated/send-to contact chip, try to look it up and
    158                             // display a chip for the corresponding local contact.
    159                             final Cursor lookupResult = ContactUtil.lookupDestination(getContext(),
    160                                     entry.getDestination()).performSynchronousQuery();
    161                             if (lookupResult != null && lookupResult.moveToNext()) {
    162                                 // Found a match, remove the generated entry and replace with
    163                                 // a better local entry.
    164                                 publishProgress(new ChipReplacementTuple(recipient,
    165                                         ContactUtil.createRecipientEntryForPhoneQuery(
    166                                                 lookupResult, true)));
    167                             } else if (PhoneUtils.isValidSmsMmsDestination(
    168                                     entry.getDestination())){
    169                                 // No match was found, but we have a valid destination so let's at
    170                                 // least create an entry that shows an avatar.
    171                                 publishProgress(new ChipReplacementTuple(recipient,
    172                                         ContactRecipientEntryUtils.constructNumberWithAvatarEntry(
    173                                                 entry.getDestination())));
    174                             } else {
    175                                 // Not a valid contact. Remove and show an error.
    176                                 publishProgress(new ChipReplacementTuple(recipient, null));
    177                                 invalidChipsRemoved++;
    178                             }
    179                         }
    180                     } else {
    181                         publishProgress(new ChipReplacementTuple(recipient, null));
    182                         invalidChipsRemoved++;
    183                     }
    184                 }
    185             }
    186             return invalidChipsRemoved;
    187         }
    188 
    189         @Override
    190         protected void onProgressUpdate(final ChipReplacementTuple... values) {
    191             for (final ChipReplacementTuple tuple : values) {
    192                 if (tuple.removedChip != null) {
    193                     final Editable text = getText();
    194                     final int chipStart = text.getSpanStart(tuple.removedChip);
    195                     final int chipEnd = text.getSpanEnd(tuple.removedChip);
    196                     if (chipStart >= 0 && chipEnd >= 0) {
    197                         text.delete(chipStart, chipEnd);
    198                     }
    199 
    200                     if (tuple.replacedChipEntry != null) {
    201                         appendRecipientEntry(tuple.replacedChipEntry);
    202                     }
    203                 }
    204             }
    205         }
    206 
    207         @Override
    208         protected void onPostExecute(final Integer invalidChipsRemoved) {
    209             mCurrentSanitizeTask = null;
    210             if (invalidChipsRemoved > 0) {
    211                 mChipsChangeListener.onInvalidContactChipsPruned(invalidChipsRemoved);
    212             }
    213         }
    214     }
    215 
    216     /**
    217      * We don't use SafeAsyncTask but instead use a single threaded executor to ensure that
    218      * all sanitization tasks are serially executed so as not to interfere with each other.
    219      */
    220     private static final Executor SANITIZE_EXECUTOR = Executors.newSingleThreadExecutor();
    221 
    222     private AsyncContactChipSanitizeTask mCurrentSanitizeTask;
    223 
    224     /**
    225      * Whenever the caller wants to start a new conversation with the list of chips we have,
    226      * make sure we asynchronously:
    227      * 1. Remove invalid chips.
    228      * 2. Attempt to resolve unknown contacts to known local contacts.
    229      * 3. Convert still unknown chips to chips with generated avatar.
    230      *
    231      * Note that we don't need to perform this synchronously since we can
    232      * resolve any unknown contacts to local contacts when needed.
    233      */
    234     private void sanitizeContactChips() {
    235         if (mCurrentSanitizeTask != null && !mCurrentSanitizeTask.isCancelled()) {
    236             mCurrentSanitizeTask.cancel(false);
    237             mCurrentSanitizeTask = null;
    238         }
    239         mCurrentSanitizeTask = new AsyncContactChipSanitizeTask();
    240         mCurrentSanitizeTask.executeOnExecutor(SANITIZE_EXECUTOR);
    241     }
    242 
    243     /**
    244      * Returns a list of ParticipantData from the entered chips in order to create
    245      * new conversation.
    246      */
    247     public ArrayList<ParticipantData> getRecipientParticipantDataForConversationCreation() {
    248         final DrawableRecipientChip[] recips = getText()
    249                 .getSpans(0, getText().length(), DrawableRecipientChip.class);
    250         final ArrayList<ParticipantData> contacts =
    251                 new ArrayList<ParticipantData>(recips.length);
    252         for (final DrawableRecipientChip recipient : recips) {
    253             final RecipientEntry entry = recipient.getEntry();
    254             if (entry != null && entry.isValid() && entry.getDestination() != null &&
    255                     PhoneUtils.isValidSmsMmsDestination(entry.getDestination())) {
    256                 contacts.add(ParticipantData.getFromRecipientEntry(recipient.getEntry()));
    257             }
    258         }
    259         sanitizeContactChips();
    260         return contacts;
    261     }
    262 
    263     /**c
    264      * Gets a set of currently selected chips' emails/phone numbers. This will facilitate the
    265      * consumer with determining quickly whether a contact is currently selected.
    266      */
    267     public Set<String> getSelectedDestinations() {
    268         Set<String> set = new HashSet<String>();
    269         final DrawableRecipientChip[] recips = getText()
    270                 .getSpans(0, getText().length(), DrawableRecipientChip.class);
    271 
    272         for (final DrawableRecipientChip recipient : recips) {
    273             final RecipientEntry entry = recipient.getEntry();
    274             if (entry != null && entry.isValid() && entry.getDestination() != null) {
    275                 set.add(PhoneUtils.getDefault().getCanonicalBySystemLocale(
    276                         entry.getDestination()));
    277             }
    278         }
    279         return set;
    280     }
    281 
    282     @Override
    283     public boolean onEditorAction(final TextView view, final int actionId, final KeyEvent event) {
    284         if (actionId == EditorInfo.IME_ACTION_DONE) {
    285             mChipsChangeListener.onEntryComplete();
    286         }
    287         return super.onEditorAction(view, actionId, event);
    288     }
    289 }
    290