• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
RecipientsEditor(Context context, AttributeSet attrs)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                 mAffected = null;
97             }
98         });
99     }
100 
101     @Override
enoughToFilter()102     public boolean enoughToFilter() {
103         if (!super.enoughToFilter()) {
104             return false;
105         }
106         // If the user is in the middle of editing an existing recipient, don't offer the
107         // auto-complete menu. Without this, when the user selects an auto-complete menu item,
108         // it will get added to the list of recipients so we end up with the old before-editing
109         // recipient and the new post-editing recipient. As a precedent, gmail does not show
110         // the auto-complete menu when editing an existing recipient.
111         int end = getSelectionEnd();
112         int len = getText().length();
113 
114         return end == len;
115 
116     }
117 
getRecipientCount()118     public int getRecipientCount() {
119         return mTokenizer.getNumbers().size();
120     }
121 
getNumbers()122     public List<String> getNumbers() {
123         return mTokenizer.getNumbers();
124     }
125 
constructContactsFromInput(boolean blocking)126     public ContactList constructContactsFromInput(boolean blocking) {
127         List<String> numbers = mTokenizer.getNumbers();
128         ContactList list = new ContactList();
129         for (String number : numbers) {
130             Contact contact = Contact.get(number, blocking);
131             contact.setNumber(number);
132             list.add(contact);
133         }
134         return list;
135     }
136 
isValidAddress(String number, boolean isMms)137     private boolean isValidAddress(String number, boolean isMms) {
138         if (isMms) {
139             return MessageUtils.isValidMmsAddress(number);
140         } else {
141             // TODO: PhoneNumberUtils.isWellFormedSmsAddress() only check if the number is a valid
142             // GSM SMS address. If the address contains a dialable char, it considers it a well
143             // formed SMS addr. CDMA doesn't work that way and has a different parser for SMS
144             // address (see CdmaSmsAddress.parse(String address)). We should definitely fix this!!!
145             return PhoneNumberUtils.isWellFormedSmsAddress(number)
146                     || Mms.isEmailAddress(number);
147         }
148     }
149 
hasValidRecipient(boolean isMms)150     public boolean hasValidRecipient(boolean isMms) {
151         for (String number : mTokenizer.getNumbers()) {
152             if (isValidAddress(number, isMms))
153                 return true;
154         }
155         return false;
156     }
157 
hasInvalidRecipient(boolean isMms)158     public boolean hasInvalidRecipient(boolean isMms) {
159         for (String number : mTokenizer.getNumbers()) {
160             if (!isValidAddress(number, isMms)) {
161                 if (MmsConfig.getEmailGateway() == null) {
162                     return true;
163                 } else if (!MessageUtils.isAlias(number)) {
164                     return true;
165                 }
166             }
167         }
168         return false;
169     }
170 
formatInvalidNumbers(boolean isMms)171     public String formatInvalidNumbers(boolean isMms) {
172         StringBuilder sb = new StringBuilder();
173         for (String number : mTokenizer.getNumbers()) {
174             if (!isValidAddress(number, isMms)) {
175                 if (sb.length() != 0) {
176                     sb.append(", ");
177                 }
178                 sb.append(number);
179             }
180         }
181         return sb.toString();
182     }
183 
containsEmail()184     public boolean containsEmail() {
185         if (TextUtils.indexOf(getText(), '@') == -1)
186             return false;
187 
188         List<String> numbers = mTokenizer.getNumbers();
189         for (String number : numbers) {
190             if (Mms.isEmailAddress(number))
191                 return true;
192         }
193         return false;
194     }
195 
contactToToken(Contact c)196     public static CharSequence contactToToken(Contact c) {
197         SpannableString s = new SpannableString(c.getNameAndNumber());
198         int len = s.length();
199 
200         if (len == 0) {
201             return s;
202         }
203 
204         s.setSpan(new Annotation("number", c.getNumber()), 0, len,
205                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
206 
207         return s;
208     }
209 
populate(ContactList list)210     public void populate(ContactList list) {
211         SpannableStringBuilder sb = new SpannableStringBuilder();
212 
213         // Very tricky bug. In the recipient editor, we always leave a trailing
214         // comma to make it easy for users to add additional recipients. When a
215         // user types (or chooses from the dropdown) a new contact Mms has never
216         // seen before, the contact gets the correct trailing comma. But when the
217         // contact gets added to the mms's contacts table, contacts sends out an
218         // onUpdate to CMA. CMA would recompute the recipients and since the
219         // recipient editor was still visible, call mRecipientsEditor.populate(recipients).
220         // This would replace the recipient that had a comma with a recipient
221         // without a comma. When a user manually added a new comma to add another
222         // recipient, this would eliminate the span inside the text. The span contains the
223         // number part of "Fred Flinstone <123-1231>". Hence, the whole
224         // "Fred Flinstone <123-1231>" would be considered the number of
225         // the first recipient and get entered into the canonical_addresses table.
226         // The fix for this particular problem is very easy. All recipients have commas.
227         // TODO: However, the root problem remains. If a user enters the recipients editor
228         // and deletes chars into an address chosen from the suggestions, it'll cause
229         // the number annotation to get deleted and the whole address (name + number) will
230         // be used as the number.
231         for (Contact c : list) {
232             sb.append(contactToToken(c)).append(", ");
233         }
234 
235         setText(sb);
236     }
237 
pointToPosition(int x, int y)238     private int pointToPosition(int x, int y) {
239         x -= getCompoundPaddingLeft();
240         y -= getExtendedPaddingTop();
241 
242 
243         x += getScrollX();
244         y += getScrollY();
245 
246         Layout layout = getLayout();
247         if (layout == null) {
248             return -1;
249         }
250 
251         int line = layout.getLineForVertical(y);
252         int off = layout.getOffsetForHorizontal(line, x);
253 
254         return off;
255     }
256 
257     @Override
onTouchEvent(MotionEvent ev)258     public boolean onTouchEvent(MotionEvent ev) {
259         final int action = ev.getAction();
260         final int x = (int) ev.getX();
261         final int y = (int) ev.getY();
262 
263         if (action == MotionEvent.ACTION_DOWN) {
264             mLongPressedPosition = pointToPosition(x, y);
265         }
266 
267         return super.onTouchEvent(ev);
268     }
269 
270     @Override
getContextMenuInfo()271     protected ContextMenuInfo getContextMenuInfo() {
272         if ((mLongPressedPosition >= 0)) {
273             Spanned text = getText();
274             if (mLongPressedPosition <= text.length()) {
275                 int start = mTokenizer.findTokenStart(text, mLongPressedPosition);
276                 int end = mTokenizer.findTokenEnd(text, start);
277 
278                 if (end != start) {
279                     String number = getNumberAt(getText(), start, end, getContext());
280                     Contact c = Contact.get(number, false);
281                     return new RecipientContextMenuInfo(c);
282                 }
283             }
284         }
285         return null;
286     }
287 
getNumberAt(Spanned sp, int start, int end, Context context)288     private static String getNumberAt(Spanned sp, int start, int end, Context context) {
289         return getFieldAt("number", sp, start, end, context);
290     }
291 
getSpanLength(Spanned sp, int start, int end, Context context)292     private static int getSpanLength(Spanned sp, int start, int end, Context context) {
293         // TODO: there's a situation where the span can lose its annotations:
294         //   - add an auto-complete contact
295         //   - add another auto-complete contact
296         //   - delete that second contact and keep deleting into the first
297         //   - we lose the annotation and can no longer get the span.
298         // Need to fix this case because it breaks auto-complete contacts with commas in the name.
299         Annotation[] a = sp.getSpans(start, end, Annotation.class);
300         if (a.length > 0) {
301             return sp.getSpanEnd(a[0]);
302         }
303         return 0;
304     }
305 
getFieldAt(String field, Spanned sp, int start, int end, Context context)306     private static String getFieldAt(String field, Spanned sp, int start, int end,
307             Context context) {
308         Annotation[] a = sp.getSpans(start, end, Annotation.class);
309         String fieldValue = getAnnotation(a, field);
310         if (TextUtils.isEmpty(fieldValue)) {
311             fieldValue = TextUtils.substring(sp, start, end);
312         }
313         return fieldValue;
314 
315     }
316 
getAnnotation(Annotation[] a, String key)317     private static String getAnnotation(Annotation[] a, String key) {
318         for (int i = 0; i < a.length; i++) {
319             if (a[i].getKey().equals(key)) {
320                 return a[i].getValue();
321             }
322         }
323 
324         return "";
325     }
326 
327     private class RecipientsEditorTokenizer
328             implements MultiAutoCompleteTextView.Tokenizer {
329         private final MultiAutoCompleteTextView mList;
330         private final Context mContext;
331 
RecipientsEditorTokenizer(Context context, MultiAutoCompleteTextView list)332         RecipientsEditorTokenizer(Context context, MultiAutoCompleteTextView list) {
333             mList = list;
334             mContext = context;
335         }
336 
337         /**
338          * Returns the start of the token that ends at offset
339          * <code>cursor</code> within <code>text</code>.
340          * It is a method from the MultiAutoCompleteTextView.Tokenizer interface.
341          */
findTokenStart(CharSequence text, int cursor)342         public int findTokenStart(CharSequence text, int cursor) {
343             int i = cursor;
344             char c;
345 
346             while (i > 0 && (c = text.charAt(i - 1)) != ',' && c != ';') {
347                 i--;
348             }
349             while (i < cursor && text.charAt(i) == ' ') {
350                 i++;
351             }
352 
353             return i;
354         }
355 
356         /**
357          * Returns the end of the token (minus trailing punctuation)
358          * that begins at offset <code>cursor</code> within <code>text</code>.
359          * It is a method from the MultiAutoCompleteTextView.Tokenizer interface.
360          */
findTokenEnd(CharSequence text, int cursor)361         public int findTokenEnd(CharSequence text, int cursor) {
362             int i = cursor;
363             int len = text.length();
364             char c;
365 
366             while (i < len) {
367                 if ((c = text.charAt(i)) == ',' || c == ';') {
368                     return i;
369                 } else {
370                     i++;
371                 }
372             }
373 
374             return len;
375         }
376 
377         /**
378          * Returns <code>text</code>, modified, if necessary, to ensure that
379          * it ends with a token terminator (for example a space or comma).
380          * It is a method from the MultiAutoCompleteTextView.Tokenizer interface.
381          */
terminateToken(CharSequence text)382         public CharSequence terminateToken(CharSequence text) {
383             int i = text.length();
384 
385             while (i > 0 && text.charAt(i - 1) == ' ') {
386                 i--;
387             }
388 
389             char c;
390             if (i > 0 && ((c = text.charAt(i - 1)) == ',' || c == ';')) {
391                 return text;
392             } else {
393                 // Use the same delimiter the user just typed.
394                 // This lets them have a mixture of commas and semicolons in their list.
395                 String separator = mLastSeparator + " ";
396                 if (text instanceof Spanned) {
397                     SpannableString sp = new SpannableString(text + separator);
398                     TextUtils.copySpansFrom((Spanned) text, 0, text.length(),
399                                             Object.class, sp, 0);
400                     return sp;
401                 } else {
402                     return text + separator;
403                 }
404             }
405         }
406 
getNumbers()407         public List<String> getNumbers() {
408             Spanned sp = mList.getText();
409             int len = sp.length();
410             List<String> list = new ArrayList<String>();
411 
412             int start = 0;
413             int i = 0;
414             while (i < len + 1) {
415                 char c;
416                 if ((i == len) || ((c = sp.charAt(i)) == ',') || (c == ';')) {
417                     if (i > start) {
418                         list.add(getNumberAt(sp, start, i, mContext));
419 
420                         // calculate the recipients total length. This is so if the name contains
421                         // commas or semis, we'll skip over the whole name to the next
422                         // recipient, rather than parsing this single name into multiple
423                         // recipients.
424                         int spanLen = getSpanLength(sp, start, i, mContext);
425                         if (spanLen > i) {
426                             i = spanLen;
427                         }
428                     }
429 
430                     i++;
431 
432                     while ((i < len) && (sp.charAt(i) == ' ')) {
433                         i++;
434                     }
435 
436                     start = i;
437                 } else {
438                     i++;
439                 }
440             }
441 
442             return list;
443         }
444     }
445 
446     static class RecipientContextMenuInfo implements ContextMenuInfo {
447         final Contact recipient;
448 
RecipientContextMenuInfo(Contact r)449         RecipientContextMenuInfo(Contact r) {
450             recipient = r;
451         }
452     }
453 }
454