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