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