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