• 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 
97                 mAffected = null;
98             }
99         });
100     }
101 
102     @Override
enoughToFilter()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 
getRecipientCount()119     public int getRecipientCount() {
120         return mTokenizer.getNumbers().size();
121     }
122 
getNumbers()123     public List<String> getNumbers() {
124         return mTokenizer.getNumbers();
125     }
126 
constructContactsFromInput()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 
isValidAddress(String number, boolean isMms)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 
hasValidRecipient(boolean isMms)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 
hasInvalidRecipient(boolean isMms)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 
formatInvalidNumbers(boolean isMms)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 
containsEmail()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 
contactToToken(Contact c)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 
populate(ContactList list)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 
pointToPosition(int x, int y)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
onTouchEvent(MotionEvent ev)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
getContextMenuInfo()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, mContext);
267                     Contact c = Contact.get(number, true);
268                     return new RecipientContextMenuInfo(c);
269                 }
270             }
271         }
272         return null;
273     }
274 
getNumberAt(Spanned sp, int start, int end, Context context)275     private static String getNumberAt(Spanned sp, int start, int end, Context context) {
276         return getFieldAt("number", sp, start, end, context);
277     }
278 
getSpanLength(Spanned sp, int start, int end, Context context)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 
getFieldAt(String field, Spanned sp, int start, int end, Context context)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 
getAnnotation(Annotation[] a, String key)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 
RecipientsEditorTokenizer(Context context, MultiAutoCompleteTextView list)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          */
findTokenStart(CharSequence text, int cursor)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          */
findTokenEnd(CharSequence text, int cursor)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          */
terminateToken(CharSequence text)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 
getNumbers()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 
RecipientContextMenuInfo(Contact r)436         RecipientContextMenuInfo(Contact r) {
437             recipient = r;
438         }
439     }
440 }
441