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