• 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 static android.content.res.Configuration.KEYBOARDHIDDEN_NO;
21 import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_ABORT;
22 import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_COMPLETE;
23 import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_START;
24 import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_STATUS_ACTION;
25 import static com.android.mms.ui.MessageListAdapter.COLUMN_ID;
26 import static com.android.mms.ui.MessageListAdapter.COLUMN_MMS_LOCKED;
27 import static com.android.mms.ui.MessageListAdapter.COLUMN_MSG_TYPE;
28 import static com.android.mms.ui.MessageListAdapter.PROJECTION;
29 
30 import java.io.File;
31 import java.io.FileInputStream;
32 import java.io.FileOutputStream;
33 import java.io.IOException;
34 import java.io.InputStream;
35 import java.io.UnsupportedEncodingException;
36 import java.net.URLDecoder;
37 import java.util.ArrayList;
38 import java.util.HashMap;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.regex.Pattern;
43 
44 import android.app.ActionBar;
45 import android.app.Activity;
46 import android.app.AlertDialog;
47 import android.app.ProgressDialog;
48 import android.content.ActivityNotFoundException;
49 import android.content.AsyncQueryHandler;
50 import android.content.BroadcastReceiver;
51 import android.content.ContentResolver;
52 import android.content.ContentUris;
53 import android.content.ContentValues;
54 import android.content.Context;
55 import android.content.DialogInterface;
56 import android.content.Intent;
57 import android.content.IntentFilter;
58 import android.content.DialogInterface.OnClickListener;
59 import android.content.res.Configuration;
60 import android.content.res.Resources;
61 import android.database.Cursor;
62 import android.database.sqlite.SQLiteException;
63 import android.database.sqlite.SqliteWrapper;
64 import android.drm.mobile1.DrmException;
65 import android.drm.mobile1.DrmRawContent;
66 import android.graphics.drawable.Drawable;
67 import android.media.RingtoneManager;
68 import android.net.Uri;
69 import android.os.Bundle;
70 import android.os.Environment;
71 import android.os.Handler;
72 import android.os.Message;
73 import android.os.Parcelable;
74 import android.os.SystemProperties;
75 import android.provider.ContactsContract;
76 import android.provider.ContactsContract.CommonDataKinds.Email;
77 import android.provider.ContactsContract.Contacts;
78 import android.provider.DrmStore;
79 import android.provider.MediaStore;
80 import android.provider.Settings;
81 import android.provider.ContactsContract.Intents;
82 import android.provider.MediaStore.Images;
83 import android.provider.MediaStore.Video;
84 import android.provider.Telephony.Mms;
85 import android.provider.Telephony.Sms;
86 import android.provider.ContactsContract.CommonDataKinds.Phone;
87 import android.telephony.PhoneNumberUtils;
88 import android.telephony.SmsMessage;
89 import android.text.ClipboardManager;
90 import android.text.Editable;
91 import android.text.InputFilter;
92 import android.text.SpannableString;
93 import android.text.Spanned;
94 import android.text.TextUtils;
95 import android.text.TextWatcher;
96 import android.text.method.TextKeyListener;
97 import android.text.style.URLSpan;
98 import android.text.util.Linkify;
99 import android.util.Log;
100 import android.view.ContextMenu;
101 import android.view.KeyEvent;
102 import android.view.LayoutInflater;
103 import android.view.Menu;
104 import android.view.MenuItem;
105 import android.view.View;
106 import android.view.ViewStub;
107 import android.view.WindowManager;
108 import android.view.ContextMenu.ContextMenuInfo;
109 import android.view.View.OnCreateContextMenuListener;
110 import android.view.View.OnKeyListener;
111 import android.view.inputmethod.InputMethodManager;
112 import android.webkit.MimeTypeMap;
113 import android.widget.AdapterView;
114 import android.widget.EditText;
115 import android.widget.ImageButton;
116 import android.widget.ImageView;
117 import android.widget.LinearLayout;
118 import android.widget.ListView;
119 import android.widget.SimpleAdapter;
120 import android.widget.CursorAdapter;
121 import android.widget.TextView;
122 import android.widget.Toast;
123 
124 import com.android.internal.telephony.TelephonyIntents;
125 import com.android.internal.telephony.TelephonyProperties;
126 import com.android.mms.LogTag;
127 import com.android.mms.MmsApp;
128 import com.android.mms.MmsConfig;
129 import com.android.mms.R;
130 import com.android.mms.TempFileProvider;
131 import com.android.mms.data.Contact;
132 import com.android.mms.data.ContactList;
133 import com.android.mms.data.Conversation;
134 import com.android.mms.data.WorkingMessage;
135 import com.android.mms.data.WorkingMessage.MessageStatusListener;
136 import com.google.android.mms.ContentType;
137 import com.google.android.mms.pdu.EncodedStringValue;
138 import com.google.android.mms.MmsException;
139 import com.google.android.mms.pdu.PduBody;
140 import com.google.android.mms.pdu.PduPart;
141 import com.google.android.mms.pdu.PduPersister;
142 import com.google.android.mms.pdu.SendReq;
143 import com.android.mms.model.SlideModel;
144 import com.android.mms.model.SlideshowModel;
145 import com.android.mms.transaction.MessagingNotification;
146 import com.android.mms.ui.MessageUtils.ResizeImageResultCallback;
147 import com.android.mms.ui.RecipientsEditor.RecipientContextMenuInfo;
148 import com.android.mms.util.SendingProgressTokenManager;
149 import com.android.mms.util.SmileyParser;
150 
151 import android.text.InputFilter.LengthFilter;
152 
153 /**
154  * This is the main UI for:
155  * 1. Composing a new message;
156  * 2. Viewing/managing message history of a conversation.
157  *
158  * This activity can handle following parameters from the intent
159  * by which it's launched.
160  * thread_id long Identify the conversation to be viewed. When creating a
161  *         new message, this parameter shouldn't be present.
162  * msg_uri Uri The message which should be opened for editing in the editor.
163  * address String The addresses of the recipients in current conversation.
164  * exit_on_sent boolean Exit this activity after the message is sent.
165  */
166 public class ComposeMessageActivity extends Activity
167         implements View.OnClickListener, TextView.OnEditorActionListener,
168         MessageStatusListener, Contact.UpdateListener {
169     public static final int REQUEST_CODE_ATTACH_IMAGE     = 100;
170     public static final int REQUEST_CODE_TAKE_PICTURE     = 101;
171     public static final int REQUEST_CODE_ATTACH_VIDEO     = 102;
172     public static final int REQUEST_CODE_TAKE_VIDEO       = 103;
173     public static final int REQUEST_CODE_ATTACH_SOUND     = 104;
174     public static final int REQUEST_CODE_RECORD_SOUND     = 105;
175     public static final int REQUEST_CODE_CREATE_SLIDESHOW = 106;
176     public static final int REQUEST_CODE_ECM_EXIT_DIALOG  = 107;
177     public static final int REQUEST_CODE_ADD_CONTACT      = 108;
178     public static final int REQUEST_CODE_PICK             = 109;
179 
180     private static final String TAG = "Mms/compose";
181 
182     private static final boolean DEBUG = false;
183     private static final boolean TRACE = false;
184     private static final boolean LOCAL_LOGV = false;
185 
186     // Menu ID
187     private static final int MENU_ADD_SUBJECT           = 0;
188     private static final int MENU_DELETE_THREAD         = 1;
189     private static final int MENU_ADD_ATTACHMENT        = 2;
190     private static final int MENU_DISCARD               = 3;
191     private static final int MENU_SEND                  = 4;
192     private static final int MENU_CALL_RECIPIENT        = 5;
193     private static final int MENU_CONVERSATION_LIST     = 6;
194     private static final int MENU_DEBUG_DUMP            = 7;
195 
196     // Context menu ID
197     private static final int MENU_VIEW_CONTACT          = 12;
198     private static final int MENU_ADD_TO_CONTACTS       = 13;
199 
200     private static final int MENU_EDIT_MESSAGE          = 14;
201     private static final int MENU_VIEW_SLIDESHOW        = 16;
202     private static final int MENU_VIEW_MESSAGE_DETAILS  = 17;
203     private static final int MENU_DELETE_MESSAGE        = 18;
204     private static final int MENU_SEARCH                = 19;
205     private static final int MENU_DELIVERY_REPORT       = 20;
206     private static final int MENU_FORWARD_MESSAGE       = 21;
207     private static final int MENU_CALL_BACK             = 22;
208     private static final int MENU_SEND_EMAIL            = 23;
209     private static final int MENU_COPY_MESSAGE_TEXT     = 24;
210     private static final int MENU_COPY_TO_SDCARD        = 25;
211     private static final int MENU_INSERT_SMILEY         = 26;
212     private static final int MENU_ADD_ADDRESS_TO_CONTACTS = 27;
213     private static final int MENU_LOCK_MESSAGE          = 28;
214     private static final int MENU_UNLOCK_MESSAGE        = 29;
215     private static final int MENU_COPY_TO_DRM_PROVIDER  = 30;
216     private static final int MENU_PREFERENCES           = 31;
217 
218     private static final int RECIPIENTS_MAX_LENGTH = 312;
219 
220     private static final int MESSAGE_LIST_QUERY_TOKEN = 9527;
221 
222     private static final int DELETE_MESSAGE_TOKEN  = 9700;
223 
224     private static final int CHARS_REMAINING_BEFORE_COUNTER_SHOWN = 10;
225 
226     private static final long NO_DATE_FOR_DIALOG = -1L;
227 
228     private static final String EXIT_ECM_RESULT = "exit_ecm_result";
229 
230     private ContentResolver mContentResolver;
231 
232     private BackgroundQueryHandler mBackgroundQueryHandler;
233 
234     private Conversation mConversation;     // Conversation we are working in
235 
236     private boolean mExitOnSent;            // Should we finish() after sending a message?
237                                             // TODO: mExitOnSent is obsolete -- remove
238 
239     private View mTopPanel;                 // View containing the recipient and subject editors
240     private View mBottomPanel;              // View containing the text editor, send button, ec.
241     private EditText mTextEditor;           // Text editor to type your message into
242     private TextView mTextCounter;          // Shows the number of characters used in text editor
243     private TextView mSendButtonMms;        // Press to send mms
244     private ImageButton mSendButtonSms;     // Press to send sms
245     private EditText mSubjectTextEditor;    // Text editor for MMS subject
246 
247     private AttachmentEditor mAttachmentEditor;
248     private View mAttachmentEditorScrollView;
249 
250     private MessageListView mMsgListView;        // ListView for messages in this conversation
251     public MessageListAdapter mMsgListAdapter;  // and its corresponding ListAdapter
252 
253     private RecipientsEditor mRecipientsEditor;  // UI control for editing recipients
254     private ImageButton mRecipientsPicker;       // UI control for recipients picker
255 
256     private boolean mIsKeyboardOpen;             // Whether the hardware keyboard is visible
257     private boolean mIsLandscape;                // Whether we're in landscape mode
258 
259     private boolean mPossiblePendingNotification;   // If the message list has changed, we may have
260                                                     // a pending notification to deal with.
261 
262     private boolean mToastForDraftSave;   // Whether to notify the user that a draft is being saved
263 
264     private boolean mSentMessage;       // true if the user has sent a message while in this
265                                         // activity. On a new compose message case, when the first
266                                         // message is sent is a MMS w/ attachment, the list blanks
267                                         // for a second before showing the sent message. But we'd
268                                         // think the message list is empty, thus show the recipients
269                                         // editor thinking it's a draft message. This flag should
270                                         // help clarify the situation.
271 
272     private WorkingMessage mWorkingMessage;         // The message currently being composed.
273 
274     private AlertDialog mSmileyDialog;
275 
276     private boolean mWaitingForSubActivity;
277     private int mLastRecipientCount;            // Used for warning the user on too many recipients.
278     private AttachmentTypeSelectorAdapter mAttachmentTypeSelectorAdapter;
279 
280     private boolean mSendingMessage;    // Indicates the current message is sending, and shouldn't send again.
281 
282     private Intent mAddContactIntent;   // Intent used to add a new contact
283 
284     private String mDebugRecipients;
285 
286     @SuppressWarnings("unused")
log(String logMsg)287     public static void log(String logMsg) {
288         Thread current = Thread.currentThread();
289         long tid = current.getId();
290         StackTraceElement[] stack = current.getStackTrace();
291         String methodName = stack[3].getMethodName();
292         // Prepend current thread ID and name of calling method to the message.
293         logMsg = "[" + tid + "] [" + methodName + "] " + logMsg;
294         Log.d(TAG, logMsg);
295     }
296 
297     //==========================================================
298     // Inner classes
299     //==========================================================
300 
editSlideshow()301     private void editSlideshow() {
302         Uri dataUri = mWorkingMessage.saveAsMms(false);
303         if (dataUri == null) {
304             return;
305         }
306         Intent intent = new Intent(this, SlideshowEditActivity.class);
307         intent.setData(dataUri);
308         startActivityForResult(intent, REQUEST_CODE_CREATE_SLIDESHOW);
309     }
310 
311     private final Handler mAttachmentEditorHandler = new Handler() {
312         @Override
313         public void handleMessage(Message msg) {
314             switch (msg.what) {
315                 case AttachmentEditor.MSG_EDIT_SLIDESHOW: {
316                     editSlideshow();
317                     break;
318                 }
319                 case AttachmentEditor.MSG_SEND_SLIDESHOW: {
320                     if (isPreparedForSending()) {
321                         ComposeMessageActivity.this.confirmSendMessageIfNeeded();
322                     }
323                     break;
324                 }
325                 case AttachmentEditor.MSG_VIEW_IMAGE:
326                 case AttachmentEditor.MSG_PLAY_VIDEO:
327                 case AttachmentEditor.MSG_PLAY_AUDIO:
328                 case AttachmentEditor.MSG_PLAY_SLIDESHOW:
329                     MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this,
330                             mWorkingMessage, msg.what);
331                     break;
332 
333                 case AttachmentEditor.MSG_REPLACE_IMAGE:
334                 case AttachmentEditor.MSG_REPLACE_VIDEO:
335                 case AttachmentEditor.MSG_REPLACE_AUDIO:
336                     showAddAttachmentDialog(true);
337                     break;
338 
339                 case AttachmentEditor.MSG_REMOVE_ATTACHMENT:
340                     mWorkingMessage.removeAttachment(true);
341                     break;
342 
343                 default:
344                     break;
345             }
346         }
347     };
348 
349     private final Handler mMessageListItemHandler = new Handler() {
350         @Override
351         public void handleMessage(Message msg) {
352             String type;
353             switch (msg.what) {
354                 case MessageListItem.MSG_LIST_EDIT_MMS:
355                     type = "mms";
356                     break;
357                 case MessageListItem.MSG_LIST_EDIT_SMS:
358                     type = "sms";
359                     break;
360                 default:
361                     Log.w(TAG, "Unknown message: " + msg.what);
362                     return;
363             }
364 
365             MessageItem msgItem = getMessageItem(type, (Long) msg.obj, false);
366             if (msgItem != null) {
367                 editMessageItem(msgItem);
368                 drawBottomPanel();
369             }
370         }
371     };
372 
373     private final OnKeyListener mSubjectKeyListener = new OnKeyListener() {
374         public boolean onKey(View v, int keyCode, KeyEvent event) {
375             if (event.getAction() != KeyEvent.ACTION_DOWN) {
376                 return false;
377             }
378 
379             // When the subject editor is empty, press "DEL" to hide the input field.
380             if ((keyCode == KeyEvent.KEYCODE_DEL) && (mSubjectTextEditor.length() == 0)) {
381                 showSubjectEditor(false);
382                 mWorkingMessage.setSubject(null, true);
383                 return true;
384             }
385 
386             return false;
387         }
388     };
389 
390     /**
391      * Return the messageItem associated with the type ("mms" or "sms") and message id.
392      * @param type Type of the message: "mms" or "sms"
393      * @param msgId Message id of the message. This is the _id of the sms or pdu row and is
394      * stored in the MessageItem
395      * @param createFromCursorIfNotInCache true if the item is not found in the MessageListAdapter's
396      * cache and the code can create a new MessageItem based on the position of the current cursor.
397      * If false, the function returns null if the MessageItem isn't in the cache.
398      * @return MessageItem or null if not found and createFromCursorIfNotInCache is false
399      */
getMessageItem(String type, long msgId, boolean createFromCursorIfNotInCache)400     private MessageItem getMessageItem(String type, long msgId,
401             boolean createFromCursorIfNotInCache) {
402         return mMsgListAdapter.getCachedMessageItem(type, msgId,
403                 createFromCursorIfNotInCache ? mMsgListAdapter.getCursor() : null);
404     }
405 
isCursorValid()406     private boolean isCursorValid() {
407         // Check whether the cursor is valid or not.
408         Cursor cursor = mMsgListAdapter.getCursor();
409         if (cursor.isClosed() || cursor.isBeforeFirst() || cursor.isAfterLast()) {
410             Log.e(TAG, "Bad cursor.", new RuntimeException());
411             return false;
412         }
413         return true;
414     }
415 
resetCounter()416     private void resetCounter() {
417         mTextCounter.setText("");
418         mTextCounter.setVisibility(View.GONE);
419     }
420 
updateCounter(CharSequence text, int start, int before, int count)421     private void updateCounter(CharSequence text, int start, int before, int count) {
422         WorkingMessage workingMessage = mWorkingMessage;
423         if (workingMessage.requiresMms()) {
424             // If we're not removing text (i.e. no chance of converting back to SMS
425             // because of this change) and we're in MMS mode, just bail out since we
426             // then won't have to calculate the length unnecessarily.
427             final boolean textRemoved = (before > count);
428             if (!textRemoved) {
429                 showSmsOrMmsSendButton(workingMessage.requiresMms());
430                 return;
431             }
432         }
433 
434         int[] params = SmsMessage.calculateLength(text, false);
435             /* SmsMessage.calculateLength returns an int[4] with:
436              *   int[0] being the number of SMS's required,
437              *   int[1] the number of code units used,
438              *   int[2] is the number of code units remaining until the next message.
439              *   int[3] is the encoding type that should be used for the message.
440              */
441         int msgCount = params[0];
442         int remainingInCurrentMessage = params[2];
443 
444         if (!MmsConfig.getMultipartSmsEnabled()) {
445             mWorkingMessage.setLengthRequiresMms(
446                     msgCount >= MmsConfig.getSmsToMmsTextThreshold(), true);
447         }
448 
449         // Show the counter only if:
450         // - We are not in MMS mode
451         // - We are going to send more than one message OR we are getting close
452         boolean showCounter = false;
453         if (!workingMessage.requiresMms() &&
454                 (msgCount > 1 ||
455                  remainingInCurrentMessage <= CHARS_REMAINING_BEFORE_COUNTER_SHOWN)) {
456             showCounter = true;
457         }
458 
459         showSmsOrMmsSendButton(workingMessage.requiresMms());
460 
461         if (showCounter) {
462             // Update the remaining characters and number of messages required.
463             String counterText = msgCount > 1 ? remainingInCurrentMessage + " / " + msgCount
464                     : String.valueOf(remainingInCurrentMessage);
465             mTextCounter.setText(counterText);
466             mTextCounter.setVisibility(View.VISIBLE);
467         } else {
468             mTextCounter.setVisibility(View.GONE);
469         }
470     }
471 
472     @Override
startActivityForResult(Intent intent, int requestCode)473     public void startActivityForResult(Intent intent, int requestCode)
474     {
475         // requestCode >= 0 means the activity in question is a sub-activity.
476         if (requestCode >= 0) {
477             mWaitingForSubActivity = true;
478         }
479 
480         super.startActivityForResult(intent, requestCode);
481     }
482 
toastConvertInfo(boolean toMms)483     private void toastConvertInfo(boolean toMms) {
484         final int resId = toMms ? R.string.converting_to_picture_message
485                 : R.string.converting_to_text_message;
486         Toast.makeText(this, resId, Toast.LENGTH_SHORT).show();
487     }
488 
489     private class DeleteMessageListener implements OnClickListener {
490         private final Uri mDeleteUri;
491         private final boolean mDeleteLocked;
492 
DeleteMessageListener(Uri uri, boolean deleteLocked)493         public DeleteMessageListener(Uri uri, boolean deleteLocked) {
494             mDeleteUri = uri;
495             mDeleteLocked = deleteLocked;
496         }
497 
DeleteMessageListener(long msgId, String type, boolean deleteLocked)498         public DeleteMessageListener(long msgId, String type, boolean deleteLocked) {
499             if ("mms".equals(type)) {
500                 mDeleteUri = ContentUris.withAppendedId(Mms.CONTENT_URI, msgId);
501             } else {
502                 mDeleteUri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgId);
503             }
504             mDeleteLocked = deleteLocked;
505         }
506 
onClick(DialogInterface dialog, int whichButton)507         public void onClick(DialogInterface dialog, int whichButton) {
508             mBackgroundQueryHandler.startDelete(DELETE_MESSAGE_TOKEN,
509                     null, mDeleteUri, mDeleteLocked ? null : "locked=0", null);
510             dialog.dismiss();
511         }
512     }
513 
514     private class DiscardDraftListener implements OnClickListener {
onClick(DialogInterface dialog, int whichButton)515         public void onClick(DialogInterface dialog, int whichButton) {
516             mWorkingMessage.discard();
517             dialog.dismiss();
518             finish();
519         }
520     }
521 
522     private class SendIgnoreInvalidRecipientListener implements OnClickListener {
onClick(DialogInterface dialog, int whichButton)523         public void onClick(DialogInterface dialog, int whichButton) {
524             sendMessage(true);
525             dialog.dismiss();
526         }
527     }
528 
529     private class CancelSendingListener implements OnClickListener {
onClick(DialogInterface dialog, int whichButton)530         public void onClick(DialogInterface dialog, int whichButton) {
531             if (isRecipientsEditorVisible()) {
532                 mRecipientsEditor.requestFocus();
533             }
534             dialog.dismiss();
535         }
536     }
537 
confirmSendMessageIfNeeded()538     private void confirmSendMessageIfNeeded() {
539         if (!isRecipientsEditorVisible()) {
540             sendMessage(true);
541             return;
542         }
543 
544         boolean isMms = mWorkingMessage.requiresMms();
545         if (mRecipientsEditor.hasInvalidRecipient(isMms)) {
546             if (mRecipientsEditor.hasValidRecipient(isMms)) {
547                 String title = getResourcesString(R.string.has_invalid_recipient,
548                         mRecipientsEditor.formatInvalidNumbers(isMms));
549                 new AlertDialog.Builder(this)
550                     .setIcon(android.R.drawable.ic_dialog_alert)
551                     .setTitle(title)
552                     .setMessage(R.string.invalid_recipient_message)
553                     .setPositiveButton(R.string.try_to_send,
554                             new SendIgnoreInvalidRecipientListener())
555                     .setNegativeButton(R.string.no, new CancelSendingListener())
556                     .show();
557             } else {
558                 new AlertDialog.Builder(this)
559                     .setIcon(android.R.drawable.ic_dialog_alert)
560                     .setTitle(R.string.cannot_send_message)
561                     .setMessage(R.string.cannot_send_message_reason)
562                     .setPositiveButton(R.string.yes, new CancelSendingListener())
563                     .show();
564             }
565         } else {
566             sendMessage(true);
567         }
568     }
569 
570     private final TextWatcher mRecipientsWatcher = new TextWatcher() {
571         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
572         }
573 
574         public void onTextChanged(CharSequence s, int start, int before, int count) {
575             // This is a workaround for bug 1609057.  Since onUserInteraction() is
576             // not called when the user touches the soft keyboard, we pretend it was
577             // called when textfields changes.  This should be removed when the bug
578             // is fixed.
579             onUserInteraction();
580         }
581 
582         public void afterTextChanged(Editable s) {
583             // Bug 1474782 describes a situation in which we send to
584             // the wrong recipient.  We have been unable to reproduce this,
585             // but the best theory we have so far is that the contents of
586             // mRecipientList somehow become stale when entering
587             // ComposeMessageActivity via onNewIntent().  This assertion is
588             // meant to catch one possible path to that, of a non-visible
589             // mRecipientsEditor having its TextWatcher fire and refreshing
590             // mRecipientList with its stale contents.
591             if (!isRecipientsEditorVisible()) {
592                 IllegalStateException e = new IllegalStateException(
593                         "afterTextChanged called with invisible mRecipientsEditor");
594                 // Make sure the crash is uploaded to the service so we
595                 // can see if this is happening in the field.
596                 Log.w(TAG,
597                      "RecipientsWatcher: afterTextChanged called with invisible mRecipientsEditor");
598                 return;
599             }
600 
601             mWorkingMessage.setWorkingRecipients(mRecipientsEditor.getNumbers());
602             mWorkingMessage.setHasEmail(mRecipientsEditor.containsEmail(), true);
603 
604             checkForTooManyRecipients();
605 
606             // Walk backwards in the text box, skipping spaces.  If the last
607             // character is a comma, update the title bar.
608             for (int pos = s.length() - 1; pos >= 0; pos--) {
609                 char c = s.charAt(pos);
610                 if (c == ' ')
611                     continue;
612 
613                 if (c == ',') {
614                     updateTitle(mConversation.getRecipients());
615                 }
616 
617                 break;
618             }
619 
620             // If we have gone to zero recipients, disable send button.
621             updateSendButtonState();
622         }
623     };
624 
checkForTooManyRecipients()625     private void checkForTooManyRecipients() {
626         final int recipientLimit = MmsConfig.getRecipientLimit();
627         if (recipientLimit != Integer.MAX_VALUE) {
628             final int recipientCount = recipientCount();
629             boolean tooMany = recipientCount > recipientLimit;
630 
631             if (recipientCount != mLastRecipientCount) {
632                 // Don't warn the user on every character they type when they're over the limit,
633                 // only when the actual # of recipients changes.
634                 mLastRecipientCount = recipientCount;
635                 if (tooMany) {
636                     String tooManyMsg = getString(R.string.too_many_recipients, recipientCount,
637                             recipientLimit);
638                     Toast.makeText(ComposeMessageActivity.this,
639                             tooManyMsg, Toast.LENGTH_LONG).show();
640                 }
641             }
642         }
643     }
644 
645     private final OnCreateContextMenuListener mRecipientsMenuCreateListener =
646         new OnCreateContextMenuListener() {
647         public void onCreateContextMenu(ContextMenu menu, View v,
648                 ContextMenuInfo menuInfo) {
649             if (menuInfo != null) {
650                 Contact c = ((RecipientContextMenuInfo) menuInfo).recipient;
651                 RecipientsMenuClickListener l = new RecipientsMenuClickListener(c);
652 
653                 menu.setHeaderTitle(c.getName());
654 
655                 if (c.existsInDatabase()) {
656                     menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact)
657                             .setOnMenuItemClickListener(l);
658                 } else if (canAddToContacts(c)){
659                     menu.add(0, MENU_ADD_TO_CONTACTS, 0, R.string.menu_add_to_contacts)
660                             .setOnMenuItemClickListener(l);
661                 }
662             }
663         }
664     };
665 
666     private final class RecipientsMenuClickListener implements MenuItem.OnMenuItemClickListener {
667         private final Contact mRecipient;
668 
RecipientsMenuClickListener(Contact recipient)669         RecipientsMenuClickListener(Contact recipient) {
670             mRecipient = recipient;
671         }
672 
onMenuItemClick(MenuItem item)673         public boolean onMenuItemClick(MenuItem item) {
674             switch (item.getItemId()) {
675                 // Context menu handlers for the recipients editor.
676                 case MENU_VIEW_CONTACT: {
677                     Uri contactUri = mRecipient.getUri();
678                     Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
679                     intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
680                     startActivity(intent);
681                     return true;
682                 }
683                 case MENU_ADD_TO_CONTACTS: {
684                     mAddContactIntent = ConversationList.createAddContactIntent(
685                             mRecipient.getNumber());
686                     ComposeMessageActivity.this.startActivityForResult(mAddContactIntent,
687                             REQUEST_CODE_ADD_CONTACT);
688                     return true;
689                 }
690             }
691             return false;
692         }
693     }
694 
canAddToContacts(Contact contact)695     private boolean canAddToContacts(Contact contact) {
696         // There are some kind of automated messages, like STK messages, that we don't want
697         // to add to contacts. These names begin with special characters, like, "*Info".
698         final String name = contact.getName();
699         if (!TextUtils.isEmpty(contact.getNumber())) {
700             char c = contact.getNumber().charAt(0);
701             if (isSpecialChar(c)) {
702                 return false;
703             }
704         }
705         if (!TextUtils.isEmpty(name)) {
706             char c = name.charAt(0);
707             if (isSpecialChar(c)) {
708                 return false;
709             }
710         }
711         if (!(Mms.isEmailAddress(name) || Mms.isPhoneNumber(name) || contact.isMe())) {
712             return false;
713         }
714         return true;
715     }
716 
isSpecialChar(char c)717     private boolean isSpecialChar(char c) {
718         return c == '*' || c == '%' || c == '$';
719     }
720 
addPositionBasedMenuItems(ContextMenu menu, View v, ContextMenuInfo menuInfo)721     private void addPositionBasedMenuItems(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
722         AdapterView.AdapterContextMenuInfo info;
723 
724         try {
725             info = (AdapterView.AdapterContextMenuInfo) menuInfo;
726         } catch (ClassCastException e) {
727             Log.e(TAG, "bad menuInfo");
728             return;
729         }
730         final int position = info.position;
731 
732         addUriSpecificMenuItems(menu, v, position);
733     }
734 
getSelectedUriFromMessageList(ListView listView, int position)735     private Uri getSelectedUriFromMessageList(ListView listView, int position) {
736         // If the context menu was opened over a uri, get that uri.
737         MessageListItem msglistItem = (MessageListItem) listView.getChildAt(position);
738         if (msglistItem == null) {
739             // FIXME: Should get the correct view. No such interface in ListView currently
740             // to get the view by position. The ListView.getChildAt(position) cannot
741             // get correct view since the list doesn't create one child for each item.
742             // And if setSelection(position) then getSelectedView(),
743             // cannot get corrent view when in touch mode.
744             return null;
745         }
746 
747         TextView textView;
748         CharSequence text = null;
749         int selStart = -1;
750         int selEnd = -1;
751 
752         //check if message sender is selected
753         textView = (TextView) msglistItem.findViewById(R.id.text_view);
754         if (textView != null) {
755             text = textView.getText();
756             selStart = textView.getSelectionStart();
757             selEnd = textView.getSelectionEnd();
758         }
759 
760         if (selStart == -1) {
761             //sender is not being selected, it may be within the message body
762             textView = (TextView) msglistItem.findViewById(R.id.body_text_view);
763             if (textView != null) {
764                 text = textView.getText();
765                 selStart = textView.getSelectionStart();
766                 selEnd = textView.getSelectionEnd();
767             }
768         }
769 
770         // Check that some text is actually selected, rather than the cursor
771         // just being placed within the TextView.
772         if (selStart != selEnd) {
773             int min = Math.min(selStart, selEnd);
774             int max = Math.max(selStart, selEnd);
775 
776             URLSpan[] urls = ((Spanned) text).getSpans(min, max,
777                                                         URLSpan.class);
778 
779             if (urls.length == 1) {
780                 return Uri.parse(urls[0].getURL());
781             }
782         }
783 
784         //no uri was selected
785         return null;
786     }
787 
addUriSpecificMenuItems(ContextMenu menu, View v, int position)788     private void addUriSpecificMenuItems(ContextMenu menu, View v, int position) {
789         Uri uri = getSelectedUriFromMessageList((ListView) v, position);
790 
791         if (uri != null) {
792             Intent intent = new Intent(null, uri);
793             intent.addCategory(Intent.CATEGORY_SELECTED_ALTERNATIVE);
794             menu.addIntentOptions(0, 0, 0,
795                     new android.content.ComponentName(this, ComposeMessageActivity.class),
796                     null, intent, 0, null);
797         }
798     }
799 
addCallAndContactMenuItems( ContextMenu menu, MsgListMenuClickListener l, MessageItem msgItem)800     private final void addCallAndContactMenuItems(
801             ContextMenu menu, MsgListMenuClickListener l, MessageItem msgItem) {
802         if (TextUtils.isEmpty(msgItem.mBody)) {
803             return;
804         }
805         SpannableString msg = new SpannableString(msgItem.mBody);
806         Linkify.addLinks(msg, Linkify.ALL);
807         ArrayList<String> uris =
808             MessageUtils.extractUris(msg.getSpans(0, msg.length(), URLSpan.class));
809 
810         // Remove any dupes so they don't get added to the menu multiple times
811         HashSet<String> collapsedUris = new HashSet<String>();
812         for (String uri : uris) {
813             collapsedUris.add(uri.toLowerCase());
814         }
815         for (String uriString : collapsedUris) {
816             String prefix = null;
817             int sep = uriString.indexOf(":");
818             if (sep >= 0) {
819                 prefix = uriString.substring(0, sep);
820                 uriString = uriString.substring(sep + 1);
821             }
822             Uri contactUri = null;
823             boolean knownPrefix = true;
824             if ("mailto".equalsIgnoreCase(prefix))  {
825                 contactUri = getContactUriForEmail(uriString);
826             } else if ("tel".equalsIgnoreCase(prefix)) {
827                 contactUri = getContactUriForPhoneNumber(uriString);
828             } else {
829                 knownPrefix = false;
830             }
831             if (knownPrefix && contactUri == null) {
832                 Intent intent = ConversationList.createAddContactIntent(uriString);
833 
834                 String addContactString = getString(R.string.menu_add_address_to_contacts,
835                         uriString);
836                 menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, addContactString)
837                     .setOnMenuItemClickListener(l)
838                     .setIntent(intent);
839             }
840         }
841     }
842 
getContactUriForEmail(String emailAddress)843     private Uri getContactUriForEmail(String emailAddress) {
844         Cursor cursor = SqliteWrapper.query(this, getContentResolver(),
845                 Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(emailAddress)),
846                 new String[] { Email.CONTACT_ID, Contacts.DISPLAY_NAME }, null, null, null);
847 
848         if (cursor != null) {
849             try {
850                 while (cursor.moveToNext()) {
851                     String name = cursor.getString(1);
852                     if (!TextUtils.isEmpty(name)) {
853                         return ContentUris.withAppendedId(Contacts.CONTENT_URI, cursor.getLong(0));
854                     }
855                 }
856             } finally {
857                 cursor.close();
858             }
859         }
860         return null;
861     }
862 
getContactUriForPhoneNumber(String phoneNumber)863     private Uri getContactUriForPhoneNumber(String phoneNumber) {
864         Contact contact = Contact.get(phoneNumber, false);
865         if (contact.existsInDatabase()) {
866             return contact.getUri();
867         }
868         return null;
869     }
870 
871     private final OnCreateContextMenuListener mMsgListMenuCreateListener =
872         new OnCreateContextMenuListener() {
873         public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
874             if (!isCursorValid()) {
875                 return;
876             }
877             Cursor cursor = mMsgListAdapter.getCursor();
878             String type = cursor.getString(COLUMN_MSG_TYPE);
879             long msgId = cursor.getLong(COLUMN_ID);
880 
881             addPositionBasedMenuItems(menu, v, menuInfo);
882 
883             MessageItem msgItem = mMsgListAdapter.getCachedMessageItem(type, msgId, cursor);
884             if (msgItem == null) {
885                 Log.e(TAG, "Cannot load message item for type = " + type
886                         + ", msgId = " + msgId);
887                 return;
888             }
889 
890             menu.setHeaderTitle(R.string.message_options);
891 
892             MsgListMenuClickListener l = new MsgListMenuClickListener();
893 
894             // It is unclear what would make most sense for copying an MMS message
895             // to the clipboard, so we currently do SMS only.
896             if (msgItem.isSms()) {
897                 // Message type is sms. Only allow "edit" if the message has a single recipient
898                 if (getRecipients().size() == 1 &&
899                         (msgItem.mBoxId == Sms.MESSAGE_TYPE_OUTBOX ||
900                                 msgItem.mBoxId == Sms.MESSAGE_TYPE_FAILED)) {
901                     menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit)
902                     .setOnMenuItemClickListener(l);
903                 }
904 
905                 menu.add(0, MENU_COPY_MESSAGE_TEXT, 0, R.string.copy_message_text)
906                 .setOnMenuItemClickListener(l);
907             }
908 
909             addCallAndContactMenuItems(menu, l, msgItem);
910 
911             // Forward is not available for undownloaded messages.
912             if (msgItem.isDownloaded()) {
913                 menu.add(0, MENU_FORWARD_MESSAGE, 0, R.string.menu_forward)
914                         .setOnMenuItemClickListener(l);
915             }
916 
917             if (msgItem.isMms()) {
918                 switch (msgItem.mBoxId) {
919                     case Mms.MESSAGE_BOX_INBOX:
920                         break;
921                     case Mms.MESSAGE_BOX_OUTBOX:
922                         // Since we currently break outgoing messages to multiple
923                         // recipients into one message per recipient, only allow
924                         // editing a message for single-recipient conversations.
925                         if (getRecipients().size() == 1) {
926                             menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit)
927                                     .setOnMenuItemClickListener(l);
928                         }
929                         break;
930                 }
931                 switch (msgItem.mAttachmentType) {
932                     case WorkingMessage.TEXT:
933                         break;
934                     case WorkingMessage.VIDEO:
935                     case WorkingMessage.IMAGE:
936                         if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) {
937                             menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard)
938                             .setOnMenuItemClickListener(l);
939                         }
940                         break;
941                     case WorkingMessage.SLIDESHOW:
942                     default:
943                         menu.add(0, MENU_VIEW_SLIDESHOW, 0, R.string.view_slideshow)
944                         .setOnMenuItemClickListener(l);
945                         if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) {
946                             menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard)
947                             .setOnMenuItemClickListener(l);
948                         }
949                         if (haveSomethingToCopyToDrmProvider(msgItem.mMsgId)) {
950                             menu.add(0, MENU_COPY_TO_DRM_PROVIDER, 0,
951                                     getDrmMimeMenuStringRsrc(msgItem.mMsgId))
952                             .setOnMenuItemClickListener(l);
953                         }
954                         break;
955                 }
956             }
957 
958             if (msgItem.mLocked) {
959                 menu.add(0, MENU_UNLOCK_MESSAGE, 0, R.string.menu_unlock)
960                     .setOnMenuItemClickListener(l);
961             } else {
962                 menu.add(0, MENU_LOCK_MESSAGE, 0, R.string.menu_lock)
963                     .setOnMenuItemClickListener(l);
964             }
965 
966             menu.add(0, MENU_VIEW_MESSAGE_DETAILS, 0, R.string.view_message_details)
967                 .setOnMenuItemClickListener(l);
968 
969             if (msgItem.mDeliveryStatus != MessageItem.DeliveryStatus.NONE || msgItem.mReadReport) {
970                 menu.add(0, MENU_DELIVERY_REPORT, 0, R.string.view_delivery_report)
971                         .setOnMenuItemClickListener(l);
972             }
973 
974             menu.add(0, MENU_DELETE_MESSAGE, 0, R.string.delete_message)
975                 .setOnMenuItemClickListener(l);
976         }
977     };
978 
editMessageItem(MessageItem msgItem)979     private void editMessageItem(MessageItem msgItem) {
980         if ("sms".equals(msgItem.mType)) {
981             editSmsMessageItem(msgItem);
982         } else {
983             editMmsMessageItem(msgItem);
984         }
985         if (msgItem.isFailedMessage() && mMsgListAdapter.getCount() <= 1) {
986             // For messages with bad addresses, let the user re-edit the recipients.
987             initRecipientsEditor();
988         }
989     }
990 
editSmsMessageItem(MessageItem msgItem)991     private void editSmsMessageItem(MessageItem msgItem) {
992         // When the message being edited is the only message in the conversation, the delete
993         // below does something subtle. The trigger "delete_obsolete_threads_pdu" sees that a
994         // thread contains no messages and silently deletes the thread. Meanwhile, the mConversation
995         // object still holds onto the old thread_id and code thinks there's a backing thread in
996         // the DB when it really has been deleted. Here we try and notice that situation and
997         // clear out the thread_id. Later on, when Conversation.ensureThreadId() is called, we'll
998         // create a new thread if necessary.
999         synchronized(mConversation) {
1000             if (mConversation.getMessageCount() <= 1) {
1001                 mConversation.clearThreadId();
1002             }
1003         }
1004         // Delete the old undelivered SMS and load its content.
1005         Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgItem.mMsgId);
1006         SqliteWrapper.delete(ComposeMessageActivity.this,
1007                 mContentResolver, uri, null, null);
1008 
1009         mWorkingMessage.setText(msgItem.mBody);
1010     }
1011 
editMmsMessageItem(MessageItem msgItem)1012     private void editMmsMessageItem(MessageItem msgItem) {
1013         // Discard the current message in progress.
1014         mWorkingMessage.discard();
1015 
1016         // Load the selected message in as the working message.
1017         mWorkingMessage = WorkingMessage.load(this, msgItem.mMessageUri);
1018         mWorkingMessage.setConversation(mConversation);
1019 
1020         drawTopPanel(false);
1021 
1022         // WorkingMessage.load() above only loads the slideshow. Set the
1023         // subject here because we already know what it is and avoid doing
1024         // another DB lookup in load() just to get it.
1025         mWorkingMessage.setSubject(msgItem.mSubject, false);
1026 
1027         if (mWorkingMessage.hasSubject()) {
1028             showSubjectEditor(true);
1029         }
1030     }
1031 
copyToClipboard(String str)1032     private void copyToClipboard(String str) {
1033         ClipboardManager clip =
1034             (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
1035         clip.setText(str);
1036     }
1037 
forwardMessage(MessageItem msgItem)1038     private void forwardMessage(MessageItem msgItem) {
1039         Intent intent = createIntent(this, 0);
1040 
1041         intent.putExtra("exit_on_sent", true);
1042         intent.putExtra("forwarded_message", true);
1043 
1044         if (msgItem.mType.equals("sms")) {
1045             intent.putExtra("sms_body", msgItem.mBody);
1046         } else {
1047             SendReq sendReq = new SendReq();
1048             String subject = getString(R.string.forward_prefix);
1049             if (msgItem.mSubject != null) {
1050                 subject += msgItem.mSubject;
1051             }
1052             sendReq.setSubject(new EncodedStringValue(subject));
1053             sendReq.setBody(msgItem.mSlideshow.makeCopy(
1054                     ComposeMessageActivity.this));
1055 
1056             Uri uri = null;
1057             try {
1058                 PduPersister persister = PduPersister.getPduPersister(this);
1059                 // Copy the parts of the message here.
1060                 uri = persister.persist(sendReq, Mms.Draft.CONTENT_URI);
1061             } catch (MmsException e) {
1062                 Log.e(TAG, "Failed to copy message: " + msgItem.mMessageUri, e);
1063                 Toast.makeText(ComposeMessageActivity.this,
1064                         R.string.cannot_save_message, Toast.LENGTH_SHORT).show();
1065                 return;
1066             }
1067 
1068             intent.putExtra("msg_uri", uri);
1069             intent.putExtra("subject", subject);
1070         }
1071         // ForwardMessageActivity is simply an alias in the manifest for ComposeMessageActivity.
1072         // We have to make an alias because ComposeMessageActivity launch flags specify
1073         // singleTop. When we forward a message, we want to start a separate ComposeMessageActivity.
1074         // The only way to do that is to override the singleTop flag, which is impossible to do
1075         // in code. By creating an alias to the activity, without the singleTop flag, we can
1076         // launch a separate ComposeMessageActivity to edit the forward message.
1077         intent.setClassName(this, "com.android.mms.ui.ForwardMessageActivity");
1078         startActivity(intent);
1079     }
1080 
1081     /**
1082      * Context menu handlers for the message list view.
1083      */
1084     private final class MsgListMenuClickListener implements MenuItem.OnMenuItemClickListener {
onMenuItemClick(MenuItem item)1085         public boolean onMenuItemClick(MenuItem item) {
1086             if (!isCursorValid()) {
1087                 return false;
1088             }
1089             Cursor cursor = mMsgListAdapter.getCursor();
1090             String type = cursor.getString(COLUMN_MSG_TYPE);
1091             long msgId = cursor.getLong(COLUMN_ID);
1092             MessageItem msgItem = getMessageItem(type, msgId, true);
1093 
1094             if (msgItem == null) {
1095                 return false;
1096             }
1097 
1098             switch (item.getItemId()) {
1099                 case MENU_EDIT_MESSAGE:
1100                     editMessageItem(msgItem);
1101                     drawBottomPanel();
1102                     return true;
1103 
1104                 case MENU_COPY_MESSAGE_TEXT:
1105                     copyToClipboard(msgItem.mBody);
1106                     return true;
1107 
1108                 case MENU_FORWARD_MESSAGE:
1109                     forwardMessage(msgItem);
1110                     return true;
1111 
1112                 case MENU_VIEW_SLIDESHOW:
1113                     MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this,
1114                             ContentUris.withAppendedId(Mms.CONTENT_URI, msgId), null);
1115                     return true;
1116 
1117                 case MENU_VIEW_MESSAGE_DETAILS: {
1118                     String messageDetails = MessageUtils.getMessageDetails(
1119                             ComposeMessageActivity.this, cursor, msgItem.mMessageSize);
1120                     new AlertDialog.Builder(ComposeMessageActivity.this)
1121                             .setTitle(R.string.message_details_title)
1122                             .setMessage(messageDetails)
1123                             .setCancelable(true)
1124                             .show();
1125                     return true;
1126                 }
1127                 case MENU_DELETE_MESSAGE: {
1128                     DeleteMessageListener l = new DeleteMessageListener(
1129                             msgItem.mMessageUri, msgItem.mLocked);
1130                     confirmDeleteDialog(l, msgItem.mLocked);
1131                     return true;
1132                 }
1133                 case MENU_DELIVERY_REPORT:
1134                     showDeliveryReport(msgId, type);
1135                     return true;
1136 
1137                 case MENU_COPY_TO_SDCARD: {
1138                     int resId = copyMedia(msgId) ? R.string.copy_to_sdcard_success :
1139                         R.string.copy_to_sdcard_fail;
1140                     Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show();
1141                     return true;
1142                 }
1143 
1144                 case MENU_COPY_TO_DRM_PROVIDER: {
1145                     int resId = getDrmMimeSavedStringRsrc(msgId, copyToDrmProvider(msgId));
1146                     Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show();
1147                     return true;
1148                 }
1149 
1150                 case MENU_LOCK_MESSAGE: {
1151                     lockMessage(msgItem, true);
1152                     return true;
1153                 }
1154 
1155                 case MENU_UNLOCK_MESSAGE: {
1156                     lockMessage(msgItem, false);
1157                     return true;
1158                 }
1159 
1160                 default:
1161                     return false;
1162             }
1163         }
1164     }
1165 
lockMessage(MessageItem msgItem, boolean locked)1166     private void lockMessage(MessageItem msgItem, boolean locked) {
1167         Uri uri;
1168         if ("sms".equals(msgItem.mType)) {
1169             uri = Sms.CONTENT_URI;
1170         } else {
1171             uri = Mms.CONTENT_URI;
1172         }
1173         final Uri lockUri = ContentUris.withAppendedId(uri, msgItem.mMsgId);
1174 
1175         final ContentValues values = new ContentValues(1);
1176         values.put("locked", locked ? 1 : 0);
1177 
1178         new Thread(new Runnable() {
1179             public void run() {
1180                 getContentResolver().update(lockUri,
1181                         values, null, null);
1182             }
1183         }, "lockMessage").start();
1184     }
1185 
1186     /**
1187      * Looks to see if there are any valid parts of the attachment that can be copied to a SD card.
1188      * @param msgId
1189      */
haveSomethingToCopyToSDCard(long msgId)1190     private boolean haveSomethingToCopyToSDCard(long msgId) {
1191         PduBody body = PduBodyCache.getPduBody(this,
1192                 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1193         if (body == null) {
1194             return false;
1195         }
1196 
1197         boolean result = false;
1198         int partNum = body.getPartsNum();
1199         for(int i = 0; i < partNum; i++) {
1200             PduPart part = body.getPart(i);
1201             String type = new String(part.getContentType());
1202 
1203             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1204                 log("[CMA] haveSomethingToCopyToSDCard: part[" + i + "] contentType=" + type);
1205             }
1206 
1207             if (ContentType.isImageType(type) || ContentType.isVideoType(type) ||
1208                     ContentType.isAudioType(type)) {
1209                 result = true;
1210                 break;
1211             }
1212         }
1213         return result;
1214     }
1215 
1216     /**
1217      * Looks to see if there are any drm'd parts of the attachment that can be copied to the
1218      * DrmProvider. Right now we only support saving audio (e.g. ringtones).
1219      * @param msgId
1220      */
haveSomethingToCopyToDrmProvider(long msgId)1221     private boolean haveSomethingToCopyToDrmProvider(long msgId) {
1222         String mimeType = getDrmMimeType(msgId);
1223         return isAudioMimeType(mimeType);
1224     }
1225 
1226     /**
1227      * Simple cache to prevent having to load the same PduBody again and again for the same uri.
1228      */
1229     private static class PduBodyCache {
1230         private static PduBody mLastPduBody;
1231         private static Uri mLastUri;
1232 
getPduBody(Context context, Uri contentUri)1233         static public PduBody getPduBody(Context context, Uri contentUri) {
1234             if (contentUri.equals(mLastUri)) {
1235                 return mLastPduBody;
1236             }
1237             try {
1238                 mLastPduBody = SlideshowModel.getPduBody(context, contentUri);
1239                 mLastUri = contentUri;
1240              } catch (MmsException e) {
1241                  Log.e(TAG, e.getMessage(), e);
1242                  return null;
1243              }
1244              return mLastPduBody;
1245         }
1246     };
1247 
1248     /**
1249      * Copies media from an Mms to the DrmProvider
1250      * @param msgId
1251      */
copyToDrmProvider(long msgId)1252     private boolean copyToDrmProvider(long msgId) {
1253         boolean result = true;
1254         PduBody body = PduBodyCache.getPduBody(this,
1255                 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1256         if (body == null) {
1257             return false;
1258         }
1259 
1260         int partNum = body.getPartsNum();
1261         for(int i = 0; i < partNum; i++) {
1262             PduPart part = body.getPart(i);
1263             String type = new String(part.getContentType());
1264 
1265             if (ContentType.isDrmType(type)) {
1266                 // All parts (but there's probably only a single one) have to be successful
1267                 // for a valid result.
1268                 result &= copyPartToDrmProvider(part);
1269             }
1270         }
1271         return result;
1272     }
1273 
mimeTypeOfDrmPart(PduPart part)1274     private String mimeTypeOfDrmPart(PduPart part) {
1275         Uri uri = part.getDataUri();
1276         InputStream input = null;
1277         try {
1278             input = mContentResolver.openInputStream(uri);
1279             if (input instanceof FileInputStream) {
1280                 FileInputStream fin = (FileInputStream) input;
1281 
1282                 DrmRawContent content = new DrmRawContent(fin, fin.available(),
1283                         DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING);
1284                 String mimeType = content.getContentType();
1285                 return mimeType;
1286             }
1287         } catch (IOException e) {
1288             // Ignore
1289             Log.e(TAG, "IOException caught while opening or reading stream", e);
1290         } catch (DrmException e) {
1291             Log.e(TAG, "DrmException caught ", e);
1292         } finally {
1293             if (null != input) {
1294                 try {
1295                     input.close();
1296                 } catch (IOException e) {
1297                     // Ignore
1298                     Log.e(TAG, "IOException caught while closing stream", e);
1299                 }
1300             }
1301         }
1302         return null;
1303     }
1304 
1305     /**
1306      * Returns the type of the first drm'd pdu part.
1307      * @param msgId
1308      */
getDrmMimeType(long msgId)1309     private String getDrmMimeType(long msgId) {
1310         PduBody body = PduBodyCache.getPduBody(this,
1311                 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1312         if (body == null) {
1313             return null;
1314         }
1315 
1316         int partNum = body.getPartsNum();
1317         for(int i = 0; i < partNum; i++) {
1318             PduPart part = body.getPart(i);
1319             String type = new String(part.getContentType());
1320 
1321             if (ContentType.isDrmType(type)) {
1322                 return mimeTypeOfDrmPart(part);
1323             }
1324         }
1325         return null;
1326     }
1327 
getDrmMimeMenuStringRsrc(long msgId)1328     private int getDrmMimeMenuStringRsrc(long msgId) {
1329         String mimeType = getDrmMimeType(msgId);
1330         if (isAudioMimeType(mimeType)) {
1331             return R.string.save_ringtone;
1332         }
1333         return 0;
1334     }
1335 
getDrmMimeSavedStringRsrc(long msgId, boolean success)1336     private int getDrmMimeSavedStringRsrc(long msgId, boolean success) {
1337         String mimeType = getDrmMimeType(msgId);
1338         if (isAudioMimeType(mimeType)) {
1339             return success ? R.string.saved_ringtone : R.string.saved_ringtone_fail;
1340         }
1341         return 0;
1342     }
1343 
isAudioMimeType(String mimeType)1344     private boolean isAudioMimeType(String mimeType) {
1345         return mimeType != null && mimeType.startsWith("audio/");
1346     }
1347 
isImageMimeType(String mimeType)1348     private boolean isImageMimeType(String mimeType) {
1349         return mimeType != null && mimeType.startsWith("image/");
1350     }
1351 
copyPartToDrmProvider(PduPart part)1352     private boolean copyPartToDrmProvider(PduPart part) {
1353         Uri uri = part.getDataUri();
1354 
1355         InputStream input = null;
1356         try {
1357             input = mContentResolver.openInputStream(uri);
1358             if (input instanceof FileInputStream) {
1359                 FileInputStream fin = (FileInputStream) input;
1360 
1361                 // Build a nice title
1362                 byte[] location = part.getName();
1363                 if (location == null) {
1364                     location = part.getFilename();
1365                 }
1366                 if (location == null) {
1367                     location = part.getContentLocation();
1368                 }
1369 
1370                 // Depending on the location, there may be an
1371                 // extension already on the name or not
1372                 String title = new String(location);
1373                 int index;
1374                 if ((index = title.indexOf(".")) == -1) {
1375                     String type = new String(part.getContentType());
1376                 } else {
1377                     title = title.substring(0, index);
1378                 }
1379 
1380                 // transfer the file to the DRM content provider
1381                 Intent item = DrmStore.addDrmFile(mContentResolver, fin, title);
1382                 if (item == null) {
1383                     Log.w(TAG, "unable to add file " + uri + " to DrmProvider");
1384                     return false;
1385                 }
1386             }
1387         } catch (IOException e) {
1388             // Ignore
1389             Log.e(TAG, "IOException caught while opening or reading stream", e);
1390             return false;
1391         } finally {
1392             if (null != input) {
1393                 try {
1394                     input.close();
1395                 } catch (IOException e) {
1396                     // Ignore
1397                     Log.e(TAG, "IOException caught while closing stream", e);
1398                     return false;
1399                 }
1400             }
1401         }
1402         return true;
1403     }
1404 
1405     /**
1406      * Copies media from an Mms to the "download" directory on the SD card
1407      * @param msgId
1408      */
copyMedia(long msgId)1409     private boolean copyMedia(long msgId) {
1410         boolean result = true;
1411         PduBody body = PduBodyCache.getPduBody(this,
1412                 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1413         if (body == null) {
1414             return false;
1415         }
1416 
1417         int partNum = body.getPartsNum();
1418         for(int i = 0; i < partNum; i++) {
1419             PduPart part = body.getPart(i);
1420             String type = new String(part.getContentType());
1421 
1422             if (ContentType.isImageType(type) || ContentType.isVideoType(type) ||
1423                     ContentType.isAudioType(type)) {
1424                 result &= copyPart(part, Long.toHexString(msgId));   // all parts have to be successful for a valid result.
1425             }
1426         }
1427         return result;
1428     }
1429 
copyPart(PduPart part, String fallback)1430     private boolean copyPart(PduPart part, String fallback) {
1431         Uri uri = part.getDataUri();
1432 
1433         InputStream input = null;
1434         FileOutputStream fout = null;
1435         try {
1436             input = mContentResolver.openInputStream(uri);
1437             if (input instanceof FileInputStream) {
1438                 FileInputStream fin = (FileInputStream) input;
1439 
1440                 byte[] location = part.getName();
1441                 if (location == null) {
1442                     location = part.getFilename();
1443                 }
1444                 if (location == null) {
1445                     location = part.getContentLocation();
1446                 }
1447 
1448                 String fileName;
1449                 if (location == null) {
1450                     // Use fallback name.
1451                     fileName = fallback;
1452                 } else {
1453                     fileName = new String(location);
1454                 }
1455                 // Depending on the location, there may be an
1456                 // extension already on the name or not
1457                 String dir = Environment.getExternalStorageDirectory() + "/"
1458                                 + Environment.DIRECTORY_DOWNLOADS  + "/";
1459                 String extension;
1460                 int index;
1461                 if ((index = fileName.indexOf(".")) == -1) {
1462                     String type = new String(part.getContentType());
1463                     extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
1464                 } else {
1465                     extension = fileName.substring(index + 1, fileName.length());
1466                     fileName = fileName.substring(0, index);
1467                 }
1468 
1469                 File file = getUniqueDestination(dir + fileName, extension);
1470 
1471                 // make sure the path is valid and directories created for this file.
1472                 File parentFile = file.getParentFile();
1473                 if (!parentFile.exists() && !parentFile.mkdirs()) {
1474                     Log.e(TAG, "[MMS] copyPart: mkdirs for " + parentFile.getPath() + " failed!");
1475                     return false;
1476                 }
1477 
1478                 fout = new FileOutputStream(file);
1479 
1480                 byte[] buffer = new byte[8000];
1481                 int size = 0;
1482                 while ((size=fin.read(buffer)) != -1) {
1483                     fout.write(buffer, 0, size);
1484                 }
1485 
1486                 // Notify other applications listening to scanner events
1487                 // that a media file has been added to the sd card
1488                 sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
1489                         Uri.fromFile(file)));
1490             }
1491         } catch (IOException e) {
1492             // Ignore
1493             Log.e(TAG, "IOException caught while opening or reading stream", e);
1494             return false;
1495         } finally {
1496             if (null != input) {
1497                 try {
1498                     input.close();
1499                 } catch (IOException e) {
1500                     // Ignore
1501                     Log.e(TAG, "IOException caught while closing stream", e);
1502                     return false;
1503                 }
1504             }
1505             if (null != fout) {
1506                 try {
1507                     fout.close();
1508                 } catch (IOException e) {
1509                     // Ignore
1510                     Log.e(TAG, "IOException caught while closing stream", e);
1511                     return false;
1512                 }
1513             }
1514         }
1515         return true;
1516     }
1517 
getUniqueDestination(String base, String extension)1518     private File getUniqueDestination(String base, String extension) {
1519         File file = new File(base + "." + extension);
1520 
1521         for (int i = 2; file.exists(); i++) {
1522             file = new File(base + "_" + i + "." + extension);
1523         }
1524         return file;
1525     }
1526 
showDeliveryReport(long messageId, String type)1527     private void showDeliveryReport(long messageId, String type) {
1528         Intent intent = new Intent(this, DeliveryReportActivity.class);
1529         intent.putExtra("message_id", messageId);
1530         intent.putExtra("message_type", type);
1531 
1532         startActivity(intent);
1533     }
1534 
1535     private final IntentFilter mHttpProgressFilter = new IntentFilter(PROGRESS_STATUS_ACTION);
1536 
1537     private final BroadcastReceiver mHttpProgressReceiver = new BroadcastReceiver() {
1538         @Override
1539         public void onReceive(Context context, Intent intent) {
1540             if (PROGRESS_STATUS_ACTION.equals(intent.getAction())) {
1541                 long token = intent.getLongExtra("token",
1542                                     SendingProgressTokenManager.NO_TOKEN);
1543                 if (token != mConversation.getThreadId()) {
1544                     return;
1545                 }
1546 
1547                 int progress = intent.getIntExtra("progress", 0);
1548                 switch (progress) {
1549                     case PROGRESS_START:
1550                         setProgressBarVisibility(true);
1551                         break;
1552                     case PROGRESS_ABORT:
1553                     case PROGRESS_COMPLETE:
1554                         setProgressBarVisibility(false);
1555                         break;
1556                     default:
1557                         setProgress(100 * progress);
1558                 }
1559             }
1560         }
1561     };
1562 
1563     private static ContactList sEmptyContactList;
1564 
getRecipients()1565     private ContactList getRecipients() {
1566         // If the recipients editor is visible, the conversation has
1567         // not really officially 'started' yet.  Recipients will be set
1568         // on the conversation once it has been saved or sent.  In the
1569         // meantime, let anyone who needs the recipient list think it
1570         // is empty rather than giving them a stale one.
1571         if (isRecipientsEditorVisible()) {
1572             if (sEmptyContactList == null) {
1573                 sEmptyContactList = new ContactList();
1574             }
1575             return sEmptyContactList;
1576         }
1577         return mConversation.getRecipients();
1578     }
1579 
updateTitle(ContactList list)1580     private void updateTitle(ContactList list) {
1581         String title = null;;
1582         String subTitle = null;
1583         int cnt = list.size();
1584         switch (cnt) {
1585             case 0: {
1586                 String recipient = null;
1587                 if (mRecipientsEditor != null) {
1588                     recipient = mRecipientsEditor.getText().toString();
1589                 }
1590                 title = TextUtils.isEmpty(recipient) ? getString(R.string.new_message) : recipient;
1591                 break;
1592             }
1593             case 1: {
1594                 title = list.get(0).getName();      // get name returns the number if there's no
1595                                                     // name available.
1596                 String number = list.get(0).getNumber();
1597                 if (!title.equals(number)) {
1598                     subTitle = PhoneNumberUtils.formatNumber(number, number,
1599                             MmsApp.getApplication().getCurrentCountryIso());
1600                 }
1601                 break;
1602             }
1603             default: {
1604                 // Handle multiple recipients
1605                 title = list.formatNames(", ");
1606                 subTitle = getResources().getQuantityString(R.plurals.recipient_count, cnt, cnt);
1607                 break;
1608             }
1609         }
1610         mDebugRecipients = list.serialize();
1611 
1612         ActionBar actionBar = getActionBar();
1613         actionBar.setTitle(title);
1614         actionBar.setSubtitle(subTitle);
1615     }
1616 
1617     // Get the recipients editor ready to be displayed onscreen.
initRecipientsEditor()1618     private void initRecipientsEditor() {
1619         if (isRecipientsEditorVisible()) {
1620             return;
1621         }
1622         // Must grab the recipients before the view is made visible because getRecipients()
1623         // returns empty recipients when the editor is visible.
1624         ContactList recipients = getRecipients();
1625 
1626         ViewStub stub = (ViewStub)findViewById(R.id.recipients_editor_stub);
1627         if (stub != null) {
1628             View stubView = stub.inflate();
1629             mRecipientsEditor = (RecipientsEditor) stubView.findViewById(R.id.recipients_editor);
1630             mRecipientsPicker = (ImageButton) stubView.findViewById(R.id.recipients_picker);
1631         } else {
1632             mRecipientsEditor = (RecipientsEditor)findViewById(R.id.recipients_editor);
1633             mRecipientsEditor.setVisibility(View.VISIBLE);
1634             mRecipientsPicker = (ImageButton)findViewById(R.id.recipients_picker);
1635         }
1636         mRecipientsPicker.setOnClickListener(this);
1637 
1638         mRecipientsEditor.setAdapter(new RecipientsAdapter(this));
1639         mRecipientsEditor.populate(recipients);
1640         mRecipientsEditor.setOnCreateContextMenuListener(mRecipientsMenuCreateListener);
1641         mRecipientsEditor.addTextChangedListener(mRecipientsWatcher);
1642         // TODO : Remove the max length limitation due to the multiple phone picker is added and the
1643         // user is able to select a large number of recipients from the Contacts. The coming
1644         // potential issue is that it is hard for user to edit a recipient from hundred of
1645         // recipients in the editor box. We may redesign the editor box UI for this use case.
1646         // mRecipientsEditor.setFilters(new InputFilter[] {
1647         //         new InputFilter.LengthFilter(RECIPIENTS_MAX_LENGTH) });
1648         mRecipientsEditor.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1649             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1650                 // After the user selects an item in the pop-up contacts list, move the
1651                 // focus to the text editor if there is only one recipient.  This helps
1652                 // the common case of selecting one recipient and then typing a message,
1653                 // but avoids annoying a user who is trying to add five recipients and
1654                 // keeps having focus stolen away.
1655                 if (mRecipientsEditor.getRecipientCount() == 1) {
1656                     // if we're in extract mode then don't request focus
1657                     final InputMethodManager inputManager = (InputMethodManager)
1658                         getSystemService(Context.INPUT_METHOD_SERVICE);
1659                     if (inputManager == null || !inputManager.isFullscreenMode()) {
1660                         mTextEditor.requestFocus();
1661                     }
1662                 }
1663             }
1664         });
1665 
1666         mRecipientsEditor.setOnFocusChangeListener(new View.OnFocusChangeListener() {
1667             public void onFocusChange(View v, boolean hasFocus) {
1668                 if (!hasFocus) {
1669                     RecipientsEditor editor = (RecipientsEditor) v;
1670                     ContactList contacts = editor.constructContactsFromInput(false);
1671                     updateTitle(contacts);
1672                 }
1673             }
1674         });
1675 
1676         mTopPanel.setVisibility(View.VISIBLE);
1677     }
1678 
1679     //==========================================================
1680     // Activity methods
1681     //==========================================================
1682 
cancelFailedToDeliverNotification(Intent intent, Context context)1683     public static boolean cancelFailedToDeliverNotification(Intent intent, Context context) {
1684         if (MessagingNotification.isFailedToDeliver(intent)) {
1685             // Cancel any failed message notifications
1686             MessagingNotification.cancelNotification(context,
1687                         MessagingNotification.MESSAGE_FAILED_NOTIFICATION_ID);
1688             return true;
1689         }
1690         return false;
1691     }
1692 
cancelFailedDownloadNotification(Intent intent, Context context)1693     public static boolean cancelFailedDownloadNotification(Intent intent, Context context) {
1694         if (MessagingNotification.isFailedToDownload(intent)) {
1695             // Cancel any failed download notifications
1696             MessagingNotification.cancelNotification(context,
1697                         MessagingNotification.DOWNLOAD_FAILED_NOTIFICATION_ID);
1698             return true;
1699         }
1700         return false;
1701     }
1702 
1703     @Override
onCreate(Bundle savedInstanceState)1704     protected void onCreate(Bundle savedInstanceState) {
1705         super.onCreate(savedInstanceState);
1706 
1707         resetConfiguration(getResources().getConfiguration());
1708 
1709         setContentView(R.layout.compose_message_activity);
1710         setProgressBarVisibility(false);
1711 
1712         getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
1713                 WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
1714 
1715         // Initialize members for UI elements.
1716         initResourceRefs();
1717 
1718         mContentResolver = getContentResolver();
1719         mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver);
1720 
1721         initialize(0);
1722 
1723         if (TRACE) {
1724             android.os.Debug.startMethodTracing("compose");
1725         }
1726     }
1727 
showSubjectEditor(boolean show)1728     private void showSubjectEditor(boolean show) {
1729         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1730             log("" + show);
1731         }
1732 
1733         if (mSubjectTextEditor == null) {
1734             // Don't bother to initialize the subject editor if
1735             // we're just going to hide it.
1736             if (show == false) {
1737                 return;
1738             }
1739             mSubjectTextEditor = (EditText)findViewById(R.id.subject);
1740             mSubjectTextEditor.setFilters(new InputFilter[] {
1741                     new LengthFilter(MmsConfig.getMaxSubjectLength())});
1742         }
1743 
1744         mSubjectTextEditor.setOnKeyListener(show ? mSubjectKeyListener : null);
1745 
1746         if (show) {
1747             mSubjectTextEditor.addTextChangedListener(mSubjectEditorWatcher);
1748         } else {
1749             mSubjectTextEditor.removeTextChangedListener(mSubjectEditorWatcher);
1750         }
1751 
1752         mSubjectTextEditor.setText(mWorkingMessage.getSubject());
1753         mSubjectTextEditor.setVisibility(show ? View.VISIBLE : View.GONE);
1754         hideOrShowTopPanel();
1755     }
1756 
hideOrShowTopPanel()1757     private void hideOrShowTopPanel() {
1758         boolean anySubViewsVisible = (isSubjectEditorVisible() || isRecipientsEditorVisible());
1759         mTopPanel.setVisibility(anySubViewsVisible ? View.VISIBLE : View.GONE);
1760     }
1761 
initialize(long originalThreadId)1762     public void initialize(long originalThreadId) {
1763         Intent intent = getIntent();
1764 
1765         // Create a new empty working message.
1766         mWorkingMessage = WorkingMessage.createEmpty(this);
1767 
1768         // Read parameters or previously saved state of this activity. This will load a new
1769         // mConversation
1770         initActivityState(intent);
1771 
1772         if (LogTag.SEVERE_WARNING && originalThreadId != 0 &&
1773                 originalThreadId == mConversation.getThreadId()) {
1774             LogTag.warnPossibleRecipientMismatch("ComposeMessageActivity.initialize: " +
1775                     " threadId didn't change from: " + originalThreadId, this);
1776         }
1777 
1778         log(" intent = " + intent +
1779             "originalThreadId = " + originalThreadId +
1780             " mConversation = " + mConversation);
1781 
1782         if (cancelFailedToDeliverNotification(getIntent(), this)) {
1783             // Show a pop-up dialog to inform user the message was
1784             // failed to deliver.
1785             undeliveredMessageDialog(getMessageDate(null));
1786         }
1787         cancelFailedDownloadNotification(getIntent(), this);
1788 
1789         // Set up the message history ListAdapter
1790         initMessageList();
1791 
1792         // Load the draft for this thread, if we aren't already handling
1793         // existing data, such as a shared picture or forwarded message.
1794         boolean isForwardedMessage = false;
1795         if (!handleSendIntent(intent)) {
1796             isForwardedMessage = handleForwardedMessage();
1797             if (!isForwardedMessage) {
1798                 loadDraft();
1799             }
1800         }
1801 
1802         // Let the working message know what conversation it belongs to
1803         mWorkingMessage.setConversation(mConversation);
1804 
1805         // Show the recipients editor if we don't have a valid thread. Hide it otherwise.
1806         if (mConversation.getThreadId() <= 0) {
1807             // Hide the recipients editor so the call to initRecipientsEditor won't get
1808             // short-circuited.
1809             hideRecipientEditor();
1810             initRecipientsEditor();
1811 
1812             // Bring up the softkeyboard so the user can immediately enter recipients. This
1813             // call won't do anything on devices with a hard keyboard.
1814             getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
1815                     WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
1816         } else {
1817             hideRecipientEditor();
1818         }
1819 
1820         updateSendButtonState();
1821 
1822         drawTopPanel(false);
1823         drawBottomPanel();
1824 
1825         onKeyboardStateChanged(mIsKeyboardOpen);
1826 
1827         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1828             log("update title, mConversation=" + mConversation.toString());
1829         }
1830 
1831         updateTitle(mConversation.getRecipients());
1832 
1833         if (isForwardedMessage && isRecipientsEditorVisible()) {
1834             // The user is forwarding the message to someone. Put the focus on the
1835             // recipient editor rather than in the message editor.
1836             mRecipientsEditor.requestFocus();
1837         }
1838     }
1839 
1840     @Override
onNewIntent(Intent intent)1841     protected void onNewIntent(Intent intent) {
1842         super.onNewIntent(intent);
1843 
1844         setIntent(intent);
1845 
1846         Conversation conversation = null;
1847         mSentMessage = false;
1848 
1849         // If we have been passed a thread_id, use that to find our
1850         // conversation.
1851 
1852         // Note that originalThreadId might be zero but if this is a draft and we save the
1853         // draft, ensureThreadId gets called async from WorkingMessage.asyncUpdateDraftSmsMessage
1854         // the thread will get a threadId behind the UI thread's back.
1855         long originalThreadId = mConversation.getThreadId();
1856         long threadId = intent.getLongExtra("thread_id", 0);
1857         Uri intentUri = intent.getData();
1858 
1859         boolean sameThread = false;
1860         if (threadId > 0) {
1861             conversation = Conversation.get(this, threadId, false);
1862         } else {
1863             if (mConversation.getThreadId() == 0) {
1864                 // We've got a draft. Make sure the working recipients are synched
1865                 // to the conversation so when we compare conversations later in this function,
1866                 // the compare will work.
1867                 mWorkingMessage.syncWorkingRecipients();
1868             }
1869             // Get the "real" conversation based on the intentUri. The intentUri might specify
1870             // the conversation by a phone number or by a thread id. We'll typically get a threadId
1871             // based uri when the user pulls down a notification while in ComposeMessageActivity and
1872             // we end up here in onNewIntent. mConversation can have a threadId of zero when we're
1873             // working on a draft. When a new message comes in for that same recipient, a
1874             // conversation will get created behind CMA's back when the message is inserted into
1875             // the database and the corresponding entry made in the threads table. The code should
1876             // use the real conversation as soon as it can rather than finding out the threadId
1877             // when sending with "ensureThreadId".
1878             conversation = Conversation.get(this, intentUri, false);
1879         }
1880 
1881         if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1882             log("onNewIntent: data=" + intentUri + ", thread_id extra is " + threadId +
1883                     ", new conversation=" + conversation + ", mConversation=" + mConversation);
1884         }
1885 
1886         // this is probably paranoid to compare both thread_ids and recipient lists,
1887         // but we want to make double sure because this is a last minute fix for Froyo
1888         // and the previous code checked thread ids only.
1889         // (we cannot just compare thread ids because there is a case where mConversation
1890         // has a stale/obsolete thread id (=1) that could collide against the new thread_id(=1),
1891         // even though the recipient lists are different)
1892         sameThread = ((conversation.getThreadId() == mConversation.getThreadId() ||
1893                 mConversation.getThreadId() == 0) &&
1894                 conversation.equals(mConversation));
1895 
1896         // Don't let any markAsRead DB updates occur before we've loaded the messages for
1897         // the thread. Unblocking occurs when we're done querying for the conversation
1898         // items.
1899         conversation.blockMarkAsRead(true);
1900 
1901         if (sameThread) {
1902             log("onNewIntent: same conversation");
1903             if (mConversation.getThreadId() == 0) {
1904                 mConversation = conversation;
1905                 mWorkingMessage.setConversation(mConversation);
1906             }
1907             mConversation.markAsRead();         // dismiss any notifications for this convo
1908         } else {
1909             if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1910                 log("onNewIntent: different conversation");
1911             }
1912             saveDraft(false);    // if we've got a draft, save it first
1913 
1914             initialize(originalThreadId);
1915         }
1916         loadMessageContent();
1917     }
1918 
sanityCheckConversation()1919     private void sanityCheckConversation() {
1920         if (mWorkingMessage.getConversation() != mConversation) {
1921             LogTag.warnPossibleRecipientMismatch(
1922                     "ComposeMessageActivity: mWorkingMessage.mConversation=" +
1923                     mWorkingMessage.getConversation() + ", mConversation=" +
1924                     mConversation + ", MISMATCH!", this);
1925         }
1926     }
1927 
1928     @Override
onRestart()1929     protected void onRestart() {
1930         super.onRestart();
1931 
1932         if (mWorkingMessage.isDiscarded()) {
1933             // If the message isn't worth saving, don't resurrect it. Doing so can lead to
1934             // a situation where a new incoming message gets the old thread id of the discarded
1935             // draft. This activity can end up displaying the recipients of the old message with
1936             // the contents of the new message. Recognize that dangerous situation and bail out
1937             // to the ConversationList where the user can enter this in a clean manner.
1938             if (mWorkingMessage.isWorthSaving()) {
1939                 if (LogTag.VERBOSE) {
1940                     log("onRestart: mWorkingMessage.unDiscard()");
1941                 }
1942                 mWorkingMessage.unDiscard();    // it was discarded in onStop().
1943 
1944                 sanityCheckConversation();
1945             } else if (isRecipientsEditorVisible()) {
1946                 if (LogTag.VERBOSE) {
1947                     log("onRestart: goToConversationList");
1948                 }
1949                 goToConversationList();
1950             } else {
1951                 if (LogTag.VERBOSE) {
1952                     log("onRestart: loadDraft");
1953                 }
1954                 loadDraft();
1955                 mWorkingMessage.setConversation(mConversation);
1956                 mAttachmentEditor.update(mWorkingMessage);
1957             }
1958         }
1959     }
1960 
1961     @Override
onStart()1962     protected void onStart() {
1963         super.onStart();
1964         mConversation.blockMarkAsRead(true);
1965 
1966         initFocus();
1967 
1968         // Register a BroadcastReceiver to listen on HTTP I/O process.
1969         registerReceiver(mHttpProgressReceiver, mHttpProgressFilter);
1970 
1971         loadMessageContent();
1972 
1973         // Update the fasttrack info in case any of the recipients' contact info changed
1974         // while we were paused. This can happen, for example, if a user changes or adds
1975         // an avatar associated with a contact.
1976         mWorkingMessage.syncWorkingRecipients();
1977 
1978         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1979             log("update title, mConversation=" + mConversation.toString());
1980         }
1981 
1982         updateTitle(mConversation.getRecipients());
1983 
1984         ActionBar actionBar = getActionBar();
1985         actionBar.setDisplayHomeAsUpEnabled(true);
1986     }
1987 
loadMessageContent()1988     public void loadMessageContent() {
1989         startMsgListQuery();
1990         updateSendFailedNotification();
1991         drawBottomPanel();
1992     }
1993 
updateSendFailedNotification()1994     private void updateSendFailedNotification() {
1995         final long threadId = mConversation.getThreadId();
1996         if (threadId <= 0)
1997             return;
1998 
1999         // updateSendFailedNotificationForThread makes a database call, so do the work off
2000         // of the ui thread.
2001         new Thread(new Runnable() {
2002             public void run() {
2003                 MessagingNotification.updateSendFailedNotificationForThread(
2004                         ComposeMessageActivity.this, threadId);
2005             }
2006         }, "updateSendFailedNotification").start();
2007     }
2008 
2009     @Override
onResume()2010     protected void onResume() {
2011         super.onResume();
2012 
2013         // OLD: get notified of presence updates to update the titlebar.
2014         // NEW: we are using ContactHeaderWidget which displays presence, but updating presence
2015         //      there is out of our control.
2016         //Contact.startPresenceObserver();
2017 
2018         addRecipientsListeners();
2019 
2020         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2021             log("update title, mConversation=" + mConversation.toString());
2022         }
2023 
2024         // There seems to be a bug in the framework such that setting the title
2025         // here gets overwritten to the original title.  Do this delayed as a
2026         // workaround.
2027         mMessageListItemHandler.postDelayed(new Runnable() {
2028             public void run() {
2029                 ContactList recipients = isRecipientsEditorVisible() ?
2030                         mRecipientsEditor.constructContactsFromInput(false) : getRecipients();
2031                 updateTitle(recipients);
2032             }
2033         }, 100);
2034     }
2035 
2036     @Override
onPause()2037     protected void onPause() {
2038         super.onPause();
2039 
2040         // OLD: stop getting notified of presence updates to update the titlebar.
2041         // NEW: we are using ContactHeaderWidget which displays presence, but updating presence
2042         //      there is out of our control.
2043         //Contact.stopPresenceObserver();
2044 
2045         removeRecipientsListeners();
2046     }
2047 
2048     @Override
onStop()2049     protected void onStop() {
2050         super.onStop();
2051 
2052         // Allow any blocked calls to update the thread's read status.
2053         mConversation.blockMarkAsRead(false);
2054 
2055         if (mMsgListAdapter != null) {
2056             mMsgListAdapter.changeCursor(null);
2057         }
2058 
2059         if (mRecipientsEditor != null) {
2060             CursorAdapter recipientsAdapter = (CursorAdapter)mRecipientsEditor.getAdapter();
2061             if (recipientsAdapter != null) {
2062                 recipientsAdapter.changeCursor(null);
2063             }
2064         }
2065 
2066         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2067             log("save draft");
2068         }
2069         saveDraft(true);
2070 
2071         // Cleanup the BroadcastReceiver.
2072         unregisterReceiver(mHttpProgressReceiver);
2073     }
2074 
2075     @Override
onDestroy()2076     protected void onDestroy() {
2077         if (TRACE) {
2078             android.os.Debug.stopMethodTracing();
2079         }
2080 
2081         super.onDestroy();
2082     }
2083 
2084     @Override
onConfigurationChanged(Configuration newConfig)2085     public void onConfigurationChanged(Configuration newConfig) {
2086         super.onConfigurationChanged(newConfig);
2087         if (LOCAL_LOGV) {
2088             Log.v(TAG, "onConfigurationChanged: " + newConfig);
2089         }
2090 
2091         if (resetConfiguration(newConfig)) {
2092             // Have to re-layout the attachment editor because we have different layouts
2093             // depending on whether we're portrait or landscape.
2094             drawTopPanel(isSubjectEditorVisible());
2095         }
2096         onKeyboardStateChanged(mIsKeyboardOpen);
2097     }
2098 
2099     // returns true if landscape/portrait configuration has changed
resetConfiguration(Configuration config)2100     private boolean resetConfiguration(Configuration config) {
2101         mIsKeyboardOpen = config.keyboardHidden == KEYBOARDHIDDEN_NO;
2102         boolean isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE;
2103         if (mIsLandscape != isLandscape) {
2104             mIsLandscape = isLandscape;
2105             return true;
2106         }
2107         return false;
2108     }
2109 
onKeyboardStateChanged(boolean isKeyboardOpen)2110     private void onKeyboardStateChanged(boolean isKeyboardOpen) {
2111         // If the keyboard is hidden, don't show focus highlights for
2112         // things that cannot receive input.
2113         if (isKeyboardOpen) {
2114             if (mRecipientsEditor != null) {
2115                 mRecipientsEditor.setFocusableInTouchMode(true);
2116             }
2117             if (mSubjectTextEditor != null) {
2118                 mSubjectTextEditor.setFocusableInTouchMode(true);
2119             }
2120             mTextEditor.setFocusableInTouchMode(true);
2121             mTextEditor.setHint(R.string.type_to_compose_text_enter_to_send);
2122         } else {
2123             if (mRecipientsEditor != null) {
2124                 mRecipientsEditor.setFocusable(false);
2125             }
2126             if (mSubjectTextEditor != null) {
2127                 mSubjectTextEditor.setFocusable(false);
2128             }
2129             mTextEditor.setFocusable(false);
2130             mTextEditor.setHint(R.string.open_keyboard_to_compose_message);
2131         }
2132     }
2133 
2134     @Override
onUserInteraction()2135     public void onUserInteraction() {
2136         checkPendingNotification();
2137     }
2138 
2139     @Override
onWindowFocusChanged(boolean hasFocus)2140     public void onWindowFocusChanged(boolean hasFocus) {
2141         if (hasFocus) {
2142             checkPendingNotification();
2143         }
2144     }
2145 
2146     @Override
onKeyDown(int keyCode, KeyEvent event)2147     public boolean onKeyDown(int keyCode, KeyEvent event) {
2148         switch (keyCode) {
2149             case KeyEvent.KEYCODE_DEL:
2150                 if ((mMsgListAdapter != null) && mMsgListView.isFocused()) {
2151                     Cursor cursor;
2152                     try {
2153                         cursor = (Cursor) mMsgListView.getSelectedItem();
2154                     } catch (ClassCastException e) {
2155                         Log.e(TAG, "Unexpected ClassCastException.", e);
2156                         return super.onKeyDown(keyCode, event);
2157                     }
2158 
2159                     if (cursor != null) {
2160                         boolean locked = cursor.getInt(COLUMN_MMS_LOCKED) != 0;
2161                         DeleteMessageListener l = new DeleteMessageListener(
2162                                 cursor.getLong(COLUMN_ID),
2163                                 cursor.getString(COLUMN_MSG_TYPE),
2164                                 locked);
2165                         confirmDeleteDialog(l, locked);
2166                         return true;
2167                     }
2168                 }
2169                 break;
2170             case KeyEvent.KEYCODE_DPAD_CENTER:
2171             case KeyEvent.KEYCODE_ENTER:
2172                 if (isPreparedForSending()) {
2173                     confirmSendMessageIfNeeded();
2174                     return true;
2175                 }
2176                 break;
2177             case KeyEvent.KEYCODE_BACK:
2178                 exitComposeMessageActivity(new Runnable() {
2179                     public void run() {
2180                         finish();
2181                     }
2182                 });
2183                 return true;
2184         }
2185 
2186         return super.onKeyDown(keyCode, event);
2187     }
2188 
exitComposeMessageActivity(final Runnable exit)2189     private void exitComposeMessageActivity(final Runnable exit) {
2190         // If the message is empty, just quit -- finishing the
2191         // activity will cause an empty draft to be deleted.
2192         if (!mWorkingMessage.isWorthSaving()) {
2193             exit.run();
2194             return;
2195         }
2196 
2197         if (isRecipientsEditorVisible() &&
2198                 !mRecipientsEditor.hasValidRecipient(mWorkingMessage.requiresMms())) {
2199             MessageUtils.showDiscardDraftConfirmDialog(this, new DiscardDraftListener());
2200             return;
2201         }
2202 
2203         mToastForDraftSave = true;
2204         exit.run();
2205     }
2206 
goToConversationList()2207     private void goToConversationList() {
2208         finish();
2209         startActivity(new Intent(this, ConversationList.class));
2210     }
2211 
hideRecipientEditor()2212     private void hideRecipientEditor() {
2213         if (mRecipientsEditor != null) {
2214             mRecipientsEditor.removeTextChangedListener(mRecipientsWatcher);
2215             mRecipientsEditor.setVisibility(View.GONE);
2216             hideOrShowTopPanel();
2217         }
2218     }
2219 
isRecipientsEditorVisible()2220     private boolean isRecipientsEditorVisible() {
2221         return (null != mRecipientsEditor)
2222                     && (View.VISIBLE == mRecipientsEditor.getVisibility());
2223     }
2224 
isSubjectEditorVisible()2225     private boolean isSubjectEditorVisible() {
2226         return (null != mSubjectTextEditor)
2227                     && (View.VISIBLE == mSubjectTextEditor.getVisibility());
2228     }
2229 
onAttachmentChanged()2230     public void onAttachmentChanged() {
2231         // Have to make sure we're on the UI thread. This function can be called off of the UI
2232         // thread when we're adding multi-attachments
2233         runOnUiThread(new Runnable() {
2234             public void run() {
2235                 drawBottomPanel();
2236                 updateSendButtonState();
2237                 drawTopPanel(isSubjectEditorVisible());
2238             }
2239         });
2240     }
2241 
onProtocolChanged(final boolean mms)2242     public void onProtocolChanged(final boolean mms) {
2243         // Have to make sure we're on the UI thread. This function can be called off of the UI
2244         // thread when we're adding multi-attachments
2245         runOnUiThread(new Runnable() {
2246             public void run() {
2247                 toastConvertInfo(mms);
2248                 showSmsOrMmsSendButton(mms);
2249 
2250                 if (mms) {
2251                     // In the case we went from a long sms with a counter to an mms because
2252                     // the user added an attachment or a subject, hide the counter --
2253                     // it doesn't apply to mms.
2254                     mTextCounter.setVisibility(View.GONE);
2255                 }
2256             }
2257         });
2258     }
2259 
2260     // Show or hide the Sms or Mms button as appropriate. Return the view so that the caller
2261     // can adjust the enableness and focusability.
showSmsOrMmsSendButton(boolean isMms)2262     private View showSmsOrMmsSendButton(boolean isMms) {
2263         View showButton;
2264         View hideButton;
2265         if (isMms) {
2266             showButton = mSendButtonMms;
2267             hideButton = mSendButtonSms;
2268         } else {
2269             showButton = mSendButtonSms;
2270             hideButton = mSendButtonMms;
2271         }
2272         showButton.setVisibility(View.VISIBLE);
2273         hideButton.setVisibility(View.GONE);
2274 
2275         return showButton;
2276     }
2277 
2278     Runnable mResetMessageRunnable = new Runnable() {
2279         public void run() {
2280             resetMessage();
2281         }
2282     };
2283 
onPreMessageSent()2284     public void onPreMessageSent() {
2285         runOnUiThread(mResetMessageRunnable);
2286     }
2287 
onMessageSent()2288     public void onMessageSent() {
2289         // If we already have messages in the list adapter, it
2290         // will be auto-requerying; don't thrash another query in.
2291         if (mMsgListAdapter.getCount() == 0) {
2292             if (LogTag.VERBOSE) {
2293                 log("onMessageSent");
2294             }
2295             startMsgListQuery();
2296         }
2297     }
2298 
onMaxPendingMessagesReached()2299     public void onMaxPendingMessagesReached() {
2300         saveDraft(false);
2301 
2302         runOnUiThread(new Runnable() {
2303             public void run() {
2304                 Toast.makeText(ComposeMessageActivity.this, R.string.too_many_unsent_mms,
2305                         Toast.LENGTH_LONG).show();
2306             }
2307         });
2308     }
2309 
onAttachmentError(final int error)2310     public void onAttachmentError(final int error) {
2311         runOnUiThread(new Runnable() {
2312             public void run() {
2313                 handleAddAttachmentError(error, R.string.type_picture);
2314                 onMessageSent();        // now requery the list of messages
2315             }
2316         });
2317     }
2318 
2319     // We don't want to show the "call" option unless there is only one
2320     // recipient and it's a phone number.
isRecipientCallable()2321     private boolean isRecipientCallable() {
2322         ContactList recipients = getRecipients();
2323         return (recipients.size() == 1 && !recipients.containsEmail());
2324     }
2325 
dialRecipient()2326     private void dialRecipient() {
2327         if (isRecipientCallable()) {
2328             String number = getRecipients().get(0).getNumber();
2329             Intent dialIntent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + number));
2330             startActivity(dialIntent);
2331         }
2332     }
2333 
2334     @Override
onPrepareOptionsMenu(Menu menu)2335     public boolean onPrepareOptionsMenu(Menu menu) {
2336         menu.clear();
2337 
2338         if (isRecipientCallable()) {
2339             MenuItem item = menu.add(0, MENU_CALL_RECIPIENT, 0, R.string.menu_call)
2340                 .setIcon(R.drawable.ic_menu_call)
2341                 .setTitle(R.string.menu_call);
2342             if (!isRecipientsEditorVisible()) {
2343                 // If we're not composing a new message, show the call icon in the actionbar
2344                 item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
2345             }
2346         }
2347 
2348         if (MmsConfig.getMmsEnabled()) {
2349             if (!isSubjectEditorVisible()) {
2350                 menu.add(0, MENU_ADD_SUBJECT, 0, R.string.add_subject).setIcon(
2351                         R.drawable.ic_menu_edit);
2352             }
2353             menu.add(0, MENU_ADD_ATTACHMENT, 0, R.string.add_attachment)
2354                 .setIcon(R.drawable.ic_menu_attachment)
2355                 .setTitle(R.string.add_attachment)
2356                 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);    // add to actionbar
2357         }
2358 
2359         if (isPreparedForSending()) {
2360             menu.add(0, MENU_SEND, 0, R.string.send).setIcon(android.R.drawable.ic_menu_send);
2361         }
2362 
2363         menu.add(0, MENU_INSERT_SMILEY, 0, R.string.menu_insert_smiley).setIcon(
2364                 R.drawable.ic_menu_emoticons);
2365 
2366         if (mMsgListAdapter.getCount() > 0) {
2367             // Removed search as part of b/1205708
2368             //menu.add(0, MENU_SEARCH, 0, R.string.menu_search).setIcon(
2369             //        R.drawable.ic_menu_search);
2370             Cursor cursor = mMsgListAdapter.getCursor();
2371             if ((null != cursor) && (cursor.getCount() > 0)) {
2372                 menu.add(0, MENU_DELETE_THREAD, 0, R.string.delete_thread).setIcon(
2373                     android.R.drawable.ic_menu_delete);
2374             }
2375         } else {
2376             menu.add(0, MENU_DISCARD, 0, R.string.discard).setIcon(android.R.drawable.ic_menu_delete);
2377         }
2378 
2379         buildAddAddressToContactMenuItem(menu);
2380 
2381         menu.add(0, MENU_PREFERENCES, 0, R.string.menu_preferences).setIcon(
2382                 android.R.drawable.ic_menu_preferences);
2383 
2384         if (LogTag.DEBUG_DUMP) {
2385             menu.add(0, MENU_DEBUG_DUMP, 0, R.string.menu_debug_dump);
2386         }
2387 
2388         return true;
2389     }
2390 
buildAddAddressToContactMenuItem(Menu menu)2391     private void buildAddAddressToContactMenuItem(Menu menu) {
2392         // Look for the first recipient we don't have a contact for and create a menu item to
2393         // add the number to contacts.
2394         for (Contact c : getRecipients()) {
2395             if (!c.existsInDatabase() && canAddToContacts(c)) {
2396                 Intent intent = ConversationList.createAddContactIntent(c.getNumber());
2397                 menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, R.string.menu_add_to_contacts)
2398                     .setIcon(android.R.drawable.ic_menu_add)
2399                     .setIntent(intent);
2400                 break;
2401             }
2402         }
2403     }
2404 
2405     @Override
onOptionsItemSelected(MenuItem item)2406     public boolean onOptionsItemSelected(MenuItem item) {
2407         switch (item.getItemId()) {
2408             case MENU_ADD_SUBJECT:
2409                 showSubjectEditor(true);
2410                 mWorkingMessage.setSubject("", true);
2411                 mSubjectTextEditor.requestFocus();
2412                 break;
2413             case MENU_ADD_ATTACHMENT:
2414                 // Launch the add-attachment list dialog
2415                 showAddAttachmentDialog(false);
2416                 break;
2417             case MENU_DISCARD:
2418                 mWorkingMessage.discard();
2419                 finish();
2420                 break;
2421             case MENU_SEND:
2422                 if (isPreparedForSending()) {
2423                     confirmSendMessageIfNeeded();
2424                 }
2425                 break;
2426             case MENU_SEARCH:
2427                 onSearchRequested();
2428                 break;
2429             case MENU_DELETE_THREAD:
2430                 confirmDeleteThread(mConversation.getThreadId());
2431                 break;
2432 
2433             case android.R.id.home:
2434             case MENU_CONVERSATION_LIST:
2435                 exitComposeMessageActivity(new Runnable() {
2436                     public void run() {
2437                         goToConversationList();
2438                     }
2439                 });
2440                 break;
2441             case MENU_CALL_RECIPIENT:
2442                 dialRecipient();
2443                 break;
2444             case MENU_INSERT_SMILEY:
2445                 showSmileyDialog();
2446                 break;
2447             case MENU_VIEW_CONTACT: {
2448                 // View the contact for the first (and only) recipient.
2449                 ContactList list = getRecipients();
2450                 if (list.size() == 1 && list.get(0).existsInDatabase()) {
2451                     Uri contactUri = list.get(0).getUri();
2452                     Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
2453                     intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
2454                     startActivity(intent);
2455                 }
2456                 break;
2457             }
2458             case MENU_ADD_ADDRESS_TO_CONTACTS:
2459                 mAddContactIntent = item.getIntent();
2460                 startActivityForResult(mAddContactIntent, REQUEST_CODE_ADD_CONTACT);
2461                 break;
2462             case MENU_PREFERENCES: {
2463                 Intent intent = new Intent(this, MessagingPreferenceActivity.class);
2464                 startActivityIfNeeded(intent, -1);
2465                 break;
2466             }
2467             case MENU_DEBUG_DUMP:
2468                 mWorkingMessage.dump();
2469                 Conversation.dump();
2470                 LogTag.dumpInternalTables(this);
2471                 break;
2472         }
2473 
2474         return true;
2475     }
2476 
confirmDeleteThread(long threadId)2477     private void confirmDeleteThread(long threadId) {
2478         Conversation.startQueryHaveLockedMessages(mBackgroundQueryHandler,
2479                 threadId, ConversationList.HAVE_LOCKED_MESSAGES_TOKEN);
2480     }
2481 
2482 //    static class SystemProperties { // TODO, temp class to get unbundling working
2483 //        static int getInt(String s, int value) {
2484 //            return value;       // just return the default value or now
2485 //        }
2486 //    }
2487 
addAttachment(int type, boolean replace)2488     private void addAttachment(int type, boolean replace) {
2489         // Calculate the size of the current slide if we're doing a replace so the
2490         // slide size can optionally be used in computing how much room is left for an attachment.
2491         int currentSlideSize = 0;
2492         SlideshowModel slideShow = mWorkingMessage.getSlideshow();
2493         if (replace && slideShow != null) {
2494             SlideModel slide = slideShow.get(0);
2495             currentSlideSize = slide.getSlideSize();
2496         }
2497         switch (type) {
2498             case AttachmentTypeSelectorAdapter.ADD_IMAGE:
2499                 MessageUtils.selectImage(this, REQUEST_CODE_ATTACH_IMAGE);
2500                 break;
2501 
2502             case AttachmentTypeSelectorAdapter.TAKE_PICTURE: {
2503                 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
2504 
2505                 intent.putExtra(MediaStore.EXTRA_OUTPUT, TempFileProvider.SCRAP_CONTENT_URI);
2506                 startActivityForResult(intent, REQUEST_CODE_TAKE_PICTURE);
2507                 break;
2508             }
2509 
2510             case AttachmentTypeSelectorAdapter.ADD_VIDEO:
2511                 MessageUtils.selectVideo(this, REQUEST_CODE_ATTACH_VIDEO);
2512                 break;
2513 
2514             case AttachmentTypeSelectorAdapter.RECORD_VIDEO: {
2515                 long sizeLimit = computeAttachmentSizeLimit(slideShow, currentSlideSize);
2516                 if (sizeLimit > 0) {
2517                     MessageUtils.recordVideo(this, REQUEST_CODE_TAKE_VIDEO, sizeLimit);
2518                 } else {
2519                     Toast.makeText(this,
2520                             getString(R.string.message_too_big_for_video),
2521                             Toast.LENGTH_SHORT).show();
2522                 }
2523             }
2524             break;
2525 
2526             case AttachmentTypeSelectorAdapter.ADD_SOUND:
2527                 MessageUtils.selectAudio(this, REQUEST_CODE_ATTACH_SOUND);
2528                 break;
2529 
2530             case AttachmentTypeSelectorAdapter.RECORD_SOUND:
2531                 long sizeLimit = computeAttachmentSizeLimit(slideShow, currentSlideSize);
2532                 MessageUtils.recordSound(this, REQUEST_CODE_RECORD_SOUND, sizeLimit);
2533                 break;
2534 
2535             case AttachmentTypeSelectorAdapter.ADD_SLIDESHOW:
2536                 editSlideshow();
2537                 break;
2538 
2539             default:
2540                 break;
2541         }
2542     }
2543 
computeAttachmentSizeLimit(SlideshowModel slideShow, int currentSlideSize)2544     public static long computeAttachmentSizeLimit(SlideshowModel slideShow, int currentSlideSize) {
2545         // Computer attachment size limit. Subtract 1K for some text.
2546         long sizeLimit = MmsConfig.getMaxMessageSize() - SlideshowModel.SLIDESHOW_SLOP;
2547         if (slideShow != null) {
2548             sizeLimit -= slideShow.getCurrentMessageSize();
2549 
2550             // We're about to ask the camera to capture some video (or the sound recorder
2551             // to record some audio) which will eventually replace the content on the current
2552             // slide. Since the current slide already has some content (which was subtracted
2553             // out just above) and that content is going to get replaced, we can add the size of the
2554             // current slide into the available space used to capture a video (or audio).
2555             sizeLimit += currentSlideSize;
2556         }
2557         return sizeLimit;
2558     }
2559 
showAddAttachmentDialog(final boolean replace)2560     private void showAddAttachmentDialog(final boolean replace) {
2561         AlertDialog.Builder builder = new AlertDialog.Builder(this);
2562         builder.setIcon(R.drawable.ic_dialog_attach);
2563         builder.setTitle(R.string.add_attachment);
2564 
2565         if (mAttachmentTypeSelectorAdapter == null) {
2566             mAttachmentTypeSelectorAdapter = new AttachmentTypeSelectorAdapter(
2567                     this, AttachmentTypeSelectorAdapter.MODE_WITH_SLIDESHOW);
2568         }
2569         builder.setAdapter(mAttachmentTypeSelectorAdapter, new DialogInterface.OnClickListener() {
2570             public void onClick(DialogInterface dialog, int which) {
2571                 addAttachment(mAttachmentTypeSelectorAdapter.buttonToCommand(which), replace);
2572                 dialog.dismiss();
2573             }
2574         });
2575 
2576         builder.show();
2577     }
2578 
2579     @Override
onActivityResult(int requestCode, int resultCode, Intent data)2580     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
2581         if (LogTag.VERBOSE) {
2582             log("requestCode=" + requestCode + ", resultCode=" + resultCode + ", data=" + data);
2583         }
2584         mWaitingForSubActivity = false;          // We're back!
2585         if (mWorkingMessage.isFakeMmsForDraft()) {
2586             // We no longer have to fake the fact we're an Mms. At this point we are or we aren't,
2587             // based on attachments and other Mms attrs.
2588             mWorkingMessage.removeFakeMmsForDraft();
2589         }
2590 
2591         if (requestCode == REQUEST_CODE_PICK) {
2592             mWorkingMessage.asyncDeleteDraftSmsMessage(mConversation);
2593         }
2594 
2595         if (requestCode == REQUEST_CODE_ADD_CONTACT) {
2596             // The user might have added a new contact. When we tell contacts to add a contact
2597             // and tap "Done", we're not returned to Messaging. If we back out to return to
2598             // messaging after adding a contact, the resultCode is RESULT_CANCELED. Therefore,
2599             // assume a contact was added and get the contact and force our cached contact to
2600             // get reloaded with the new info (such as contact name). After the
2601             // contact is reloaded, the function onUpdate() in this file will get called
2602             // and it will update the title bar, etc.
2603             if (mAddContactIntent != null) {
2604                 String address =
2605                     mAddContactIntent.getStringExtra(ContactsContract.Intents.Insert.EMAIL);
2606                 if (address == null) {
2607                     address =
2608                         mAddContactIntent.getStringExtra(ContactsContract.Intents.Insert.PHONE);
2609                 }
2610                 if (address != null) {
2611                     Contact contact = Contact.get(address, false);
2612                     if (contact != null) {
2613                         contact.reload();
2614                     }
2615                 }
2616             }
2617         }
2618 
2619         if (resultCode != RESULT_OK){
2620             if (LogTag.VERBOSE) log("bail due to resultCode=" + resultCode);
2621             return;
2622         }
2623 
2624         switch (requestCode) {
2625             case REQUEST_CODE_CREATE_SLIDESHOW:
2626                 if (data != null) {
2627                     WorkingMessage newMessage = WorkingMessage.load(this, data.getData());
2628                     if (newMessage != null) {
2629                         mWorkingMessage = newMessage;
2630                         mWorkingMessage.setConversation(mConversation);
2631                         drawTopPanel(false);
2632                         updateSendButtonState();
2633                     }
2634                 }
2635                 break;
2636 
2637             case REQUEST_CODE_TAKE_PICTURE: {
2638                 // create a file based uri and pass to addImage(). We want to read the JPEG
2639                 // data directly from file (using UriImage) instead of decoding it into a Bitmap,
2640                 // which takes up too much memory and could easily lead to OOM.
2641                 File file = new File(TempFileProvider.getScrapPath());
2642                 Uri uri = Uri.fromFile(file);
2643                 addImage(uri, false);
2644                 break;
2645             }
2646 
2647             case REQUEST_CODE_ATTACH_IMAGE: {
2648                 if (data != null) {
2649                     addImage(data.getData(), false);
2650                 }
2651                 break;
2652             }
2653 
2654             case REQUEST_CODE_TAKE_VIDEO:
2655                 Uri videoUri = TempFileProvider.renameScrapFile(".3gp", null);
2656                 addVideo(videoUri, false);      // can handle null videoUri
2657                 break;
2658 
2659             case REQUEST_CODE_ATTACH_VIDEO:
2660                 if (data != null) {
2661                     addVideo(data.getData(), false);
2662                 }
2663                 break;
2664 
2665             case REQUEST_CODE_ATTACH_SOUND: {
2666                 Uri uri = (Uri) data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
2667                 if (Settings.System.DEFAULT_RINGTONE_URI.equals(uri)) {
2668                     break;
2669                 }
2670                 addAudio(uri);
2671                 break;
2672             }
2673 
2674             case REQUEST_CODE_RECORD_SOUND:
2675                 if (data != null) {
2676                     addAudio(data.getData());
2677                 }
2678                 break;
2679 
2680             case REQUEST_CODE_ECM_EXIT_DIALOG:
2681                 boolean outOfEmergencyMode = data.getBooleanExtra(EXIT_ECM_RESULT, false);
2682                 if (outOfEmergencyMode) {
2683                     sendMessage(false);
2684                 }
2685                 break;
2686 
2687             case REQUEST_CODE_PICK:
2688                 if (data != null) {
2689                     processPickResult(data);
2690                 }
2691                 break;
2692 
2693             default:
2694                 if (LogTag.VERBOSE) log("bail due to unknown requestCode=" + requestCode);
2695                 break;
2696         }
2697     }
2698 
processPickResult(final Intent data)2699     private void processPickResult(final Intent data) {
2700         // The EXTRA_PHONE_URIS stores the phone's urls that were selected by user in the
2701         // multiple phone picker.
2702         final Parcelable[] uris =
2703             data.getParcelableArrayExtra(Intents.EXTRA_PHONE_URIS);
2704 
2705         final int recipientCount = uris != null ? uris.length : 0;
2706 
2707         final int recipientLimit = MmsConfig.getRecipientLimit();
2708         if (recipientLimit != Integer.MAX_VALUE && recipientCount > recipientLimit) {
2709             new AlertDialog.Builder(this)
2710                     .setTitle(R.string.pick_too_many_recipients)
2711                     .setIcon(android.R.drawable.ic_dialog_alert)
2712                     .setMessage(getString(R.string.too_many_recipients, recipientCount, recipientLimit))
2713                     .setPositiveButton(android.R.string.ok, null)
2714                     .create().show();
2715             return;
2716         }
2717 
2718         final Handler handler = new Handler();
2719         final ProgressDialog progressDialog = new ProgressDialog(this);
2720         progressDialog.setTitle(getText(R.string.pick_too_many_recipients));
2721         progressDialog.setMessage(getText(R.string.adding_recipients));
2722         progressDialog.setIndeterminate(true);
2723         progressDialog.setCancelable(false);
2724 
2725         final Runnable showProgress = new Runnable() {
2726             public void run() {
2727                 progressDialog.show();
2728             }
2729         };
2730         // Only show the progress dialog if we can not finish off parsing the return data in 1s,
2731         // otherwise the dialog could flicker.
2732         handler.postDelayed(showProgress, 1000);
2733 
2734         new Thread(new Runnable() {
2735             public void run() {
2736                 final ContactList list;
2737                  try {
2738                     list = ContactList.blockingGetByUris(uris);
2739                 } finally {
2740                     handler.removeCallbacks(showProgress);
2741                     progressDialog.dismiss();
2742                 }
2743                 // TODO: there is already code to update the contact header widget and recipients
2744                 // editor if the contacts change. we can re-use that code.
2745                 final Runnable populateWorker = new Runnable() {
2746                     public void run() {
2747                         mRecipientsEditor.populate(list);
2748                         updateTitle(list);
2749                     }
2750                 };
2751                 handler.post(populateWorker);
2752             }
2753         }).start();
2754     }
2755 
2756     private final ResizeImageResultCallback mResizeImageCallback = new ResizeImageResultCallback() {
2757         // TODO: make this produce a Uri, that's what we want anyway
2758         public void onResizeResult(PduPart part, boolean append) {
2759             if (part == null) {
2760                 handleAddAttachmentError(WorkingMessage.UNKNOWN_ERROR, R.string.type_picture);
2761                 return;
2762             }
2763 
2764             Context context = ComposeMessageActivity.this;
2765             PduPersister persister = PduPersister.getPduPersister(context);
2766             int result;
2767 
2768             Uri messageUri = mWorkingMessage.saveAsMms(true);
2769             if (messageUri == null) {
2770                 result = WorkingMessage.UNKNOWN_ERROR;
2771             } else {
2772                 try {
2773                     Uri dataUri = persister.persistPart(part, ContentUris.parseId(messageUri));
2774                     result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, dataUri, append);
2775                     if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2776                         log("ResizeImageResultCallback: dataUri=" + dataUri);
2777                     }
2778                 } catch (MmsException e) {
2779                     result = WorkingMessage.UNKNOWN_ERROR;
2780                 }
2781             }
2782 
2783             handleAddAttachmentError(result, R.string.type_picture);
2784         }
2785     };
2786 
handleAddAttachmentError(final int error, final int mediaTypeStringId)2787     private void handleAddAttachmentError(final int error, final int mediaTypeStringId) {
2788         if (error == WorkingMessage.OK) {
2789             return;
2790         }
2791 
2792         runOnUiThread(new Runnable() {
2793             public void run() {
2794                 Resources res = getResources();
2795                 String mediaType = res.getString(mediaTypeStringId);
2796                 String title, message;
2797 
2798                 switch(error) {
2799                 case WorkingMessage.UNKNOWN_ERROR:
2800                     message = res.getString(R.string.failed_to_add_media, mediaType);
2801                     Toast.makeText(ComposeMessageActivity.this, message, Toast.LENGTH_SHORT).show();
2802                     return;
2803                 case WorkingMessage.UNSUPPORTED_TYPE:
2804                     title = res.getString(R.string.unsupported_media_format, mediaType);
2805                     message = res.getString(R.string.select_different_media, mediaType);
2806                     break;
2807                 case WorkingMessage.MESSAGE_SIZE_EXCEEDED:
2808                     title = res.getString(R.string.exceed_message_size_limitation, mediaType);
2809                     message = res.getString(R.string.failed_to_add_media, mediaType);
2810                     break;
2811                 case WorkingMessage.IMAGE_TOO_LARGE:
2812                     title = res.getString(R.string.failed_to_resize_image);
2813                     message = res.getString(R.string.resize_image_error_information);
2814                     break;
2815                 default:
2816                     throw new IllegalArgumentException("unknown error " + error);
2817                 }
2818 
2819                 MessageUtils.showErrorDialog(ComposeMessageActivity.this, title, message);
2820             }
2821         });
2822     }
2823 
addImage(Uri uri, boolean append)2824     private void addImage(Uri uri, boolean append) {
2825         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2826             log("append=" + append + ", uri=" + uri);
2827         }
2828 
2829         int result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, uri, append);
2830 
2831         if (result == WorkingMessage.IMAGE_TOO_LARGE ||
2832             result == WorkingMessage.MESSAGE_SIZE_EXCEEDED) {
2833             if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2834                 log("resize image " + uri);
2835             }
2836             MessageUtils.resizeImageAsync(this,
2837                     uri, mAttachmentEditorHandler, mResizeImageCallback, append);
2838             return;
2839         }
2840         handleAddAttachmentError(result, R.string.type_picture);
2841     }
2842 
addVideo(Uri uri, boolean append)2843     private void addVideo(Uri uri, boolean append) {
2844         if (uri != null) {
2845             int result = mWorkingMessage.setAttachment(WorkingMessage.VIDEO, uri, append);
2846             handleAddAttachmentError(result, R.string.type_video);
2847         }
2848     }
2849 
addAudio(Uri uri)2850     private void addAudio(Uri uri) {
2851         int result = mWorkingMessage.setAttachment(WorkingMessage.AUDIO, uri, false);
2852         handleAddAttachmentError(result, R.string.type_audio);
2853     }
2854 
handleForwardedMessage()2855     private boolean handleForwardedMessage() {
2856         Intent intent = getIntent();
2857 
2858         // If this is a forwarded message, it will have an Intent extra
2859         // indicating so.  If not, bail out.
2860         if (intent.getBooleanExtra("forwarded_message", false) == false) {
2861             return false;
2862         }
2863 
2864         Uri uri = intent.getParcelableExtra("msg_uri");
2865 
2866         if (Log.isLoggable(LogTag.APP, Log.DEBUG)) {
2867             log("" + uri);
2868         }
2869 
2870         if (uri != null) {
2871             mWorkingMessage = WorkingMessage.load(this, uri);
2872             mWorkingMessage.setSubject(intent.getStringExtra("subject"), false);
2873         } else {
2874             mWorkingMessage.setText(intent.getStringExtra("sms_body"));
2875         }
2876 
2877         // let's clear the message thread for forwarded messages
2878         mMsgListAdapter.changeCursor(null);
2879 
2880         return true;
2881     }
2882 
handleSendIntent(Intent intent)2883     private boolean handleSendIntent(Intent intent) {
2884         Bundle extras = intent.getExtras();
2885         if (extras == null) {
2886             return false;
2887         }
2888 
2889         final String mimeType = intent.getType();
2890         String action = intent.getAction();
2891         if (Intent.ACTION_SEND.equals(action)) {
2892             if (extras.containsKey(Intent.EXTRA_STREAM)) {
2893                 Uri uri = (Uri)extras.getParcelable(Intent.EXTRA_STREAM);
2894                 addAttachment(mimeType, uri, false);
2895                 return true;
2896             } else if (extras.containsKey(Intent.EXTRA_TEXT)) {
2897                 mWorkingMessage.setText(extras.getString(Intent.EXTRA_TEXT));
2898                 return true;
2899             }
2900         } else if (Intent.ACTION_SEND_MULTIPLE.equals(action) &&
2901                 extras.containsKey(Intent.EXTRA_STREAM)) {
2902             SlideshowModel slideShow = mWorkingMessage.getSlideshow();
2903             final ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
2904             int currentSlideCount = slideShow != null ? slideShow.size() : 0;
2905             int importCount = uris.size();
2906             if (importCount + currentSlideCount > SlideshowEditor.MAX_SLIDE_NUM) {
2907                 importCount = Math.min(SlideshowEditor.MAX_SLIDE_NUM - currentSlideCount,
2908                         importCount);
2909                 Toast.makeText(ComposeMessageActivity.this,
2910                         getString(R.string.too_many_attachments,
2911                                 SlideshowEditor.MAX_SLIDE_NUM, importCount),
2912                                 Toast.LENGTH_LONG).show();
2913             }
2914 
2915             // Attach all the pictures/videos off of the UI thread.
2916             // Show a progress alert if adding all the slides hasn't finished
2917             // within one second.
2918             // Stash the runnable for showing it away so we can cancel
2919             // it later if adding completes ahead of the deadline.
2920             final AlertDialog dialog = new AlertDialog.Builder(ComposeMessageActivity.this)
2921                 .setIcon(android.R.drawable.ic_dialog_alert)
2922                 .setTitle(R.string.adding_attachments_title)
2923                 .setMessage(R.string.adding_attachments)
2924                 .create();
2925             final Runnable showProgress = new Runnable() {
2926                 public void run() {
2927                     dialog.show();
2928                 }
2929             };
2930             // Schedule it for one second from now.
2931             mAttachmentEditorHandler.postDelayed(showProgress, 1000);
2932 
2933             final int numberToImport = importCount;
2934             new Thread(new Runnable() {
2935                 public void run() {
2936                     for (int i = 0; i < numberToImport; i++) {
2937                         Parcelable uri = uris.get(i);
2938                         addAttachment(mimeType, (Uri) uri, true);
2939                     }
2940                     // Cancel pending show of the progress alert if necessary.
2941                     mAttachmentEditorHandler.removeCallbacks(showProgress);
2942                     dialog.dismiss();
2943                 }
2944             }, "addAttachment").start();
2945             return true;
2946         }
2947 
2948         return false;
2949     }
2950 
2951     // mVideoUri will look like this: content://media/external/video/media
2952     private static final String mVideoUri = Video.Media.getContentUri("external").toString();
2953     // mImageUri will look like this: content://media/external/images/media
2954     private static final String mImageUri = Images.Media.getContentUri("external").toString();
2955 
addAttachment(String type, Uri uri, boolean append)2956     private void addAttachment(String type, Uri uri, boolean append) {
2957         if (uri != null) {
2958             // When we're handling Intent.ACTION_SEND_MULTIPLE, the passed in items can be
2959             // videos, and/or images, and/or some other unknown types we don't handle. When
2960             // a single attachment is "shared" the type will specify an image or video. When
2961             // there are multiple types, the type passed in is "*/*". In that case, we've got
2962             // to look at the uri to figure out if it is an image or video.
2963             boolean wildcard = "*/*".equals(type);
2964             if (type.startsWith("image/") || (wildcard && uri.toString().startsWith(mImageUri))) {
2965                 addImage(uri, append);
2966             } else if (type.startsWith("video/") ||
2967                     (wildcard && uri.toString().startsWith(mVideoUri))) {
2968                 addVideo(uri, append);
2969             }
2970         }
2971     }
2972 
getResourcesString(int id, String mediaName)2973     private String getResourcesString(int id, String mediaName) {
2974         Resources r = getResources();
2975         return r.getString(id, mediaName);
2976     }
2977 
drawBottomPanel()2978     private void drawBottomPanel() {
2979         // Reset the counter for text editor.
2980         resetCounter();
2981 
2982         if (mWorkingMessage.hasSlideshow()) {
2983             mBottomPanel.setVisibility(View.GONE);
2984             mAttachmentEditor.requestFocus();
2985             return;
2986         }
2987 
2988         mBottomPanel.setVisibility(View.VISIBLE);
2989 
2990         CharSequence text = mWorkingMessage.getText();
2991 
2992         // TextView.setTextKeepState() doesn't like null input.
2993         if (text != null) {
2994             mTextEditor.setTextKeepState(text);
2995         } else {
2996             mTextEditor.setText("");
2997         }
2998     }
2999 
drawTopPanel(boolean showSubjectEditor)3000     private void drawTopPanel(boolean showSubjectEditor) {
3001         boolean showingAttachment = mAttachmentEditor.update(mWorkingMessage);
3002         mAttachmentEditorScrollView.setVisibility(showingAttachment ? View.VISIBLE : View.GONE);
3003         showSubjectEditor(showSubjectEditor || mWorkingMessage.hasSubject());
3004     }
3005 
3006     //==========================================================
3007     // Interface methods
3008     //==========================================================
3009 
onClick(View v)3010     public void onClick(View v) {
3011         if ((v == mSendButtonSms || v == mSendButtonMms) && isPreparedForSending()) {
3012             confirmSendMessageIfNeeded();
3013         } else if ((v == mRecipientsPicker)) {
3014             launchMultiplePhonePicker();
3015         }
3016     }
3017 
launchMultiplePhonePicker()3018     private void launchMultiplePhonePicker() {
3019         Intent intent = new Intent(Intents.ACTION_GET_MULTIPLE_PHONES);
3020         intent.addCategory("android.intent.category.DEFAULT");
3021         intent.setType(Phone.CONTENT_TYPE);
3022         // We have to wait for the constructing complete.
3023         ContactList contacts = mRecipientsEditor.constructContactsFromInput(true);
3024         int recipientsCount = 0;
3025         int urisCount = 0;
3026         Uri[] uris = new Uri[contacts.size()];
3027         urisCount = 0;
3028         for (Contact contact : contacts) {
3029             if (Contact.CONTACT_METHOD_TYPE_PHONE == contact.getContactMethodType()) {
3030                     uris[urisCount++] = contact.getPhoneUri();
3031             }
3032         }
3033         if (urisCount > 0) {
3034             intent.putExtra(Intents.EXTRA_PHONE_URIS, uris);
3035         }
3036         startActivityForResult(intent, REQUEST_CODE_PICK);
3037     }
3038 
onEditorAction(TextView v, int actionId, KeyEvent event)3039     public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
3040         if (event != null) {
3041             // if shift key is down, then we want to insert the '\n' char in the TextView;
3042             // otherwise, the default action is to send the message.
3043             if (!event.isShiftPressed()) {
3044                 if (isPreparedForSending()) {
3045                     confirmSendMessageIfNeeded();
3046                 }
3047                 return true;
3048             }
3049             return false;
3050         }
3051 
3052         if (isPreparedForSending()) {
3053             confirmSendMessageIfNeeded();
3054         }
3055         return true;
3056     }
3057 
3058     private final TextWatcher mTextEditorWatcher = new TextWatcher() {
3059         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3060         }
3061 
3062         public void onTextChanged(CharSequence s, int start, int before, int count) {
3063             // This is a workaround for bug 1609057.  Since onUserInteraction() is
3064             // not called when the user touches the soft keyboard, we pretend it was
3065             // called when textfields changes.  This should be removed when the bug
3066             // is fixed.
3067             onUserInteraction();
3068 
3069             mWorkingMessage.setText(s);
3070 
3071             updateSendButtonState();
3072 
3073             updateCounter(s, start, before, count);
3074 
3075             ensureCorrectButtonHeight();
3076         }
3077 
3078         public void afterTextChanged(Editable s) {
3079         }
3080     };
3081 
3082     /**
3083      * Ensures that if the text edit box extends past two lines then the
3084      * button will be shifted up to allow enough space for the character
3085      * counter string to be placed beneath it.
3086      */
ensureCorrectButtonHeight()3087     private void ensureCorrectButtonHeight() {
3088         int currentTextLines = mTextEditor.getLineCount();
3089         if (currentTextLines <= 2) {
3090             mTextCounter.setVisibility(View.GONE);
3091         }
3092         else if (currentTextLines > 2 && mTextCounter.getVisibility() == View.GONE) {
3093             // Making the counter invisible ensures that it is used to correctly
3094             // calculate the position of the send button even if we choose not to
3095             // display the text.
3096             mTextCounter.setVisibility(View.INVISIBLE);
3097         }
3098     }
3099 
3100     private final TextWatcher mSubjectEditorWatcher = new TextWatcher() {
3101         public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
3102 
3103         public void onTextChanged(CharSequence s, int start, int before, int count) {
3104             mWorkingMessage.setSubject(s, true);
3105         }
3106 
3107         public void afterTextChanged(Editable s) { }
3108     };
3109 
3110     //==========================================================
3111     // Private methods
3112     //==========================================================
3113 
3114     /**
3115      * Initialize all UI elements from resources.
3116      */
initResourceRefs()3117     private void initResourceRefs() {
3118         mMsgListView = (MessageListView) findViewById(R.id.history);
3119         mMsgListView.setDivider(null);      // no divider so we look like IM conversation.
3120 
3121         // called to enable us to show some padding between the message list and the
3122         // input field but when the message list is scrolled that padding area is filled
3123         // in with message content
3124         mMsgListView.setClipToPadding(false);
3125 
3126         // turn off children clipping because we draw the border outside of our own
3127         // bounds at the bottom.  The background is also drawn in code to avoid drawing
3128         // the top edge.
3129         mMsgListView.setClipChildren(false);
3130 
3131         mBottomPanel = findViewById(R.id.bottom_panel);
3132         mTextEditor = (EditText) findViewById(R.id.embedded_text_editor);
3133         mTextEditor.setOnEditorActionListener(this);
3134         mTextEditor.addTextChangedListener(mTextEditorWatcher);
3135         mTextEditor.setFilters(new InputFilter[] {
3136                 new LengthFilter(MmsConfig.getMaxTextLimit())});
3137         mTextCounter = (TextView) findViewById(R.id.text_counter);
3138         mSendButtonMms = (TextView) findViewById(R.id.send_button_mms);
3139         mSendButtonSms = (ImageButton) findViewById(R.id.send_button_sms);
3140         mSendButtonMms.setOnClickListener(this);
3141         mSendButtonSms.setOnClickListener(this);
3142         mTopPanel = findViewById(R.id.recipients_subject_linear);
3143         mTopPanel.setFocusable(false);
3144         mAttachmentEditor = (AttachmentEditor) findViewById(R.id.attachment_editor);
3145         mAttachmentEditor.setHandler(mAttachmentEditorHandler);
3146         mAttachmentEditorScrollView = findViewById(R.id.attachment_editor_scroll_view);
3147     }
3148 
confirmDeleteDialog(OnClickListener listener, boolean locked)3149     private void confirmDeleteDialog(OnClickListener listener, boolean locked) {
3150         AlertDialog.Builder builder = new AlertDialog.Builder(this);
3151         builder.setTitle(locked ? R.string.confirm_dialog_locked_title :
3152             R.string.confirm_dialog_title);
3153         builder.setIcon(android.R.drawable.ic_dialog_alert);
3154         builder.setCancelable(true);
3155         builder.setMessage(locked ? R.string.confirm_delete_locked_message :
3156                     R.string.confirm_delete_message);
3157         builder.setPositiveButton(R.string.delete, listener);
3158         builder.setNegativeButton(R.string.no, null);
3159         builder.show();
3160     }
3161 
undeliveredMessageDialog(long date)3162     void undeliveredMessageDialog(long date) {
3163         String body;
3164         LinearLayout dialog = (LinearLayout) LayoutInflater.from(this).inflate(
3165                 R.layout.retry_sending_dialog, null);
3166 
3167         if (date >= 0) {
3168             body = getString(R.string.undelivered_msg_dialog_body,
3169                     MessageUtils.formatTimeStampString(this, date));
3170         } else {
3171             // FIXME: we can not get sms retry time.
3172             body = getString(R.string.undelivered_sms_dialog_body);
3173         }
3174 
3175         ((TextView) dialog.findViewById(R.id.body_text_view)).setText(body);
3176 
3177         Toast undeliveredDialog = new Toast(this);
3178         undeliveredDialog.setView(dialog);
3179         undeliveredDialog.setDuration(Toast.LENGTH_LONG);
3180         undeliveredDialog.show();
3181     }
3182 
startMsgListQuery()3183     private void startMsgListQuery() {
3184         Uri conversationUri = mConversation.getUri();
3185 
3186         if (conversationUri == null) {
3187             log("##### startMsgListQuery: conversationUri is null, bail!");
3188             return;
3189         }
3190 
3191         long threadId = mConversation.getThreadId();
3192         if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3193             log("startMsgListQuery for " + conversationUri + ", threadId=" + threadId);
3194         }
3195 
3196         // Cancel any pending queries
3197         mBackgroundQueryHandler.cancelOperation(MESSAGE_LIST_QUERY_TOKEN);
3198         try {
3199             // Kick off the new query
3200             mBackgroundQueryHandler.startQuery(
3201                     MESSAGE_LIST_QUERY_TOKEN,
3202                     threadId /* cookie */,
3203                     conversationUri,
3204                     PROJECTION,
3205                     null, null, null);
3206         } catch (SQLiteException e) {
3207             SqliteWrapper.checkSQLiteException(this, e);
3208         }
3209     }
3210 
initMessageList()3211     private void initMessageList() {
3212         if (mMsgListAdapter != null) {
3213             return;
3214         }
3215 
3216         String highlightString = getIntent().getStringExtra("highlight");
3217         Pattern highlight = highlightString == null
3218             ? null
3219             : Pattern.compile("\\b" + Pattern.quote(highlightString), Pattern.CASE_INSENSITIVE);
3220 
3221         // Initialize the list adapter with a null cursor.
3222         mMsgListAdapter = new MessageListAdapter(this, null, mMsgListView, true, highlight);
3223         mMsgListAdapter.setOnDataSetChangedListener(mDataSetChangedListener);
3224         mMsgListAdapter.setMsgListItemHandler(mMessageListItemHandler);
3225         mMsgListView.setAdapter(mMsgListAdapter);
3226         mMsgListView.setItemsCanFocus(false);
3227         mMsgListView.setVisibility(View.VISIBLE);
3228         mMsgListView.setOnCreateContextMenuListener(mMsgListMenuCreateListener);
3229         mMsgListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
3230             public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
3231                 if (view != null) {
3232                     ((MessageListItem) view).onMessageListItemClick();
3233                 }
3234             }
3235         });
3236     }
3237 
loadDraft()3238     private void loadDraft() {
3239         if (mWorkingMessage.isWorthSaving()) {
3240             Log.w(TAG, "called with non-empty working message");
3241             return;
3242         }
3243 
3244         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3245             log("call WorkingMessage.loadDraft");
3246         }
3247 
3248         mWorkingMessage = WorkingMessage.loadDraft(this, mConversation);
3249     }
3250 
saveDraft(boolean isStopping)3251     private void saveDraft(boolean isStopping) {
3252         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3253             LogTag.debug("saveDraft");
3254         }
3255         // TODO: Do something better here.  Maybe make discard() legal
3256         // to call twice and make isEmpty() return true if discarded
3257         // so it is caught in the clause above this one?
3258         if (mWorkingMessage.isDiscarded()) {
3259             return;
3260         }
3261 
3262         if (!mWaitingForSubActivity &&
3263                 !mWorkingMessage.isWorthSaving() &&
3264                 (!isRecipientsEditorVisible() || recipientCount() == 0)) {
3265             if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3266                 log("not worth saving, discard WorkingMessage and bail");
3267             }
3268             mWorkingMessage.discard();
3269             return;
3270         }
3271 
3272         mWorkingMessage.saveDraft(isStopping);
3273 
3274         if (mToastForDraftSave) {
3275             Toast.makeText(this, R.string.message_saved_as_draft,
3276                     Toast.LENGTH_SHORT).show();
3277         }
3278     }
3279 
isPreparedForSending()3280     private boolean isPreparedForSending() {
3281         int recipientCount = recipientCount();
3282 
3283         return recipientCount > 0 && recipientCount <= MmsConfig.getRecipientLimit() &&
3284             (mWorkingMessage.hasAttachment() ||
3285                     mWorkingMessage.hasText() ||
3286                     mWorkingMessage.hasSubject());
3287     }
3288 
recipientCount()3289     private int recipientCount() {
3290         int recipientCount;
3291 
3292         // To avoid creating a bunch of invalid Contacts when the recipients
3293         // editor is in flux, we keep the recipients list empty.  So if the
3294         // recipients editor is showing, see if there is anything in it rather
3295         // than consulting the empty recipient list.
3296         if (isRecipientsEditorVisible()) {
3297             recipientCount = mRecipientsEditor.getRecipientCount();
3298         } else {
3299             recipientCount = getRecipients().size();
3300         }
3301         return recipientCount;
3302     }
3303 
sendMessage(boolean bCheckEcmMode)3304     private void sendMessage(boolean bCheckEcmMode) {
3305         if (bCheckEcmMode) {
3306             // TODO: expose this in telephony layer for SDK build
3307             String inEcm = SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE);
3308             if (Boolean.parseBoolean(inEcm)) {
3309                 try {
3310                     startActivityForResult(
3311                             new Intent(TelephonyIntents.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS, null),
3312                             REQUEST_CODE_ECM_EXIT_DIALOG);
3313                     return;
3314                 } catch (ActivityNotFoundException e) {
3315                     // continue to send message
3316                     Log.e(TAG, "Cannot find EmergencyCallbackModeExitDialog", e);
3317                 }
3318             }
3319         }
3320 
3321         if (!mSendingMessage) {
3322             if (LogTag.SEVERE_WARNING) {
3323                 String sendingRecipients = mConversation.getRecipients().serialize();
3324                 if (!sendingRecipients.equals(mDebugRecipients)) {
3325                     String workingRecipients = mWorkingMessage.getWorkingRecipients();
3326                     if (!mDebugRecipients.equals(workingRecipients)) {
3327                         LogTag.warnPossibleRecipientMismatch("ComposeMessageActivity.sendMessage" +
3328                                 " recipients in window: \"" +
3329                                 mDebugRecipients + "\" differ from recipients from conv: \"" +
3330                                 sendingRecipients + "\" and working recipients: " +
3331                                 workingRecipients, this);
3332                     }
3333                 }
3334                 sanityCheckConversation();
3335             }
3336 
3337             // send can change the recipients. Make sure we remove the listeners first and then add
3338             // them back once the recipient list has settled.
3339             removeRecipientsListeners();
3340 
3341             mWorkingMessage.send(mDebugRecipients);
3342 
3343             mSentMessage = true;
3344             mSendingMessage = true;
3345             addRecipientsListeners();
3346         }
3347         // But bail out if we are supposed to exit after the message is sent.
3348         if (mExitOnSent) {
3349             finish();
3350         }
3351     }
3352 
resetMessage()3353     private void resetMessage() {
3354         if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3355             log("");
3356         }
3357 
3358         // Make the attachment editor hide its view.
3359         mAttachmentEditor.hideView();
3360         mAttachmentEditorScrollView.setVisibility(View.GONE);
3361 
3362         // Hide the subject editor.
3363         showSubjectEditor(false);
3364 
3365         // Focus to the text editor.
3366         mTextEditor.requestFocus();
3367 
3368         // We have to remove the text change listener while the text editor gets cleared and
3369         // we subsequently turn the message back into SMS. When the listener is listening while
3370         // doing the clearing, it's fighting to update its counts and itself try and turn
3371         // the message one way or the other.
3372         mTextEditor.removeTextChangedListener(mTextEditorWatcher);
3373 
3374         // Clear the text box.
3375         TextKeyListener.clear(mTextEditor.getText());
3376 
3377         mWorkingMessage.clearConversation(mConversation, false);
3378         mWorkingMessage = WorkingMessage.createEmpty(this);
3379         mWorkingMessage.setConversation(mConversation);
3380 
3381         hideRecipientEditor();
3382         drawBottomPanel();
3383 
3384         // "Or not", in this case.
3385         updateSendButtonState();
3386 
3387         // Our changes are done. Let the listener respond to text changes once again.
3388         mTextEditor.addTextChangedListener(mTextEditorWatcher);
3389 
3390         // Close the soft on-screen keyboard if we're in landscape mode so the user can see the
3391         // conversation.
3392         if (mIsLandscape) {
3393             InputMethodManager inputMethodManager =
3394                 (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
3395 
3396             inputMethodManager.hideSoftInputFromWindow(mTextEditor.getWindowToken(), 0);
3397         }
3398 
3399         mLastRecipientCount = 0;
3400         mSendingMessage = false;
3401    }
3402 
updateSendButtonState()3403     private void updateSendButtonState() {
3404         boolean enable = false;
3405         if (isPreparedForSending()) {
3406             // When the type of attachment is slideshow, we should
3407             // also hide the 'Send' button since the slideshow view
3408             // already has a 'Send' button embedded.
3409             if (!mWorkingMessage.hasSlideshow()) {
3410                 enable = true;
3411             } else {
3412                 mAttachmentEditor.setCanSend(true);
3413             }
3414         } else if (null != mAttachmentEditor){
3415             mAttachmentEditor.setCanSend(false);
3416         }
3417 
3418         View sendButton = showSmsOrMmsSendButton(mWorkingMessage.requiresMms());
3419         sendButton.setEnabled(enable);
3420         sendButton.setFocusable(enable);
3421     }
3422 
getMessageDate(Uri uri)3423     private long getMessageDate(Uri uri) {
3424         if (uri != null) {
3425             Cursor cursor = SqliteWrapper.query(this, mContentResolver,
3426                     uri, new String[] { Mms.DATE }, null, null, null);
3427             if (cursor != null) {
3428                 try {
3429                     if ((cursor.getCount() == 1) && cursor.moveToFirst()) {
3430                         return cursor.getLong(0) * 1000L;
3431                     }
3432                 } finally {
3433                     cursor.close();
3434                 }
3435             }
3436         }
3437         return NO_DATE_FOR_DIALOG;
3438     }
3439 
initActivityState(Intent intent)3440     private void initActivityState(Intent intent) {
3441         // If we have been passed a thread_id, use that to find our conversation.
3442         long threadId = intent.getLongExtra("thread_id", 0);
3443         if (threadId > 0) {
3444             if (LogTag.VERBOSE) log("get mConversation by threadId " + threadId);
3445             mConversation = Conversation.get(this, threadId, false);
3446         } else {
3447             Uri intentData = intent.getData();
3448             if (intentData != null) {
3449                 // try to get a conversation based on the data URI passed to our intent.
3450                 if (LogTag.VERBOSE) log("get mConversation by intentData " + intentData);
3451                 mConversation = Conversation.get(this, intentData, false);
3452                 mWorkingMessage.setText(getBody(intentData));
3453             } else {
3454                 // special intent extra parameter to specify the address
3455                 String address = intent.getStringExtra("address");
3456                 if (!TextUtils.isEmpty(address)) {
3457                     if (LogTag.VERBOSE) log("get mConversation by address " + address);
3458                     mConversation = Conversation.get(this, ContactList.getByNumbers(address,
3459                             false /* don't block */, true /* replace number */), false);
3460                 } else {
3461                     if (LogTag.VERBOSE) log("create new conversation");
3462                     mConversation = Conversation.createNew(this);
3463                 }
3464             }
3465         }
3466         addRecipientsListeners();
3467 
3468         mExitOnSent = intent.getBooleanExtra("exit_on_sent", false);
3469         if (intent.hasExtra("sms_body")) {
3470             mWorkingMessage.setText(intent.getStringExtra("sms_body"));
3471         }
3472         mWorkingMessage.setSubject(intent.getStringExtra("subject"), false);
3473     }
3474 
initFocus()3475     private void initFocus() {
3476         if (!mIsKeyboardOpen) {
3477             return;
3478         }
3479 
3480         // If the recipients editor is visible, there is nothing in it,
3481         // and the text editor is not already focused, focus the
3482         // recipients editor.
3483         if (isRecipientsEditorVisible()
3484                 && TextUtils.isEmpty(mRecipientsEditor.getText())
3485                 && !mTextEditor.isFocused()) {
3486             mRecipientsEditor.requestFocus();
3487             return;
3488         }
3489 
3490         // If we decided not to focus the recipients editor, focus the text editor.
3491         mTextEditor.requestFocus();
3492     }
3493 
3494     private final MessageListAdapter.OnDataSetChangedListener
3495                     mDataSetChangedListener = new MessageListAdapter.OnDataSetChangedListener() {
3496         public void onDataSetChanged(MessageListAdapter adapter) {
3497             mPossiblePendingNotification = true;
3498         }
3499 
3500         public void onContentChanged(MessageListAdapter adapter) {
3501             if (LogTag.VERBOSE) {
3502                 log("MessageListAdapter.OnDataSetChangedListener.onContentChanged");
3503             }
3504             startMsgListQuery();
3505         }
3506     };
3507 
checkPendingNotification()3508     private void checkPendingNotification() {
3509         if (mPossiblePendingNotification && hasWindowFocus()) {
3510             mConversation.markAsRead();
3511             mPossiblePendingNotification = false;
3512         }
3513     }
3514 
3515     private final class BackgroundQueryHandler extends AsyncQueryHandler {
BackgroundQueryHandler(ContentResolver contentResolver)3516         public BackgroundQueryHandler(ContentResolver contentResolver) {
3517             super(contentResolver);
3518         }
3519 
3520         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)3521         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
3522             switch(token) {
3523                 case MESSAGE_LIST_QUERY_TOKEN:
3524                     // check consistency between the query result and 'mConversation'
3525                     long tid = (Long) cookie;
3526 
3527                     if (LogTag.VERBOSE) {
3528                         log("##### onQueryComplete: msg history result for threadId " + tid);
3529                     }
3530                     if (tid != mConversation.getThreadId()) {
3531                         log("onQueryComplete: msg history query result is for threadId " +
3532                                 tid + ", but mConversation has threadId " +
3533                                 mConversation.getThreadId() + " starting a new query");
3534                         startMsgListQuery();
3535                         return;
3536                     }
3537 
3538                     // check consistency b/t mConversation & mWorkingMessage.mConversation
3539                     ComposeMessageActivity.this.sanityCheckConversation();
3540 
3541                     int newSelectionPos = -1;
3542                     long targetMsgId = getIntent().getLongExtra("select_id", -1);
3543                     if (targetMsgId != -1) {
3544                         cursor.moveToPosition(-1);
3545                         while (cursor.moveToNext()) {
3546                             long msgId = cursor.getLong(COLUMN_ID);
3547                             if (msgId == targetMsgId) {
3548                                 newSelectionPos = cursor.getPosition();
3549                                 break;
3550                             }
3551                         }
3552                     }
3553 
3554                     mMsgListAdapter.changeCursor(cursor);
3555                     if (newSelectionPos != -1) {
3556                         mMsgListView.setSelection(newSelectionPos);
3557                     }
3558                     // Adjust the conversation's message count to match reality. The
3559                     // conversation's message count is eventually used in
3560                     // WorkingMessage.clearConversation to determine whether to delete
3561                     // the conversation or not.
3562                     mConversation.setMessageCount(mMsgListAdapter.getCount());
3563 
3564                     // Once we have completed the query for the message history, if
3565                     // there is nothing in the cursor and we are not composing a new
3566                     // message, we must be editing a draft in a new conversation (unless
3567                     // mSentMessage is true).
3568                     // Show the recipients editor to give the user a chance to add
3569                     // more people before the conversation begins.
3570                     if (cursor.getCount() == 0 && !isRecipientsEditorVisible() && !mSentMessage) {
3571                         initRecipientsEditor();
3572                     }
3573 
3574                     // FIXME: freshing layout changes the focused view to an unexpected
3575                     // one, set it back to TextEditor forcely.
3576                     mTextEditor.requestFocus();
3577 
3578                     mConversation.blockMarkAsRead(false);
3579                     return;
3580 
3581                 case ConversationList.HAVE_LOCKED_MESSAGES_TOKEN:
3582                     ArrayList<Long> threadIds = (ArrayList<Long>)cookie;
3583                     ConversationList.confirmDeleteThreadDialog(
3584                             new ConversationList.DeleteThreadListener(threadIds,
3585                                 mBackgroundQueryHandler, ComposeMessageActivity.this),
3586                             threadIds,
3587                             cursor != null && cursor.getCount() > 0,
3588                             ComposeMessageActivity.this);
3589                     break;
3590             }
3591         }
3592 
3593         @Override
onDeleteComplete(int token, Object cookie, int result)3594         protected void onDeleteComplete(int token, Object cookie, int result) {
3595             switch(token) {
3596                 case ConversationList.DELETE_CONVERSATION_TOKEN:
3597                     mConversation.setMessageCount(0);
3598                     // fall through
3599                 case DELETE_MESSAGE_TOKEN:
3600                     // Update the notification for new messages since they
3601                     // may be deleted.
3602                     MessagingNotification.nonBlockingUpdateNewMessageIndicator(
3603                             ComposeMessageActivity.this, false, false);
3604                     // Update the notification for failed messages since they
3605                     // may be deleted.
3606                     updateSendFailedNotification();
3607                     break;
3608             }
3609             // If we're deleting the whole conversation, throw away
3610             // our current working message and bail.
3611             if (token == ConversationList.DELETE_CONVERSATION_TOKEN) {
3612                 mWorkingMessage.discard();
3613 
3614                 // Rebuild the contacts cache now that a thread and its associated unique
3615                 // recipients have been deleted.
3616                 Contact.init(ComposeMessageActivity.this);
3617 
3618                 // Make sure the conversation cache reflects the threads in the DB.
3619                 Conversation.init(ComposeMessageActivity.this);
3620                 finish();
3621             }
3622         }
3623     }
3624 
showSmileyDialog()3625     private void showSmileyDialog() {
3626         if (mSmileyDialog == null) {
3627             int[] icons = SmileyParser.DEFAULT_SMILEY_RES_IDS;
3628             String[] names = getResources().getStringArray(
3629                     SmileyParser.DEFAULT_SMILEY_NAMES);
3630             final String[] texts = getResources().getStringArray(
3631                     SmileyParser.DEFAULT_SMILEY_TEXTS);
3632 
3633             final int N = names.length;
3634 
3635             List<Map<String, ?>> entries = new ArrayList<Map<String, ?>>();
3636             for (int i = 0; i < N; i++) {
3637                 // We might have different ASCII for the same icon, skip it if
3638                 // the icon is already added.
3639                 boolean added = false;
3640                 for (int j = 0; j < i; j++) {
3641                     if (icons[i] == icons[j]) {
3642                         added = true;
3643                         break;
3644                     }
3645                 }
3646                 if (!added) {
3647                     HashMap<String, Object> entry = new HashMap<String, Object>();
3648 
3649                     entry. put("icon", icons[i]);
3650                     entry. put("name", names[i]);
3651                     entry.put("text", texts[i]);
3652 
3653                     entries.add(entry);
3654                 }
3655             }
3656 
3657             final SimpleAdapter a = new SimpleAdapter(
3658                     this,
3659                     entries,
3660                     R.layout.smiley_menu_item,
3661                     new String[] {"icon", "name", "text"},
3662                     new int[] {R.id.smiley_icon, R.id.smiley_name, R.id.smiley_text});
3663             SimpleAdapter.ViewBinder viewBinder = new SimpleAdapter.ViewBinder() {
3664                 public boolean setViewValue(View view, Object data, String textRepresentation) {
3665                     if (view instanceof ImageView) {
3666                         Drawable img = getResources().getDrawable((Integer)data);
3667                         ((ImageView)view).setImageDrawable(img);
3668                         return true;
3669                     }
3670                     return false;
3671                 }
3672             };
3673             a.setViewBinder(viewBinder);
3674 
3675             AlertDialog.Builder b = new AlertDialog.Builder(this);
3676 
3677             b.setTitle(getString(R.string.menu_insert_smiley));
3678 
3679             b.setCancelable(true);
3680             b.setAdapter(a, new DialogInterface.OnClickListener() {
3681                 @SuppressWarnings("unchecked")
3682                 public final void onClick(DialogInterface dialog, int which) {
3683                     HashMap<String, Object> item = (HashMap<String, Object>) a.getItem(which);
3684 
3685                     String smiley = (String)item.get("text");
3686                     if (mSubjectTextEditor != null && mSubjectTextEditor.hasFocus()) {
3687                         mSubjectTextEditor.append(smiley);
3688                     } else {
3689                         mTextEditor.append(smiley);
3690                     }
3691 
3692                     dialog.dismiss();
3693                 }
3694             });
3695 
3696             mSmileyDialog = b.create();
3697         }
3698 
3699         mSmileyDialog.show();
3700     }
3701 
onUpdate(final Contact updated)3702     public void onUpdate(final Contact updated) {
3703         // Using an existing handler for the post, rather than conjuring up a new one.
3704         mMessageListItemHandler.post(new Runnable() {
3705             public void run() {
3706                 ContactList recipients = isRecipientsEditorVisible() ?
3707                         mRecipientsEditor.constructContactsFromInput(false) : getRecipients();
3708                 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3709                     log("[CMA] onUpdate contact updated: " + updated);
3710                     log("[CMA] onUpdate recipients: " + recipients);
3711                 }
3712                 updateTitle(recipients);
3713 
3714                 // The contact information for one (or more) of the recipients has changed.
3715                 // Rebuild the message list so each MessageItem will get the last contact info.
3716                 ComposeMessageActivity.this.mMsgListAdapter.notifyDataSetChanged();
3717 
3718                 if (mRecipientsEditor != null) {
3719                     mRecipientsEditor.populate(recipients);
3720                 }
3721             }
3722         });
3723     }
3724 
addRecipientsListeners()3725     private void addRecipientsListeners() {
3726         Contact.addListener(this);
3727     }
3728 
removeRecipientsListeners()3729     private void removeRecipientsListeners() {
3730         Contact.removeListener(this);
3731     }
3732 
createIntent(Context context, long threadId)3733     public static Intent createIntent(Context context, long threadId) {
3734         Intent intent = new Intent(context, ComposeMessageActivity.class);
3735 
3736         if (threadId > 0) {
3737             intent.setData(Conversation.getUri(threadId));
3738         }
3739 
3740         return intent;
3741    }
3742 
getBody(Uri uri)3743     private String getBody(Uri uri) {
3744         if (uri == null) {
3745             return null;
3746         }
3747         String urlStr = uri.getSchemeSpecificPart();
3748         if (!urlStr.contains("?")) {
3749             return null;
3750         }
3751         urlStr = urlStr.substring(urlStr.indexOf('?') + 1);
3752         String[] params = urlStr.split("&");
3753         for (String p : params) {
3754             if (p.startsWith("body=")) {
3755                 try {
3756                     return URLDecoder.decode(p.substring(5), "UTF-8");
3757                 } catch (UnsupportedEncodingException e) { }
3758             }
3759         }
3760         return null;
3761     }
3762 }
3763