Home | History | Annotate | Download | only in ui
      1 /*
      2  * Copyright (C) 2008 Esmertec AG.
      3  * Copyright (C) 2008 The Android Open Source Project
      4  *
      5  * Licensed under the Apache License, Version 2.0 (the "License");
      6  * you may not use this file except in compliance with the License.
      7  * You may obtain a copy of the License at
      8  *
      9  *      http://www.apache.org/licenses/LICENSE-2.0
     10  *
     11  * Unless required by applicable law or agreed to in writing, software
     12  * distributed under the License is distributed on an "AS IS" BASIS,
     13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     14  * See the License for the specific language governing permissions and
     15  * limitations under the License.
     16  */
     17 
     18 package com.android.mms.ui;
     19 
     20 import com.android.mms.MmsConfig;
     21 import com.android.mms.data.Contact;
     22 import com.android.mms.data.ContactList;
     23 
     24 import android.content.Context;
     25 import android.provider.Telephony.Mms;
     26 import android.telephony.PhoneNumberUtils;
     27 import android.text.Annotation;
     28 import android.text.Editable;
     29 import android.text.Layout;
     30 import android.text.Spannable;
     31 import android.text.SpannableString;
     32 import android.text.SpannableStringBuilder;
     33 import android.text.Spanned;
     34 import android.text.TextUtils;
     35 import android.text.TextWatcher;
     36 import android.util.AttributeSet;
     37 import android.view.inputmethod.EditorInfo;
     38 import android.view.MotionEvent;
     39 import android.view.ContextMenu.ContextMenuInfo;
     40 import android.widget.MultiAutoCompleteTextView;
     41 
     42 import java.util.ArrayList;
     43 import java.util.List;
     44 
     45 /**
     46  * Provide UI for editing the recipients of multi-media messages.
     47  */
     48 public class RecipientsEditor extends MultiAutoCompleteTextView {
     49     private int mLongPressedPosition = -1;
     50     private final RecipientsEditorTokenizer mTokenizer;
     51     private char mLastSeparator = ',';
     52 
     53     public RecipientsEditor(Context context, AttributeSet attrs) {
     54         super(context, attrs, android.R.attr.autoCompleteTextViewStyle);
     55         mTokenizer = new RecipientsEditorTokenizer(context, this);
     56         setTokenizer(mTokenizer);
     57         // For the focus to move to the message body when soft Next is pressed
     58         setImeOptions(EditorInfo.IME_ACTION_NEXT);
     59 
     60         /*
     61          * The point of this TextWatcher is that when the user chooses
     62          * an address completion from the AutoCompleteTextView menu, it
     63          * is marked up with Annotation objects to tie it back to the
     64          * address book entry that it came from.  If the user then goes
     65          * back and edits that part of the text, it no longer corresponds
     66          * to that address book entry and needs to have the Annotations
     67          * claiming that it does removed.
     68          */
     69         addTextChangedListener(new TextWatcher() {
     70             private Annotation[] mAffected;
     71 
     72             public void beforeTextChanged(CharSequence s, int start,
     73                     int count, int after) {
     74                 mAffected = ((Spanned) s).getSpans(start, start + count,
     75                         Annotation.class);
     76             }
     77 
     78             public void onTextChanged(CharSequence s, int start,
     79                     int before, int after) {
     80                 if (before == 0 && after == 1) {    // inserting a character
     81                     char c = s.charAt(start);
     82                     if (c == ',' || c == ';') {
     83                         // Remember the delimiter the user typed to end this recipient. We'll
     84                         // need it shortly in terminateToken().
     85                         mLastSeparator = c;
     86                     }
     87                 }
     88             }
     89 
     90             public void afterTextChanged(Editable s) {
     91                 if (mAffected != null) {
     92                     for (Annotation a : mAffected) {
     93                         s.removeSpan(a);
     94                     }
     95                 }
     96 
     97                 mAffected = null;
     98             }
     99         });
    100     }
    101 
    102     @Override
    103     public boolean enoughToFilter() {
    104         if (!super.enoughToFilter()) {
    105             return false;
    106         }
    107         // If the user is in the middle of editing an existing recipient, don't offer the
    108         // auto-complete menu. Without this, when the user selects an auto-complete menu item,
    109         // it will get added to the list of recipients so we end up with the old before-editing
    110         // recipient and the new post-editing recipient. As a precedent, gmail does not show
    111         // the auto-complete menu when editing an existing recipient.
    112         int end = getSelectionEnd();
    113         int len = getText().length();
    114 
    115         return end == len;
    116 
    117     }
    118 
    119     public int getRecipientCount() {
    120         return mTokenizer.getNumbers().size();
    121     }
    122 
    123     public List<String> getNumbers() {
    124         return mTokenizer.getNumbers();
    125     }
    126 
    127     public ContactList constructContactsFromInput() {
    128         List<String> numbers = mTokenizer.getNumbers();
    129         ContactList list = new ContactList();
    130         for (String number : numbers) {
    131             Contact contact = Contact.get(number, false);
    132             contact.setNumber(number);
    133             list.add(contact);
    134         }
    135         return list;
    136     }
    137 
    138     private boolean isValidAddress(String number, boolean isMms) {
    139         if (isMms) {
    140             return MessageUtils.isValidMmsAddress(number);
    141         } else {
    142             // TODO: PhoneNumberUtils.isWellFormedSmsAddress() only check if the number is a valid
    143             // GSM SMS address. If the address contains a dialable char, it considers it a well
    144             // formed SMS addr. CDMA doesn't work that way and has a different parser for SMS
    145             // address (see CdmaSmsAddress.parse(String address)). We should definitely fix this!!!
    146             return PhoneNumberUtils.isWellFormedSmsAddress(number)
    147                     || Mms.isEmailAddress(number);
    148         }
    149     }
    150 
    151     public boolean hasValidRecipient(boolean isMms) {
    152         for (String number : mTokenizer.getNumbers()) {
    153             if (isValidAddress(number, isMms))
    154                 return true;
    155         }
    156         return false;
    157     }
    158 
    159     public boolean hasInvalidRecipient(boolean isMms) {
    160         for (String number : mTokenizer.getNumbers()) {
    161             if (!isValidAddress(number, isMms)) {
    162                 if (MmsConfig.getEmailGateway() == null) {
    163                     return true;
    164                 } else if (!MessageUtils.isAlias(number)) {
    165                     return true;
    166                 }
    167             }
    168         }
    169         return false;
    170     }
    171 
    172     public String formatInvalidNumbers(boolean isMms) {
    173         StringBuilder sb = new StringBuilder();
    174         for (String number : mTokenizer.getNumbers()) {
    175             if (!isValidAddress(number, isMms)) {
    176                 if (sb.length() != 0) {
    177                     sb.append(", ");
    178                 }
    179                 sb.append(number);
    180             }
    181         }
    182         return sb.toString();
    183     }
    184 
    185     public boolean containsEmail() {
    186         if (TextUtils.indexOf(getText(), '@') == -1)
    187             return false;
    188 
    189         List<String> numbers = mTokenizer.getNumbers();
    190         for (String number : numbers) {
    191             if (Mms.isEmailAddress(number))
    192                 return true;
    193         }
    194         return false;
    195     }
    196 
    197     public static CharSequence contactToToken(Contact c) {
    198         SpannableString s = new SpannableString(c.getNameAndNumber());
    199         int len = s.length();
    200 
    201         if (len == 0) {
    202             return s;
    203         }
    204 
    205         s.setSpan(new Annotation("number", c.getNumber()), 0, len,
    206                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    207 
    208         return s;
    209     }
    210 
    211     public void populate(ContactList list) {
    212         SpannableStringBuilder sb = new SpannableStringBuilder();
    213 
    214         for (Contact c : list) {
    215             if (sb.length() != 0) {
    216                 sb.append(", ");
    217             }
    218 
    219             sb.append(contactToToken(c));
    220         }
    221 
    222         setText(sb);
    223     }
    224 
    225     private int pointToPosition(int x, int y) {
    226         x -= getCompoundPaddingLeft();
    227         y -= getExtendedPaddingTop();
    228 
    229 
    230         x += getScrollX();
    231         y += getScrollY();
    232 
    233         Layout layout = getLayout();
    234         if (layout == null) {
    235             return -1;
    236         }
    237 
    238         int line = layout.getLineForVertical(y);
    239         int off = layout.getOffsetForHorizontal(line, x);
    240 
    241         return off;
    242     }
    243 
    244     @Override
    245     public boolean onTouchEvent(MotionEvent ev) {
    246         final int action = ev.getAction();
    247         final int x = (int) ev.getX();
    248         final int y = (int) ev.getY();
    249 
    250         if (action == MotionEvent.ACTION_DOWN) {
    251             mLongPressedPosition = pointToPosition(x, y);
    252         }
    253 
    254         return super.onTouchEvent(ev);
    255     }
    256 
    257     @Override
    258     protected ContextMenuInfo getContextMenuInfo() {
    259         if ((mLongPressedPosition >= 0)) {
    260             Spanned text = getText();
    261             if (mLongPressedPosition <= text.length()) {
    262                 int start = mTokenizer.findTokenStart(text, mLongPressedPosition);
    263                 int end = mTokenizer.findTokenEnd(text, start);
    264 
    265                 if (end != start) {
    266                     String number = getNumberAt(getText(), start, end, getContext());
    267                     Contact c = Contact.get(number, false);
    268                     return new RecipientContextMenuInfo(c);
    269                 }
    270             }
    271         }
    272         return null;
    273     }
    274 
    275     private static String getNumberAt(Spanned sp, int start, int end, Context context) {
    276         return getFieldAt("number", sp, start, end, context);
    277     }
    278 
    279     private static int getSpanLength(Spanned sp, int start, int end, Context context) {
    280         // TODO: there's a situation where the span can lose its annotations:
    281         //   - add an auto-complete contact
    282         //   - add another auto-complete contact
    283         //   - delete that second contact and keep deleting into the first
    284         //   - we lose the annotation and can no longer get the span.
    285         // Need to fix this case because it breaks auto-complete contacts with commas in the name.
    286         Annotation[] a = sp.getSpans(start, end, Annotation.class);
    287         if (a.length > 0) {
    288             return sp.getSpanEnd(a[0]);
    289         }
    290         return 0;
    291     }
    292 
    293     private static String getFieldAt(String field, Spanned sp, int start, int end,
    294             Context context) {
    295         Annotation[] a = sp.getSpans(start, end, Annotation.class);
    296         String fieldValue = getAnnotation(a, field);
    297         if (TextUtils.isEmpty(fieldValue)) {
    298             fieldValue = TextUtils.substring(sp, start, end);
    299         }
    300         return fieldValue;
    301 
    302     }
    303 
    304     private static String getAnnotation(Annotation[] a, String key) {
    305         for (int i = 0; i < a.length; i++) {
    306             if (a[i].getKey().equals(key)) {
    307                 return a[i].getValue();
    308             }
    309         }
    310 
    311         return "";
    312     }
    313 
    314     private class RecipientsEditorTokenizer
    315             implements MultiAutoCompleteTextView.Tokenizer {
    316         private final MultiAutoCompleteTextView mList;
    317         private final Context mContext;
    318 
    319         RecipientsEditorTokenizer(Context context, MultiAutoCompleteTextView list) {
    320             mList = list;
    321             mContext = context;
    322         }
    323 
    324         /**
    325          * Returns the start of the token that ends at offset
    326          * <code>cursor</code> within <code>text</code>.
    327          * It is a method from the MultiAutoCompleteTextView.Tokenizer interface.
    328          */
    329         public int findTokenStart(CharSequence text, int cursor) {
    330             int i = cursor;
    331             char c;
    332 
    333             while (i > 0 && (c = text.charAt(i - 1)) != ',' && c != ';') {
    334                 i--;
    335             }
    336             while (i < cursor && text.charAt(i) == ' ') {
    337                 i++;
    338             }
    339 
    340             return i;
    341         }
    342 
    343         /**
    344          * Returns the end of the token (minus trailing punctuation)
    345          * that begins at offset <code>cursor</code> within <code>text</code>.
    346          * It is a method from the MultiAutoCompleteTextView.Tokenizer interface.
    347          */
    348         public int findTokenEnd(CharSequence text, int cursor) {
    349             int i = cursor;
    350             int len = text.length();
    351             char c;
    352 
    353             while (i < len) {
    354                 if ((c = text.charAt(i)) == ',' || c == ';') {
    355                     return i;
    356                 } else {
    357                     i++;
    358                 }
    359             }
    360 
    361             return len;
    362         }
    363 
    364         /**
    365          * Returns <code>text</code>, modified, if necessary, to ensure that
    366          * it ends with a token terminator (for example a space or comma).
    367          * It is a method from the MultiAutoCompleteTextView.Tokenizer interface.
    368          */
    369         public CharSequence terminateToken(CharSequence text) {
    370             int i = text.length();
    371 
    372             while (i > 0 && text.charAt(i - 1) == ' ') {
    373                 i--;
    374             }
    375 
    376             char c;
    377             if (i > 0 && ((c = text.charAt(i - 1)) == ',' || c == ';')) {
    378                 return text;
    379             } else {
    380                 // Use the same delimiter the user just typed.
    381                 // This lets them have a mixture of commas and semicolons in their list.
    382                 String separator = mLastSeparator + " ";
    383                 if (text instanceof Spanned) {
    384                     SpannableString sp = new SpannableString(text + separator);
    385                     TextUtils.copySpansFrom((Spanned) text, 0, text.length(),
    386                                             Object.class, sp, 0);
    387                     return sp;
    388                 } else {
    389                     return text + separator;
    390                 }
    391             }
    392         }
    393 
    394         public List<String> getNumbers() {
    395             Spanned sp = mList.getText();
    396             int len = sp.length();
    397             List<String> list = new ArrayList<String>();
    398 
    399             int start = 0;
    400             int i = 0;
    401             while (i < len + 1) {
    402                 char c;
    403                 if ((i == len) || ((c = sp.charAt(i)) == ',') || (c == ';')) {
    404                     if (i > start) {
    405                         list.add(getNumberAt(sp, start, i, mContext));
    406 
    407                         // calculate the recipients total length. This is so if the name contains
    408                         // commas or semis, we'll skip over the whole name to the next
    409                         // recipient, rather than parsing this single name into multiple
    410                         // recipients.
    411                         int spanLen = getSpanLength(sp, start, i, mContext);
    412                         if (spanLen > i) {
    413                             i = spanLen;
    414                         }
    415                     }
    416 
    417                     i++;
    418 
    419                     while ((i < len) && (sp.charAt(i) == ' ')) {
    420                         i++;
    421                     }
    422 
    423                     start = i;
    424                 } else {
    425                     i++;
    426                 }
    427             }
    428 
    429             return list;
    430         }
    431     }
    432 
    433     static class RecipientContextMenuInfo implements ContextMenuInfo {
    434         final Contact recipient;
    435 
    436         RecipientContextMenuInfo(Contact r) {
    437             recipient = r;
    438         }
    439     }
    440 }
    441