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