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