ComposeMessageActivity.java revision b1b7ee90078a41caf0508dd13df2416bfce8e023
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.IMAGE:
427                            case WorkingMessage.VIDEO:
428                            case WorkingMessage.AUDIO:
429                            case WorkingMessage.SLIDESHOW:
430                                MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this,
431                                        msgItem.mMessageUri, msgItem.mSlideshow,
432                                        getAsyncDialog());
433                                break;
434                        }
435                        break;
436
437                    default:
438                        Log.w(TAG, "Unknown message: " + msg.what);
439                        return;
440                }
441            }
442        }
443    };
444
445    private final OnKeyListener mSubjectKeyListener = new OnKeyListener() {
446        @Override
447        public boolean onKey(View v, int keyCode, KeyEvent event) {
448            if (event.getAction() != KeyEvent.ACTION_DOWN) {
449                return false;
450            }
451
452            // When the subject editor is empty, press "DEL" to hide the input field.
453            if ((keyCode == KeyEvent.KEYCODE_DEL) && (mSubjectTextEditor.length() == 0)) {
454                showSubjectEditor(false);
455                mWorkingMessage.setSubject(null, true);
456                return true;
457            }
458            return false;
459        }
460    };
461
462    /**
463     * Return the messageItem associated with the type ("mms" or "sms") and message id.
464     * @param type Type of the message: "mms" or "sms"
465     * @param msgId Message id of the message. This is the _id of the sms or pdu row and is
466     * stored in the MessageItem
467     * @param createFromCursorIfNotInCache true if the item is not found in the MessageListAdapter's
468     * cache and the code can create a new MessageItem based on the position of the current cursor.
469     * If false, the function returns null if the MessageItem isn't in the cache.
470     * @return MessageItem or null if not found and createFromCursorIfNotInCache is false
471     */
472    private MessageItem getMessageItem(String type, long msgId,
473            boolean createFromCursorIfNotInCache) {
474        return mMsgListAdapter.getCachedMessageItem(type, msgId,
475                createFromCursorIfNotInCache ? mMsgListAdapter.getCursor() : null);
476    }
477
478    private boolean isCursorValid() {
479        // Check whether the cursor is valid or not.
480        Cursor cursor = mMsgListAdapter.getCursor();
481        if (cursor.isClosed() || cursor.isBeforeFirst() || cursor.isAfterLast()) {
482            Log.e(TAG, "Bad cursor.", new RuntimeException());
483            return false;
484        }
485        return true;
486    }
487
488    private void resetCounter() {
489        mTextCounter.setText("");
490        mTextCounter.setVisibility(View.GONE);
491    }
492
493    private void updateCounter(CharSequence text, int start, int before, int count) {
494        WorkingMessage workingMessage = mWorkingMessage;
495        if (workingMessage.requiresMms()) {
496            // If we're not removing text (i.e. no chance of converting back to SMS
497            // because of this change) and we're in MMS mode, just bail out since we
498            // then won't have to calculate the length unnecessarily.
499            final boolean textRemoved = (before > count);
500            if (!textRemoved) {
501                showSmsOrMmsSendButton(workingMessage.requiresMms());
502                return;
503            }
504        }
505
506        int[] params = SmsMessage.calculateLength(text, false);
507            /* SmsMessage.calculateLength returns an int[4] with:
508             *   int[0] being the number of SMS's required,
509             *   int[1] the number of code units used,
510             *   int[2] is the number of code units remaining until the next message.
511             *   int[3] is the encoding type that should be used for the message.
512             */
513        int msgCount = params[0];
514        int remainingInCurrentMessage = params[2];
515
516        if (!MmsConfig.getMultipartSmsEnabled()) {
517            // The provider doesn't support multi-part sms's so as soon as the user types
518            // an sms longer than one segment, we have to turn the message into an mms.
519            mWorkingMessage.setLengthRequiresMms(msgCount > 1, true);
520        }
521
522        // Show the counter only if:
523        // - We are not in MMS mode
524        // - We are going to send more than one message OR we are getting close
525        boolean showCounter = false;
526        if (!workingMessage.requiresMms() &&
527                (msgCount > 1 ||
528                 remainingInCurrentMessage <= CHARS_REMAINING_BEFORE_COUNTER_SHOWN)) {
529            showCounter = true;
530        }
531
532        showSmsOrMmsSendButton(workingMessage.requiresMms());
533
534        if (showCounter) {
535            // Update the remaining characters and number of messages required.
536            String counterText = msgCount > 1 ? remainingInCurrentMessage + " / " + msgCount
537                    : String.valueOf(remainingInCurrentMessage);
538            mTextCounter.setText(counterText);
539            mTextCounter.setVisibility(View.VISIBLE);
540        } else {
541            mTextCounter.setVisibility(View.GONE);
542        }
543    }
544
545    @Override
546    public void startActivityForResult(Intent intent, int requestCode)
547    {
548        // requestCode >= 0 means the activity in question is a sub-activity.
549        if (requestCode >= 0) {
550            mWaitingForSubActivity = true;
551        }
552
553        super.startActivityForResult(intent, requestCode);
554    }
555
556    private void toastConvertInfo(boolean toMms) {
557        final int resId = toMms ? R.string.converting_to_picture_message
558                : R.string.converting_to_text_message;
559        Toast.makeText(this, resId, Toast.LENGTH_SHORT).show();
560    }
561
562    private class DeleteMessageListener implements OnClickListener {
563        private final Uri mDeleteUri;
564        private final boolean mDeleteLocked;
565
566        public DeleteMessageListener(Uri uri, boolean deleteLocked) {
567            mDeleteUri = uri;
568            mDeleteLocked = deleteLocked;
569        }
570
571        public DeleteMessageListener(long msgId, String type, boolean deleteLocked) {
572            if ("mms".equals(type)) {
573                mDeleteUri = ContentUris.withAppendedId(Mms.CONTENT_URI, msgId);
574            } else {
575                mDeleteUri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgId);
576            }
577            mDeleteLocked = deleteLocked;
578        }
579
580        @Override
581        public void onClick(DialogInterface dialog, int whichButton) {
582            PduCache.getInstance().purge(mDeleteUri);
583            mBackgroundQueryHandler.startDelete(DELETE_MESSAGE_TOKEN,
584                    null, mDeleteUri, mDeleteLocked ? null : "locked=0", null);
585            dialog.dismiss();
586        }
587    }
588
589    private class DiscardDraftListener implements OnClickListener {
590        @Override
591        public void onClick(DialogInterface dialog, int whichButton) {
592            mWorkingMessage.discard();
593            dialog.dismiss();
594            finish();
595        }
596    }
597
598    private class SendIgnoreInvalidRecipientListener implements OnClickListener {
599        @Override
600        public void onClick(DialogInterface dialog, int whichButton) {
601            sendMessage(true);
602            dialog.dismiss();
603        }
604    }
605
606    private class CancelSendingListener implements OnClickListener {
607        @Override
608        public void onClick(DialogInterface dialog, int whichButton) {
609            if (isRecipientsEditorVisible()) {
610                mRecipientsEditor.requestFocus();
611            }
612            dialog.dismiss();
613        }
614    }
615
616    private void confirmSendMessageIfNeeded() {
617        if (!isRecipientsEditorVisible()) {
618            sendMessage(true);
619            return;
620        }
621
622        boolean isMms = mWorkingMessage.requiresMms();
623        if (mRecipientsEditor.hasInvalidRecipient(isMms)) {
624            if (mRecipientsEditor.hasValidRecipient(isMms)) {
625                String title = getResourcesString(R.string.has_invalid_recipient,
626                        mRecipientsEditor.formatInvalidNumbers(isMms));
627                new AlertDialog.Builder(this)
628                    .setTitle(title)
629                    .setMessage(R.string.invalid_recipient_message)
630                    .setPositiveButton(R.string.try_to_send,
631                            new SendIgnoreInvalidRecipientListener())
632                    .setNegativeButton(R.string.no, new CancelSendingListener())
633                    .show();
634            } else {
635                new AlertDialog.Builder(this)
636                    .setTitle(R.string.cannot_send_message)
637                    .setMessage(R.string.cannot_send_message_reason)
638                    .setPositiveButton(R.string.yes, new CancelSendingListener())
639                    .show();
640            }
641        } else {
642            sendMessage(true);
643        }
644    }
645
646    private final TextWatcher mRecipientsWatcher = new TextWatcher() {
647        @Override
648        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
649        }
650
651        @Override
652        public void onTextChanged(CharSequence s, int start, int before, int count) {
653            // This is a workaround for bug 1609057.  Since onUserInteraction() is
654            // not called when the user touches the soft keyboard, we pretend it was
655            // called when textfields changes.  This should be removed when the bug
656            // is fixed.
657            onUserInteraction();
658        }
659
660        @Override
661        public void afterTextChanged(Editable s) {
662            // Bug 1474782 describes a situation in which we send to
663            // the wrong recipient.  We have been unable to reproduce this,
664            // but the best theory we have so far is that the contents of
665            // mRecipientList somehow become stale when entering
666            // ComposeMessageActivity via onNewIntent().  This assertion is
667            // meant to catch one possible path to that, of a non-visible
668            // mRecipientsEditor having its TextWatcher fire and refreshing
669            // mRecipientList with its stale contents.
670            if (!isRecipientsEditorVisible()) {
671                IllegalStateException e = new IllegalStateException(
672                        "afterTextChanged called with invisible mRecipientsEditor");
673                // Make sure the crash is uploaded to the service so we
674                // can see if this is happening in the field.
675                Log.w(TAG,
676                     "RecipientsWatcher: afterTextChanged called with invisible mRecipientsEditor");
677                return;
678            }
679
680            mWorkingMessage.setWorkingRecipients(mRecipientsEditor.getNumbers());
681            mWorkingMessage.setHasEmail(mRecipientsEditor.containsEmail(), true);
682
683            checkForTooManyRecipients();
684
685            // Walk backwards in the text box, skipping spaces.  If the last
686            // character is a comma, update the title bar.
687            for (int pos = s.length() - 1; pos >= 0; pos--) {
688                char c = s.charAt(pos);
689                if (c == ' ')
690                    continue;
691
692                if (c == ',') {
693                    updateTitle(mConversation.getRecipients());
694                }
695
696                break;
697            }
698
699            // If we have gone to zero recipients, disable send button.
700            updateSendButtonState();
701        }
702    };
703
704    private void checkForTooManyRecipients() {
705        final int recipientLimit = MmsConfig.getRecipientLimit();
706        if (recipientLimit != Integer.MAX_VALUE) {
707            final int recipientCount = recipientCount();
708            boolean tooMany = recipientCount > recipientLimit;
709
710            if (recipientCount != mLastRecipientCount) {
711                // Don't warn the user on every character they type when they're over the limit,
712                // only when the actual # of recipients changes.
713                mLastRecipientCount = recipientCount;
714                if (tooMany) {
715                    String tooManyMsg = getString(R.string.too_many_recipients, recipientCount,
716                            recipientLimit);
717                    Toast.makeText(ComposeMessageActivity.this,
718                            tooManyMsg, Toast.LENGTH_LONG).show();
719                }
720            }
721        }
722    }
723
724    private final OnCreateContextMenuListener mRecipientsMenuCreateListener =
725        new OnCreateContextMenuListener() {
726        @Override
727        public void onCreateContextMenu(ContextMenu menu, View v,
728                ContextMenuInfo menuInfo) {
729            if (menuInfo != null) {
730                Contact c = ((RecipientContextMenuInfo) menuInfo).recipient;
731                RecipientsMenuClickListener l = new RecipientsMenuClickListener(c);
732
733                menu.setHeaderTitle(c.getName());
734
735                if (c.existsInDatabase()) {
736                    menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact)
737                            .setOnMenuItemClickListener(l);
738                } else if (canAddToContacts(c)){
739                    menu.add(0, MENU_ADD_TO_CONTACTS, 0, R.string.menu_add_to_contacts)
740                            .setOnMenuItemClickListener(l);
741                }
742            }
743        }
744    };
745
746    private final class RecipientsMenuClickListener implements MenuItem.OnMenuItemClickListener {
747        private final Contact mRecipient;
748
749        RecipientsMenuClickListener(Contact recipient) {
750            mRecipient = recipient;
751        }
752
753        @Override
754        public boolean onMenuItemClick(MenuItem item) {
755            switch (item.getItemId()) {
756                // Context menu handlers for the recipients editor.
757                case MENU_VIEW_CONTACT: {
758                    Uri contactUri = mRecipient.getUri();
759                    Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
760                    intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
761                    startActivity(intent);
762                    return true;
763                }
764                case MENU_ADD_TO_CONTACTS: {
765                    mAddContactIntent = ConversationList.createAddContactIntent(
766                            mRecipient.getNumber());
767                    ComposeMessageActivity.this.startActivityForResult(mAddContactIntent,
768                            REQUEST_CODE_ADD_CONTACT);
769                    return true;
770                }
771            }
772            return false;
773        }
774    }
775
776    private boolean canAddToContacts(Contact contact) {
777        // There are some kind of automated messages, like STK messages, that we don't want
778        // to add to contacts. These names begin with special characters, like, "*Info".
779        final String name = contact.getName();
780        if (!TextUtils.isEmpty(contact.getNumber())) {
781            char c = contact.getNumber().charAt(0);
782            if (isSpecialChar(c)) {
783                return false;
784            }
785        }
786        if (!TextUtils.isEmpty(name)) {
787            char c = name.charAt(0);
788            if (isSpecialChar(c)) {
789                return false;
790            }
791        }
792        if (!(Mms.isEmailAddress(name) ||
793                AddressUtils.isPossiblePhoneNumber(name) ||
794                contact.isMe())) {
795            return false;
796        }
797        return true;
798    }
799
800    private boolean isSpecialChar(char c) {
801        return c == '*' || c == '%' || c == '$';
802    }
803
804    private void addPositionBasedMenuItems(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
805        AdapterView.AdapterContextMenuInfo info;
806
807        try {
808            info = (AdapterView.AdapterContextMenuInfo) menuInfo;
809        } catch (ClassCastException e) {
810            Log.e(TAG, "bad menuInfo");
811            return;
812        }
813        final int position = info.position;
814
815        addUriSpecificMenuItems(menu, v, position);
816    }
817
818    private Uri getSelectedUriFromMessageList(ListView listView, int position) {
819        // If the context menu was opened over a uri, get that uri.
820        MessageListItem msglistItem = (MessageListItem) listView.getChildAt(position);
821        if (msglistItem == null) {
822            // FIXME: Should get the correct view. No such interface in ListView currently
823            // to get the view by position. The ListView.getChildAt(position) cannot
824            // get correct view since the list doesn't create one child for each item.
825            // And if setSelection(position) then getSelectedView(),
826            // cannot get corrent view when in touch mode.
827            return null;
828        }
829
830        TextView textView;
831        CharSequence text = null;
832        int selStart = -1;
833        int selEnd = -1;
834
835        //check if message sender is selected
836        textView = (TextView) msglistItem.findViewById(R.id.text_view);
837        if (textView != null) {
838            text = textView.getText();
839            selStart = textView.getSelectionStart();
840            selEnd = textView.getSelectionEnd();
841        }
842
843        // Check that some text is actually selected, rather than the cursor
844        // just being placed within the TextView.
845        if (selStart != selEnd) {
846            int min = Math.min(selStart, selEnd);
847            int max = Math.max(selStart, selEnd);
848
849            URLSpan[] urls = ((Spanned) text).getSpans(min, max,
850                                                        URLSpan.class);
851
852            if (urls.length == 1) {
853                return Uri.parse(urls[0].getURL());
854            }
855        }
856
857        //no uri was selected
858        return null;
859    }
860
861    private void addUriSpecificMenuItems(ContextMenu menu, View v, int position) {
862        Uri uri = getSelectedUriFromMessageList((ListView) v, position);
863
864        if (uri != null) {
865            Intent intent = new Intent(null, uri);
866            intent.addCategory(Intent.CATEGORY_SELECTED_ALTERNATIVE);
867            menu.addIntentOptions(0, 0, 0,
868                    new android.content.ComponentName(this, ComposeMessageActivity.class),
869                    null, intent, 0, null);
870        }
871    }
872
873    private final void addCallAndContactMenuItems(
874            ContextMenu menu, MsgListMenuClickListener l, MessageItem msgItem) {
875        if (TextUtils.isEmpty(msgItem.mBody)) {
876            return;
877        }
878        SpannableString msg = new SpannableString(msgItem.mBody);
879        Linkify.addLinks(msg, Linkify.ALL);
880        ArrayList<String> uris =
881            MessageUtils.extractUris(msg.getSpans(0, msg.length(), URLSpan.class));
882
883        // Remove any dupes so they don't get added to the menu multiple times
884        HashSet<String> collapsedUris = new HashSet<String>();
885        for (String uri : uris) {
886            collapsedUris.add(uri.toLowerCase());
887        }
888        for (String uriString : collapsedUris) {
889            String prefix = null;
890            int sep = uriString.indexOf(":");
891            if (sep >= 0) {
892                prefix = uriString.substring(0, sep);
893                uriString = uriString.substring(sep + 1);
894            }
895            Uri contactUri = null;
896            boolean knownPrefix = true;
897            if ("mailto".equalsIgnoreCase(prefix))  {
898                contactUri = getContactUriForEmail(uriString);
899            } else if ("tel".equalsIgnoreCase(prefix)) {
900                contactUri = getContactUriForPhoneNumber(uriString);
901            } else {
902                knownPrefix = false;
903            }
904            if (knownPrefix && contactUri == null) {
905                Intent intent = ConversationList.createAddContactIntent(uriString);
906
907                String addContactString = getString(R.string.menu_add_address_to_contacts,
908                        uriString);
909                menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, addContactString)
910                    .setOnMenuItemClickListener(l)
911                    .setIntent(intent);
912            }
913        }
914    }
915
916    private Uri getContactUriForEmail(String emailAddress) {
917        Cursor cursor = SqliteWrapper.query(this, getContentResolver(),
918                Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(emailAddress)),
919                new String[] { Email.CONTACT_ID, Contacts.DISPLAY_NAME }, null, null, null);
920
921        if (cursor != null) {
922            try {
923                while (cursor.moveToNext()) {
924                    String name = cursor.getString(1);
925                    if (!TextUtils.isEmpty(name)) {
926                        return ContentUris.withAppendedId(Contacts.CONTENT_URI, cursor.getLong(0));
927                    }
928                }
929            } finally {
930                cursor.close();
931            }
932        }
933        return null;
934    }
935
936    private Uri getContactUriForPhoneNumber(String phoneNumber) {
937        Contact contact = Contact.get(phoneNumber, false);
938        if (contact.existsInDatabase()) {
939            return contact.getUri();
940        }
941        return null;
942    }
943
944    private final OnCreateContextMenuListener mMsgListMenuCreateListener =
945        new OnCreateContextMenuListener() {
946        @Override
947        public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
948            if (!isCursorValid()) {
949                return;
950            }
951            Cursor cursor = mMsgListAdapter.getCursor();
952            String type = cursor.getString(COLUMN_MSG_TYPE);
953            long msgId = cursor.getLong(COLUMN_ID);
954
955            addPositionBasedMenuItems(menu, v, menuInfo);
956
957            MessageItem msgItem = mMsgListAdapter.getCachedMessageItem(type, msgId, cursor);
958            if (msgItem == null) {
959                Log.e(TAG, "Cannot load message item for type = " + type
960                        + ", msgId = " + msgId);
961                return;
962            }
963
964            menu.setHeaderTitle(R.string.message_options);
965
966            MsgListMenuClickListener l = new MsgListMenuClickListener(msgItem);
967
968            // It is unclear what would make most sense for copying an MMS message
969            // to the clipboard, so we currently do SMS only.
970            if (msgItem.isSms()) {
971                // Message type is sms. Only allow "edit" if the message has a single recipient
972                if (getRecipients().size() == 1 &&
973                        (msgItem.mBoxId == Sms.MESSAGE_TYPE_OUTBOX ||
974                                msgItem.mBoxId == Sms.MESSAGE_TYPE_FAILED)) {
975                    menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit)
976                    .setOnMenuItemClickListener(l);
977                }
978
979                menu.add(0, MENU_COPY_MESSAGE_TEXT, 0, R.string.copy_message_text)
980                .setOnMenuItemClickListener(l);
981            }
982
983            addCallAndContactMenuItems(menu, l, msgItem);
984
985            // Forward is not available for undownloaded messages.
986            if (msgItem.isDownloaded() && (msgItem.isSms() || isForwardable(msgId))) {
987                menu.add(0, MENU_FORWARD_MESSAGE, 0, R.string.menu_forward)
988                        .setOnMenuItemClickListener(l);
989            }
990
991            if (msgItem.isMms()) {
992                switch (msgItem.mBoxId) {
993                    case Mms.MESSAGE_BOX_INBOX:
994                        break;
995                    case Mms.MESSAGE_BOX_OUTBOX:
996                        // Since we currently break outgoing messages to multiple
997                        // recipients into one message per recipient, only allow
998                        // editing a message for single-recipient conversations.
999                        if (getRecipients().size() == 1) {
1000                            menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit)
1001                                    .setOnMenuItemClickListener(l);
1002                        }
1003                        break;
1004                }
1005                switch (msgItem.mAttachmentType) {
1006                    case WorkingMessage.TEXT:
1007                        break;
1008                    case WorkingMessage.VIDEO:
1009                    case WorkingMessage.IMAGE:
1010                        if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) {
1011                            menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard)
1012                            .setOnMenuItemClickListener(l);
1013                        }
1014                        break;
1015                    case WorkingMessage.SLIDESHOW:
1016                    default:
1017                        menu.add(0, MENU_VIEW_SLIDESHOW, 0, R.string.view_slideshow)
1018                        .setOnMenuItemClickListener(l);
1019                        if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) {
1020                            menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard)
1021                            .setOnMenuItemClickListener(l);
1022                        }
1023                        if (isDrmRingtoneWithRights(msgItem.mMsgId)) {
1024                            menu.add(0, MENU_SAVE_RINGTONE, 0,
1025                                    getDrmMimeMenuStringRsrc(msgItem.mMsgId))
1026                            .setOnMenuItemClickListener(l);
1027                        }
1028                        break;
1029                }
1030            }
1031
1032            if (msgItem.mLocked) {
1033                menu.add(0, MENU_UNLOCK_MESSAGE, 0, R.string.menu_unlock)
1034                    .setOnMenuItemClickListener(l);
1035            } else {
1036                menu.add(0, MENU_LOCK_MESSAGE, 0, R.string.menu_lock)
1037                    .setOnMenuItemClickListener(l);
1038            }
1039
1040            menu.add(0, MENU_VIEW_MESSAGE_DETAILS, 0, R.string.view_message_details)
1041                .setOnMenuItemClickListener(l);
1042
1043            if (msgItem.mDeliveryStatus != MessageItem.DeliveryStatus.NONE || msgItem.mReadReport) {
1044                menu.add(0, MENU_DELIVERY_REPORT, 0, R.string.view_delivery_report)
1045                        .setOnMenuItemClickListener(l);
1046            }
1047
1048            menu.add(0, MENU_DELETE_MESSAGE, 0, R.string.delete_message)
1049                .setOnMenuItemClickListener(l);
1050        }
1051    };
1052
1053    private void editMessageItem(MessageItem msgItem) {
1054        if ("sms".equals(msgItem.mType)) {
1055            editSmsMessageItem(msgItem);
1056        } else {
1057            editMmsMessageItem(msgItem);
1058        }
1059        if (msgItem.isFailedMessage() && mMsgListAdapter.getCount() <= 1) {
1060            // For messages with bad addresses, let the user re-edit the recipients.
1061            initRecipientsEditor();
1062        }
1063    }
1064
1065    private void editSmsMessageItem(MessageItem msgItem) {
1066        // When the message being edited is the only message in the conversation, the delete
1067        // below does something subtle. The trigger "delete_obsolete_threads_pdu" sees that a
1068        // thread contains no messages and silently deletes the thread. Meanwhile, the mConversation
1069        // object still holds onto the old thread_id and code thinks there's a backing thread in
1070        // the DB when it really has been deleted. Here we try and notice that situation and
1071        // clear out the thread_id. Later on, when Conversation.ensureThreadId() is called, we'll
1072        // create a new thread if necessary.
1073        synchronized(mConversation) {
1074            if (mConversation.getMessageCount() <= 1) {
1075                mConversation.clearThreadId();
1076                MessagingNotification.setCurrentlyDisplayedThreadId(
1077                    MessagingNotification.THREAD_NONE);
1078            }
1079        }
1080        // Delete the old undelivered SMS and load its content.
1081        Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgItem.mMsgId);
1082        SqliteWrapper.delete(ComposeMessageActivity.this,
1083                mContentResolver, uri, null, null);
1084
1085        mWorkingMessage.setText(msgItem.mBody);
1086    }
1087
1088    private void editMmsMessageItem(MessageItem msgItem) {
1089        // Load the selected message in as the working message.
1090        WorkingMessage newWorkingMessage = WorkingMessage.load(this, msgItem.mMessageUri);
1091        if (newWorkingMessage == null) {
1092            return;
1093        }
1094
1095        // Discard the current message in progress.
1096        mWorkingMessage.discard();
1097
1098        mWorkingMessage = newWorkingMessage;
1099        mWorkingMessage.setConversation(mConversation);
1100        invalidateOptionsMenu();
1101
1102        drawTopPanel(false);
1103
1104        // WorkingMessage.load() above only loads the slideshow. Set the
1105        // subject here because we already know what it is and avoid doing
1106        // another DB lookup in load() just to get it.
1107        mWorkingMessage.setSubject(msgItem.mSubject, false);
1108
1109        if (mWorkingMessage.hasSubject()) {
1110            showSubjectEditor(true);
1111        }
1112    }
1113
1114    private void copyToClipboard(String str) {
1115        ClipboardManager clipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
1116        clipboard.setPrimaryClip(ClipData.newPlainText(null, str));
1117    }
1118
1119    private void forwardMessage(final MessageItem msgItem) {
1120        mTempThreadId = 0;
1121        // The user wants to forward the message. If the message is an mms message, we need to
1122        // persist the pdu to disk. This is done in a background task.
1123        // If the task takes longer than a half second, a progress dialog is displayed.
1124        // Once the PDU persisting is done, another runnable on the UI thread get executed to start
1125        // the ForwardMessageActivity.
1126        getAsyncDialog().runAsync(new Runnable() {
1127            @Override
1128            public void run() {
1129                // This runnable gets run in a background thread.
1130                if (msgItem.mType.equals("mms")) {
1131                    SendReq sendReq = new SendReq();
1132                    String subject = getString(R.string.forward_prefix);
1133                    if (msgItem.mSubject != null) {
1134                        subject += msgItem.mSubject;
1135                    }
1136                    sendReq.setSubject(new EncodedStringValue(subject));
1137                    sendReq.setBody(msgItem.mSlideshow.makeCopy());
1138
1139                    mTempMmsUri = null;
1140                    try {
1141                        PduPersister persister =
1142                                PduPersister.getPduPersister(ComposeMessageActivity.this);
1143                        // Copy the parts of the message here.
1144                        mTempMmsUri = persister.persist(sendReq, Mms.Draft.CONTENT_URI);
1145                        mTempThreadId = MessagingNotification.getThreadId(
1146                                ComposeMessageActivity.this, mTempMmsUri);
1147                    } catch (MmsException e) {
1148                        Log.e(TAG, "Failed to copy message: " + msgItem.mMessageUri);
1149                        Toast.makeText(ComposeMessageActivity.this,
1150                                R.string.cannot_save_message, Toast.LENGTH_SHORT).show();
1151                        return;
1152                    }
1153                }
1154            }
1155        }, new Runnable() {
1156            @Override
1157            public void run() {
1158                // Once the above background thread is complete, this runnable is run
1159                // on the UI thread.
1160                Intent intent = createIntent(ComposeMessageActivity.this, 0);
1161
1162                intent.putExtra("exit_on_sent", true);
1163                intent.putExtra("forwarded_message", true);
1164                if (mTempThreadId > 0) {
1165                    intent.putExtra("thread_id", mTempThreadId);
1166                }
1167
1168                if (msgItem.mType.equals("sms")) {
1169                    intent.putExtra("sms_body", msgItem.mBody);
1170                } else {
1171                    intent.putExtra("msg_uri", mTempMmsUri);
1172                    String subject = getString(R.string.forward_prefix);
1173                    if (msgItem.mSubject != null) {
1174                        subject += msgItem.mSubject;
1175                    }
1176                    intent.putExtra("subject", subject);
1177                }
1178                // ForwardMessageActivity is simply an alias in the manifest for
1179                // ComposeMessageActivity. We have to make an alias because ComposeMessageActivity
1180                // launch flags specify singleTop. When we forward a message, we want to start a
1181                // separate ComposeMessageActivity. The only way to do that is to override the
1182                // singleTop flag, which is impossible to do in code. By creating an alias to the
1183                // activity, without the singleTop flag, we can launch a separate
1184                // ComposeMessageActivity to edit the forward message.
1185                intent.setClassName(ComposeMessageActivity.this,
1186                        "com.android.mms.ui.ForwardMessageActivity");
1187                startActivity(intent);
1188            }
1189        }, R.string.building_slideshow_title);
1190    }
1191
1192    /**
1193     * Context menu handlers for the message list view.
1194     */
1195    private final class MsgListMenuClickListener implements MenuItem.OnMenuItemClickListener {
1196        private MessageItem mMsgItem;
1197
1198        public MsgListMenuClickListener(MessageItem msgItem) {
1199            mMsgItem = msgItem;
1200        }
1201
1202        @Override
1203        public boolean onMenuItemClick(MenuItem item) {
1204            if (mMsgItem == null) {
1205                return false;
1206            }
1207
1208            switch (item.getItemId()) {
1209                case MENU_EDIT_MESSAGE:
1210                    editMessageItem(mMsgItem);
1211                    drawBottomPanel();
1212                    return true;
1213
1214                case MENU_COPY_MESSAGE_TEXT:
1215                    copyToClipboard(mMsgItem.mBody);
1216                    return true;
1217
1218                case MENU_FORWARD_MESSAGE:
1219                    forwardMessage(mMsgItem);
1220                    return true;
1221
1222                case MENU_VIEW_SLIDESHOW:
1223                    MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this,
1224                            ContentUris.withAppendedId(Mms.CONTENT_URI, mMsgItem.mMsgId), null,
1225                            getAsyncDialog());
1226                    return true;
1227
1228                case MENU_VIEW_MESSAGE_DETAILS: {
1229                    Cursor cursor = mMsgListAdapter.getCursorForItem(mMsgItem);
1230                    if (cursor == null) {
1231                        return false;
1232                    }
1233                    String messageDetails = MessageUtils.getMessageDetails(
1234                            ComposeMessageActivity.this, cursor, mMsgItem.mMessageSize);
1235                    new AlertDialog.Builder(ComposeMessageActivity.this)
1236                            .setTitle(R.string.message_details_title)
1237                            .setMessage(messageDetails)
1238                            .setCancelable(true)
1239                            .show();
1240                    return true;
1241                }
1242                case MENU_DELETE_MESSAGE: {
1243                    DeleteMessageListener l = new DeleteMessageListener(
1244                            mMsgItem.mMessageUri, mMsgItem.mLocked);
1245                    confirmDeleteDialog(l, mMsgItem.mLocked);
1246                    return true;
1247                }
1248                case MENU_DELIVERY_REPORT:
1249                    showDeliveryReport(mMsgItem.mMsgId, mMsgItem.mType);
1250                    return true;
1251
1252                case MENU_COPY_TO_SDCARD: {
1253                    int resId = copyMedia(mMsgItem.mMsgId) ? R.string.copy_to_sdcard_success :
1254                        R.string.copy_to_sdcard_fail;
1255                    Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show();
1256                    return true;
1257                }
1258
1259                case MENU_SAVE_RINGTONE: {
1260                    int resId = getDrmMimeSavedStringRsrc(mMsgItem.mMsgId,
1261                            saveRingtone(mMsgItem.mMsgId));
1262                    Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show();
1263                    return true;
1264                }
1265
1266                case MENU_LOCK_MESSAGE: {
1267                    lockMessage(mMsgItem, true);
1268                    return true;
1269                }
1270
1271                case MENU_UNLOCK_MESSAGE: {
1272                    lockMessage(mMsgItem, false);
1273                    return true;
1274                }
1275
1276                default:
1277                    return false;
1278            }
1279        }
1280    }
1281
1282    private void lockMessage(MessageItem msgItem, boolean locked) {
1283        Uri uri;
1284        if ("sms".equals(msgItem.mType)) {
1285            uri = Sms.CONTENT_URI;
1286        } else {
1287            uri = Mms.CONTENT_URI;
1288        }
1289        final Uri lockUri = ContentUris.withAppendedId(uri, msgItem.mMsgId);
1290
1291        final ContentValues values = new ContentValues(1);
1292        values.put("locked", locked ? 1 : 0);
1293
1294        new Thread(new Runnable() {
1295            @Override
1296            public void run() {
1297                getContentResolver().update(lockUri,
1298                        values, null, null);
1299            }
1300        }, "ComposeMessageActivity.lockMessage").start();
1301    }
1302
1303    /**
1304     * Looks to see if there are any valid parts of the attachment that can be copied to a SD card.
1305     * @param msgId
1306     */
1307    private boolean haveSomethingToCopyToSDCard(long msgId) {
1308        PduBody body = null;
1309        try {
1310            body = SlideshowModel.getPduBody(this,
1311                        ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1312        } catch (MmsException e) {
1313            Log.e(TAG, "haveSomethingToCopyToSDCard can't load pdu body: " + msgId);
1314        }
1315        if (body == null) {
1316            return false;
1317        }
1318
1319        boolean result = false;
1320        int partNum = body.getPartsNum();
1321        for(int i = 0; i < partNum; i++) {
1322            PduPart part = body.getPart(i);
1323            String type = new String(part.getContentType());
1324
1325            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1326                log("[CMA] haveSomethingToCopyToSDCard: part[" + i + "] contentType=" + type);
1327            }
1328
1329            if (ContentType.isImageType(type) || ContentType.isVideoType(type) ||
1330                    ContentType.isAudioType(type) || DrmUtils.isDrmType(type)) {
1331                result = true;
1332                break;
1333            }
1334        }
1335        return result;
1336    }
1337
1338    /**
1339     * Copies media from an Mms to the DrmProvider
1340     * @param msgId
1341     */
1342    private boolean saveRingtone(long msgId) {
1343        boolean result = true;
1344        PduBody body = null;
1345        try {
1346            body = SlideshowModel.getPduBody(this,
1347                        ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1348        } catch (MmsException e) {
1349            Log.e(TAG, "copyToDrmProvider can't load pdu body: " + msgId);
1350        }
1351        if (body == null) {
1352            return false;
1353        }
1354
1355        int partNum = body.getPartsNum();
1356        for(int i = 0; i < partNum; i++) {
1357            PduPart part = body.getPart(i);
1358            String type = new String(part.getContentType());
1359
1360            if (DrmUtils.isDrmType(type)) {
1361                // All parts (but there's probably only a single one) have to be successful
1362                // for a valid result.
1363                result &= copyPart(part, Long.toHexString(msgId));
1364            }
1365        }
1366        return result;
1367    }
1368
1369    /**
1370     * Returns true if any part is drm'd audio with ringtone rights.
1371     * @param msgId
1372     * @return true if one of the parts is drm'd audio with rights to save as a ringtone.
1373     */
1374    private boolean isDrmRingtoneWithRights(long msgId) {
1375        PduBody body = null;
1376        try {
1377            body = SlideshowModel.getPduBody(this,
1378                        ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1379        } catch (MmsException e) {
1380            Log.e(TAG, "isDrmRingtoneWithRights can't load pdu body: " + msgId);
1381        }
1382        if (body == null) {
1383            return false;
1384        }
1385
1386        int partNum = body.getPartsNum();
1387        for (int i = 0; i < partNum; i++) {
1388            PduPart part = body.getPart(i);
1389            String type = new String(part.getContentType());
1390
1391            if (DrmUtils.isDrmType(type)) {
1392                String mimeType = MmsApp.getApplication().getDrmManagerClient()
1393                        .getOriginalMimeType(part.getDataUri());
1394                if (ContentType.isAudioType(mimeType) && DrmUtils.haveRightsForAction(part.getDataUri(),
1395                        DrmStore.Action.RINGTONE)) {
1396                    return true;
1397                }
1398            }
1399        }
1400        return false;
1401    }
1402
1403    /**
1404     * Returns true if all drm'd parts are forwardable.
1405     * @param msgId
1406     * @return true if all drm'd parts are forwardable.
1407     */
1408    private boolean isForwardable(long msgId) {
1409        PduBody body = null;
1410        try {
1411            body = SlideshowModel.getPduBody(this,
1412                        ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1413        } catch (MmsException e) {
1414            Log.e(TAG, "getDrmMimeType can't load pdu body: " + msgId);
1415        }
1416        if (body == null) {
1417            return false;
1418        }
1419
1420        int partNum = body.getPartsNum();
1421        for (int i = 0; i < partNum; i++) {
1422            PduPart part = body.getPart(i);
1423            String type = new String(part.getContentType());
1424
1425            if (DrmUtils.isDrmType(type) && !DrmUtils.haveRightsForAction(part.getDataUri(),
1426                        DrmStore.Action.TRANSFER)) {
1427                    return false;
1428            }
1429        }
1430        return true;
1431    }
1432
1433    private int getDrmMimeMenuStringRsrc(long msgId) {
1434        if (isDrmRingtoneWithRights(msgId)) {
1435            return R.string.save_ringtone;
1436        }
1437        return 0;
1438    }
1439
1440    private int getDrmMimeSavedStringRsrc(long msgId, boolean success) {
1441        if (isDrmRingtoneWithRights(msgId)) {
1442            return success ? R.string.saved_ringtone : R.string.saved_ringtone_fail;
1443        }
1444        return 0;
1445    }
1446
1447    /**
1448     * Copies media from an Mms to the "download" directory on the SD card. If any of the parts
1449     * are audio types, drm'd or not, they're copied to the "Ringtones" directory.
1450     * @param msgId
1451     */
1452    private boolean copyMedia(long msgId) {
1453        boolean result = true;
1454        PduBody body = null;
1455        try {
1456            body = SlideshowModel.getPduBody(this,
1457                        ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1458        } catch (MmsException e) {
1459            Log.e(TAG, "copyMedia can't load pdu body: " + msgId);
1460        }
1461        if (body == null) {
1462            return false;
1463        }
1464
1465        int partNum = body.getPartsNum();
1466        for(int i = 0; i < partNum; i++) {
1467            PduPart part = body.getPart(i);
1468
1469            // all parts have to be successful for a valid result.
1470            result &= copyPart(part, Long.toHexString(msgId));
1471        }
1472        return result;
1473    }
1474
1475    private boolean copyPart(PduPart part, String fallback) {
1476        Uri uri = part.getDataUri();
1477        String type = new String(part.getContentType());
1478        boolean isDrm = DrmUtils.isDrmType(type);
1479        if (isDrm) {
1480            type = MmsApp.getApplication().getDrmManagerClient()
1481                    .getOriginalMimeType(part.getDataUri());
1482        }
1483        if (ContentType.APP_SMIL.equals(type)) {
1484            return true;    // we're not going to save the "application/smil" that often can
1485                            // be a downloaded part. Pretend it was saved.
1486        }
1487        InputStream input = null;
1488        FileOutputStream fout = null;
1489        try {
1490            input = mContentResolver.openInputStream(uri);
1491            if (input instanceof FileInputStream) {
1492                FileInputStream fin = (FileInputStream) input;
1493
1494                byte[] location = part.getName();
1495                if (location == null) {
1496                    location = part.getFilename();
1497                }
1498                if (location == null) {
1499                    location = part.getContentLocation();
1500                }
1501
1502                String fileName;
1503                if (location == null) {
1504                    // Use fallback name.
1505                    fileName = fallback;
1506                } else {
1507                    // For locally captured videos, fileName can end up being something like this:
1508                    //      /mnt/sdcard/Android/data/com.android.mms/cache/.temp1.3gp
1509                    fileName = new String(location);
1510                }
1511                File originalFile = new File(fileName);
1512                fileName = originalFile.getName();  // Strip the full path of where the "part" is
1513                                                    // stored down to just the leaf filename.
1514
1515                // Depending on the location, there may be an
1516                // extension already on the name or not. If we've got audio, put the attachment
1517                // in the Ringtones directory.
1518                String dir = Environment.getExternalStorageDirectory() + "/"
1519                                + (ContentType.isAudioType(type) ? Environment.DIRECTORY_RINGTONES :
1520                                    Environment.DIRECTORY_DOWNLOADS)  + "/";
1521                String extension;
1522                int index;
1523                if ((index = fileName.lastIndexOf('.')) == -1) {
1524                    extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
1525                } else {
1526                    extension = fileName.substring(index + 1, fileName.length());
1527                    fileName = fileName.substring(0, index);
1528                }
1529                if (isDrm) {
1530                    extension += DrmUtils.getConvertExtension(type);
1531                }
1532                File file = getUniqueDestination(dir + fileName, extension);
1533
1534                // make sure the path is valid and directories created for this file.
1535                File parentFile = file.getParentFile();
1536                if (!parentFile.exists() && !parentFile.mkdirs()) {
1537                    Log.e(TAG, "[MMS] copyPart: mkdirs for " + parentFile.getPath() + " failed!");
1538                    return false;
1539                }
1540
1541                fout = new FileOutputStream(file);
1542
1543                byte[] buffer = new byte[8000];
1544                int size = 0;
1545                while ((size=fin.read(buffer)) != -1) {
1546                    fout.write(buffer, 0, size);
1547                }
1548
1549                // Notify other applications listening to scanner events
1550                // that a media file has been added to the sd card
1551                sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
1552                        Uri.fromFile(file)));
1553            }
1554        } catch (IOException e) {
1555            // Ignore
1556            Log.e(TAG, "IOException caught while opening or reading stream", e);
1557            return false;
1558        } finally {
1559            if (null != input) {
1560                try {
1561                    input.close();
1562                } catch (IOException e) {
1563                    // Ignore
1564                    Log.e(TAG, "IOException caught while closing stream", e);
1565                    return false;
1566                }
1567            }
1568            if (null != fout) {
1569                try {
1570                    fout.close();
1571                } catch (IOException e) {
1572                    // Ignore
1573                    Log.e(TAG, "IOException caught while closing stream", e);
1574                    return false;
1575                }
1576            }
1577        }
1578        return true;
1579    }
1580
1581    private File getUniqueDestination(String base, String extension) {
1582        File file = new File(base + "." + extension);
1583
1584        for (int i = 2; file.exists(); i++) {
1585            file = new File(base + "_" + i + "." + extension);
1586        }
1587        return file;
1588    }
1589
1590    private void showDeliveryReport(long messageId, String type) {
1591        Intent intent = new Intent(this, DeliveryReportActivity.class);
1592        intent.putExtra("message_id", messageId);
1593        intent.putExtra("message_type", type);
1594
1595        startActivity(intent);
1596    }
1597
1598    private final IntentFilter mHttpProgressFilter = new IntentFilter(PROGRESS_STATUS_ACTION);
1599
1600    private final BroadcastReceiver mHttpProgressReceiver = new BroadcastReceiver() {
1601        @Override
1602        public void onReceive(Context context, Intent intent) {
1603            if (PROGRESS_STATUS_ACTION.equals(intent.getAction())) {
1604                long token = intent.getLongExtra("token",
1605                                    SendingProgressTokenManager.NO_TOKEN);
1606                if (token != mConversation.getThreadId()) {
1607                    return;
1608                }
1609
1610                int progress = intent.getIntExtra("progress", 0);
1611                switch (progress) {
1612                    case PROGRESS_START:
1613                        setProgressBarVisibility(true);
1614                        break;
1615                    case PROGRESS_ABORT:
1616                    case PROGRESS_COMPLETE:
1617                        setProgressBarVisibility(false);
1618                        break;
1619                    default:
1620                        setProgress(100 * progress);
1621                }
1622            }
1623        }
1624    };
1625
1626    private static ContactList sEmptyContactList;
1627
1628    private ContactList getRecipients() {
1629        // If the recipients editor is visible, the conversation has
1630        // not really officially 'started' yet.  Recipients will be set
1631        // on the conversation once it has been saved or sent.  In the
1632        // meantime, let anyone who needs the recipient list think it
1633        // is empty rather than giving them a stale one.
1634        if (isRecipientsEditorVisible()) {
1635            if (sEmptyContactList == null) {
1636                sEmptyContactList = new ContactList();
1637            }
1638            return sEmptyContactList;
1639        }
1640        return mConversation.getRecipients();
1641    }
1642
1643    private void updateTitle(ContactList list) {
1644        String title = null;;
1645        String subTitle = null;
1646        int cnt = list.size();
1647        switch (cnt) {
1648            case 0: {
1649                String recipient = null;
1650                if (mRecipientsEditor != null) {
1651                    recipient = mRecipientsEditor.getText().toString();
1652                }
1653                title = TextUtils.isEmpty(recipient) ? getString(R.string.new_message) : recipient;
1654                break;
1655            }
1656            case 1: {
1657                title = list.get(0).getName();      // get name returns the number if there's no
1658                                                    // name available.
1659                String number = list.get(0).getNumber();
1660                if (!title.equals(number)) {
1661                    subTitle = PhoneNumberUtils.formatNumber(number, number,
1662                            MmsApp.getApplication().getCurrentCountryIso());
1663                }
1664                break;
1665            }
1666            default: {
1667                // Handle multiple recipients
1668                title = list.formatNames(", ");
1669                subTitle = getResources().getQuantityString(R.plurals.recipient_count, cnt, cnt);
1670                break;
1671            }
1672        }
1673        mDebugRecipients = list.serialize();
1674
1675        ActionBar actionBar = getActionBar();
1676        actionBar.setTitle(title);
1677        actionBar.setSubtitle(subTitle);
1678    }
1679
1680    // Get the recipients editor ready to be displayed onscreen.
1681    private void initRecipientsEditor() {
1682        if (isRecipientsEditorVisible()) {
1683            return;
1684        }
1685        // Must grab the recipients before the view is made visible because getRecipients()
1686        // returns empty recipients when the editor is visible.
1687        ContactList recipients = getRecipients();
1688
1689        ViewStub stub = (ViewStub)findViewById(R.id.recipients_editor_stub);
1690        if (stub != null) {
1691            View stubView = stub.inflate();
1692            mRecipientsEditor = (RecipientsEditor) stubView.findViewById(R.id.recipients_editor);
1693            mRecipientsPicker = (ImageButton) stubView.findViewById(R.id.recipients_picker);
1694        } else {
1695            mRecipientsEditor = (RecipientsEditor)findViewById(R.id.recipients_editor);
1696            mRecipientsEditor.setVisibility(View.VISIBLE);
1697            mRecipientsPicker = (ImageButton)findViewById(R.id.recipients_picker);
1698        }
1699        mRecipientsPicker.setOnClickListener(this);
1700
1701        mRecipientsEditor.setAdapter(new ChipsRecipientAdapter(this));
1702        mRecipientsEditor.populate(recipients);
1703        mRecipientsEditor.setOnCreateContextMenuListener(mRecipientsMenuCreateListener);
1704        mRecipientsEditor.addTextChangedListener(mRecipientsWatcher);
1705        // TODO : Remove the max length limitation due to the multiple phone picker is added and the
1706        // user is able to select a large number of recipients from the Contacts. The coming
1707        // potential issue is that it is hard for user to edit a recipient from hundred of
1708        // recipients in the editor box. We may redesign the editor box UI for this use case.
1709        // mRecipientsEditor.setFilters(new InputFilter[] {
1710        //         new InputFilter.LengthFilter(RECIPIENTS_MAX_LENGTH) });
1711
1712        mRecipientsEditor.setOnSelectChipRunnable(new Runnable() {
1713            public void run() {
1714                // After the user selects an item in the pop-up contacts list, move the
1715                // focus to the text editor if there is only one recipient.  This helps
1716                // the common case of selecting one recipient and then typing a message,
1717                // but avoids annoying a user who is trying to add five recipients and
1718                // keeps having focus stolen away.
1719                if (mRecipientsEditor.getRecipientCount() == 1) {
1720                    // if we're in extract mode then don't request focus
1721                    final InputMethodManager inputManager = (InputMethodManager)
1722                        getSystemService(Context.INPUT_METHOD_SERVICE);
1723                    if (inputManager == null || !inputManager.isFullscreenMode()) {
1724                        mTextEditor.requestFocus();
1725                    }
1726                }
1727            }
1728        });
1729
1730        mRecipientsEditor.setOnFocusChangeListener(new View.OnFocusChangeListener() {
1731            @Override
1732            public void onFocusChange(View v, boolean hasFocus) {
1733                if (!hasFocus) {
1734                    RecipientsEditor editor = (RecipientsEditor) v;
1735                    ContactList contacts = editor.constructContactsFromInput(false);
1736                    updateTitle(contacts);
1737                }
1738            }
1739        });
1740
1741        PhoneNumberFormatter.setPhoneNumberFormattingTextWatcher(this, mRecipientsEditor);
1742
1743        mTopPanel.setVisibility(View.VISIBLE);
1744    }
1745
1746    //==========================================================
1747    // Activity methods
1748    //==========================================================
1749
1750    public static boolean cancelFailedToDeliverNotification(Intent intent, Context context) {
1751        if (MessagingNotification.isFailedToDeliver(intent)) {
1752            // Cancel any failed message notifications
1753            MessagingNotification.cancelNotification(context,
1754                        MessagingNotification.MESSAGE_FAILED_NOTIFICATION_ID);
1755            return true;
1756        }
1757        return false;
1758    }
1759
1760    public static boolean cancelFailedDownloadNotification(Intent intent, Context context) {
1761        if (MessagingNotification.isFailedToDownload(intent)) {
1762            // Cancel any failed download notifications
1763            MessagingNotification.cancelNotification(context,
1764                        MessagingNotification.DOWNLOAD_FAILED_NOTIFICATION_ID);
1765            return true;
1766        }
1767        return false;
1768    }
1769
1770    @Override
1771    protected void onCreate(Bundle savedInstanceState) {
1772        super.onCreate(savedInstanceState);
1773
1774        resetConfiguration(getResources().getConfiguration());
1775
1776        setContentView(R.layout.compose_message_activity);
1777        setProgressBarVisibility(false);
1778
1779        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
1780                WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
1781
1782        // Initialize members for UI elements.
1783        initResourceRefs();
1784
1785        mContentResolver = getContentResolver();
1786        mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver);
1787
1788        initialize(savedInstanceState, 0);
1789
1790        if (TRACE) {
1791            android.os.Debug.startMethodTracing("compose");
1792        }
1793    }
1794
1795    private void showSubjectEditor(boolean show) {
1796        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1797            log("" + show);
1798        }
1799
1800        if (mSubjectTextEditor == null) {
1801            // Don't bother to initialize the subject editor if
1802            // we're just going to hide it.
1803            if (show == false) {
1804                return;
1805            }
1806            mSubjectTextEditor = (EditText)findViewById(R.id.subject);
1807            mSubjectTextEditor.setFilters(new InputFilter[] {
1808                    new LengthFilter(MmsConfig.getMaxSubjectLength())});
1809        }
1810
1811        mSubjectTextEditor.setOnKeyListener(show ? mSubjectKeyListener : null);
1812
1813        if (show) {
1814            mSubjectTextEditor.addTextChangedListener(mSubjectEditorWatcher);
1815        } else {
1816            mSubjectTextEditor.removeTextChangedListener(mSubjectEditorWatcher);
1817        }
1818
1819        mSubjectTextEditor.setText(mWorkingMessage.getSubject());
1820        mSubjectTextEditor.setVisibility(show ? View.VISIBLE : View.GONE);
1821        hideOrShowTopPanel();
1822    }
1823
1824    private void hideOrShowTopPanel() {
1825        boolean anySubViewsVisible = (isSubjectEditorVisible() || isRecipientsEditorVisible());
1826        mTopPanel.setVisibility(anySubViewsVisible ? View.VISIBLE : View.GONE);
1827    }
1828
1829    public void initialize(Bundle savedInstanceState, long originalThreadId) {
1830        // Create a new empty working message.
1831        mWorkingMessage = WorkingMessage.createEmpty(this);
1832
1833        // Read parameters or previously saved state of this activity. This will load a new
1834        // mConversation
1835        initActivityState(savedInstanceState);
1836
1837        if (LogTag.SEVERE_WARNING && originalThreadId != 0 &&
1838                originalThreadId == mConversation.getThreadId()) {
1839            LogTag.warnPossibleRecipientMismatch("ComposeMessageActivity.initialize: " +
1840                    " threadId didn't change from: " + originalThreadId, this);
1841        }
1842
1843        log("savedInstanceState = " + savedInstanceState +
1844            " intent = " + getIntent() +
1845            " mConversation = " + mConversation);
1846
1847        if (cancelFailedToDeliverNotification(getIntent(), this)) {
1848            // Show a pop-up dialog to inform user the message was
1849            // failed to deliver.
1850            undeliveredMessageDialog(getMessageDate(null));
1851        }
1852        cancelFailedDownloadNotification(getIntent(), this);
1853
1854        // Set up the message history ListAdapter
1855        initMessageList();
1856
1857        // Load the draft for this thread, if we aren't already handling
1858        // existing data, such as a shared picture or forwarded message.
1859        boolean isForwardedMessage = false;
1860        // We don't attempt to handle the Intent.ACTION_SEND when saveInstanceState is non-null.
1861        // saveInstanceState is non-null when this activity is killed. In that case, we already
1862        // handled the attachment or the send, so we don't try and parse the intent again.
1863        boolean intentHandled = savedInstanceState == null &&
1864            (handleSendIntent() || handleForwardedMessage());
1865        if (!intentHandled) {
1866            loadDraft();
1867        }
1868
1869        // Let the working message know what conversation it belongs to
1870        mWorkingMessage.setConversation(mConversation);
1871
1872        // Show the recipients editor if we don't have a valid thread. Hide it otherwise.
1873        if (mConversation.getThreadId() <= 0) {
1874            // Hide the recipients editor so the call to initRecipientsEditor won't get
1875            // short-circuited.
1876            hideRecipientEditor();
1877            initRecipientsEditor();
1878
1879            // Bring up the softkeyboard so the user can immediately enter recipients. This
1880            // call won't do anything on devices with a hard keyboard.
1881            getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
1882                    WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
1883        } else {
1884            hideRecipientEditor();
1885        }
1886        invalidateOptionsMenu();    // do after show/hide of recipients editor because the options
1887                                    // menu depends on the recipients, which depending upon the
1888                                    // visibility of the recipients editor, returns a different
1889                                    // value (see getRecipients()).
1890
1891        updateSendButtonState();
1892
1893        drawTopPanel(false);
1894        if (intentHandled) {
1895            // We're not loading a draft, so we can draw the bottom panel immediately.
1896            drawBottomPanel();
1897        }
1898
1899        onKeyboardStateChanged(mIsKeyboardOpen);
1900
1901        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1902            log("update title, mConversation=" + mConversation.toString());
1903        }
1904
1905        updateTitle(mConversation.getRecipients());
1906
1907        if (isForwardedMessage && isRecipientsEditorVisible()) {
1908            // The user is forwarding the message to someone. Put the focus on the
1909            // recipient editor rather than in the message editor.
1910            mRecipientsEditor.requestFocus();
1911        }
1912    }
1913
1914    @Override
1915    protected void onNewIntent(Intent intent) {
1916        super.onNewIntent(intent);
1917
1918        setIntent(intent);
1919
1920        Conversation conversation = null;
1921        mSentMessage = false;
1922
1923        // If we have been passed a thread_id, use that to find our
1924        // conversation.
1925
1926        // Note that originalThreadId might be zero but if this is a draft and we save the
1927        // draft, ensureThreadId gets called async from WorkingMessage.asyncUpdateDraftSmsMessage
1928        // the thread will get a threadId behind the UI thread's back.
1929        long originalThreadId = mConversation.getThreadId();
1930        long threadId = intent.getLongExtra("thread_id", 0);
1931        Uri intentUri = intent.getData();
1932
1933        boolean sameThread = false;
1934        if (threadId > 0) {
1935            conversation = Conversation.get(this, threadId, false);
1936        } else {
1937            if (mConversation.getThreadId() == 0) {
1938                // We've got a draft. Make sure the working recipients are synched
1939                // to the conversation so when we compare conversations later in this function,
1940                // the compare will work.
1941                mWorkingMessage.syncWorkingRecipients();
1942            }
1943            // Get the "real" conversation based on the intentUri. The intentUri might specify
1944            // the conversation by a phone number or by a thread id. We'll typically get a threadId
1945            // based uri when the user pulls down a notification while in ComposeMessageActivity and
1946            // we end up here in onNewIntent. mConversation can have a threadId of zero when we're
1947            // working on a draft. When a new message comes in for that same recipient, a
1948            // conversation will get created behind CMA's back when the message is inserted into
1949            // the database and the corresponding entry made in the threads table. The code should
1950            // use the real conversation as soon as it can rather than finding out the threadId
1951            // when sending with "ensureThreadId".
1952            conversation = Conversation.get(this, intentUri, false);
1953        }
1954
1955        if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1956            log("onNewIntent: data=" + intentUri + ", thread_id extra is " + threadId +
1957                    ", new conversation=" + conversation + ", mConversation=" + mConversation);
1958        }
1959
1960        // this is probably paranoid to compare both thread_ids and recipient lists,
1961        // but we want to make double sure because this is a last minute fix for Froyo
1962        // and the previous code checked thread ids only.
1963        // (we cannot just compare thread ids because there is a case where mConversation
1964        // has a stale/obsolete thread id (=1) that could collide against the new thread_id(=1),
1965        // even though the recipient lists are different)
1966        sameThread = ((conversation.getThreadId() == mConversation.getThreadId() ||
1967                mConversation.getThreadId() == 0) &&
1968                conversation.equals(mConversation));
1969
1970        // Don't let any markAsRead DB updates occur before we've loaded the messages for
1971        // the thread. Unblocking occurs when we're done querying for the conversation
1972        // items.
1973        conversation.blockMarkAsRead(true);
1974
1975        if (sameThread) {
1976            log("onNewIntent: same conversation");
1977            if (mConversation.getThreadId() == 0) {
1978                mConversation = conversation;
1979                mWorkingMessage.setConversation(mConversation);
1980                MessagingNotification.setCurrentlyDisplayedThreadId(mConversation.getThreadId());
1981                invalidateOptionsMenu();
1982            }
1983            mConversation.markAsRead();         // dismiss any notifications for this convo
1984        } else {
1985            if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1986                log("onNewIntent: different conversation");
1987            }
1988            saveDraft(false);    // if we've got a draft, save it first
1989
1990            initialize(null, originalThreadId);
1991        }
1992        loadMessageContent();
1993    }
1994
1995    private void sanityCheckConversation() {
1996        if (mWorkingMessage.getConversation() != mConversation) {
1997            LogTag.warnPossibleRecipientMismatch(
1998                    "ComposeMessageActivity: mWorkingMessage.mConversation=" +
1999                    mWorkingMessage.getConversation() + ", mConversation=" +
2000                    mConversation + ", MISMATCH!", this);
2001        }
2002    }
2003
2004    @Override
2005    protected void onRestart() {
2006        super.onRestart();
2007
2008        if (mWorkingMessage.isDiscarded()) {
2009            // If the message isn't worth saving, don't resurrect it. Doing so can lead to
2010            // a situation where a new incoming message gets the old thread id of the discarded
2011            // draft. This activity can end up displaying the recipients of the old message with
2012            // the contents of the new message. Recognize that dangerous situation and bail out
2013            // to the ConversationList where the user can enter this in a clean manner.
2014            if (mWorkingMessage.isWorthSaving()) {
2015                if (LogTag.VERBOSE) {
2016                    log("onRestart: mWorkingMessage.unDiscard()");
2017                }
2018                mWorkingMessage.unDiscard();    // it was discarded in onStop().
2019
2020                sanityCheckConversation();
2021            } else if (isRecipientsEditorVisible()) {
2022                if (LogTag.VERBOSE) {
2023                    log("onRestart: goToConversationList");
2024                }
2025                goToConversationList();
2026            } else {
2027                if (LogTag.VERBOSE) {
2028                    log("onRestart: loadDraft");
2029                }
2030                loadDraft();
2031                mWorkingMessage.setConversation(mConversation);
2032                mAttachmentEditor.update(mWorkingMessage);
2033                invalidateOptionsMenu();
2034            }
2035        }
2036    }
2037
2038    @Override
2039    protected void onStart() {
2040        super.onStart();
2041        mConversation.blockMarkAsRead(true);
2042
2043        initFocus();
2044
2045        // Register a BroadcastReceiver to listen on HTTP I/O process.
2046        registerReceiver(mHttpProgressReceiver, mHttpProgressFilter);
2047
2048        loadMessageContent();
2049
2050        // Update the fasttrack info in case any of the recipients' contact info changed
2051        // while we were paused. This can happen, for example, if a user changes or adds
2052        // an avatar associated with a contact.
2053        mWorkingMessage.syncWorkingRecipients();
2054
2055        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2056            log("update title, mConversation=" + mConversation.toString());
2057        }
2058
2059        updateTitle(mConversation.getRecipients());
2060
2061        ActionBar actionBar = getActionBar();
2062        actionBar.setDisplayHomeAsUpEnabled(true);
2063    }
2064
2065    public void loadMessageContent() {
2066        startMsgListQuery();
2067        updateSendFailedNotification();
2068        drawBottomPanel();
2069    }
2070
2071    private void updateSendFailedNotification() {
2072        final long threadId = mConversation.getThreadId();
2073        if (threadId <= 0)
2074            return;
2075
2076        // updateSendFailedNotificationForThread makes a database call, so do the work off
2077        // of the ui thread.
2078        new Thread(new Runnable() {
2079            @Override
2080            public void run() {
2081                MessagingNotification.updateSendFailedNotificationForThread(
2082                        ComposeMessageActivity.this, threadId);
2083            }
2084        }, "ComposeMessageActivity.updateSendFailedNotification").start();
2085    }
2086
2087    @Override
2088    public void onSaveInstanceState(Bundle outState) {
2089        super.onSaveInstanceState(outState);
2090
2091        outState.putString("recipients", getRecipients().serialize());
2092
2093        mWorkingMessage.writeStateToBundle(outState);
2094
2095        if (mExitOnSent) {
2096            outState.putBoolean("exit_on_sent", mExitOnSent);
2097        }
2098    }
2099
2100    @Override
2101    protected void onResume() {
2102        super.onResume();
2103
2104        // OLD: get notified of presence updates to update the titlebar.
2105        // NEW: we are using ContactHeaderWidget which displays presence, but updating presence
2106        //      there is out of our control.
2107        //Contact.startPresenceObserver();
2108
2109        addRecipientsListeners();
2110
2111        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2112            log("update title, mConversation=" + mConversation.toString());
2113        }
2114
2115        // There seems to be a bug in the framework such that setting the title
2116        // here gets overwritten to the original title.  Do this delayed as a
2117        // workaround.
2118        mMessageListItemHandler.postDelayed(new Runnable() {
2119            @Override
2120            public void run() {
2121                ContactList recipients = isRecipientsEditorVisible() ?
2122                        mRecipientsEditor.constructContactsFromInput(false) : getRecipients();
2123                updateTitle(recipients);
2124            }
2125        }, 100);
2126
2127        MessagingNotification.setCurrentlyDisplayedThreadId(mConversation.getThreadId());
2128    }
2129
2130    @Override
2131    protected void onPause() {
2132        super.onPause();
2133
2134        // OLD: stop getting notified of presence updates to update the titlebar.
2135        // NEW: we are using ContactHeaderWidget which displays presence, but updating presence
2136        //      there is out of our control.
2137        //Contact.stopPresenceObserver();
2138
2139        removeRecipientsListeners();
2140
2141        // remove any callback to display a progress spinner
2142        if (mAsyncDialog != null) {
2143            mAsyncDialog.clearPendingProgressDialog();
2144        }
2145
2146        MessagingNotification.setCurrentlyDisplayedThreadId(MessagingNotification.THREAD_NONE);
2147    }
2148
2149    @Override
2150    protected void onStop() {
2151        super.onStop();
2152
2153        // Allow any blocked calls to update the thread's read status.
2154        mConversation.blockMarkAsRead(false);
2155
2156        if (mMsgListAdapter != null) {
2157            mMsgListAdapter.changeCursor(null);
2158            mMsgListAdapter.cancelBackgroundLoading();
2159        }
2160
2161        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2162            log("save draft");
2163        }
2164        saveDraft(true);
2165
2166        // Cleanup the BroadcastReceiver.
2167        unregisterReceiver(mHttpProgressReceiver);
2168    }
2169
2170    @Override
2171    protected void onDestroy() {
2172        if (TRACE) {
2173            android.os.Debug.stopMethodTracing();
2174        }
2175
2176        super.onDestroy();
2177    }
2178
2179    @Override
2180    public void onConfigurationChanged(Configuration newConfig) {
2181        super.onConfigurationChanged(newConfig);
2182        if (LOCAL_LOGV) {
2183            Log.v(TAG, "onConfigurationChanged: " + newConfig);
2184        }
2185
2186        if (resetConfiguration(newConfig)) {
2187            // Have to re-layout the attachment editor because we have different layouts
2188            // depending on whether we're portrait or landscape.
2189            drawTopPanel(isSubjectEditorVisible());
2190        }
2191        onKeyboardStateChanged(mIsKeyboardOpen);
2192    }
2193
2194    // returns true if landscape/portrait configuration has changed
2195    private boolean resetConfiguration(Configuration config) {
2196        mIsKeyboardOpen = config.keyboardHidden == KEYBOARDHIDDEN_NO;
2197        boolean isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE;
2198        if (mIsLandscape != isLandscape) {
2199            mIsLandscape = isLandscape;
2200            return true;
2201        }
2202        return false;
2203    }
2204
2205    private void onKeyboardStateChanged(boolean isKeyboardOpen) {
2206        // If the keyboard is hidden, don't show focus highlights for
2207        // things that cannot receive input.
2208        if (isKeyboardOpen) {
2209            if (mRecipientsEditor != null) {
2210                mRecipientsEditor.setFocusableInTouchMode(true);
2211            }
2212            if (mSubjectTextEditor != null) {
2213                mSubjectTextEditor.setFocusableInTouchMode(true);
2214            }
2215            mTextEditor.setFocusableInTouchMode(true);
2216            mTextEditor.setHint(R.string.type_to_compose_text_enter_to_send);
2217        } else {
2218            if (mRecipientsEditor != null) {
2219                mRecipientsEditor.setFocusable(false);
2220            }
2221            if (mSubjectTextEditor != null) {
2222                mSubjectTextEditor.setFocusable(false);
2223            }
2224            mTextEditor.setFocusable(false);
2225            mTextEditor.setHint(R.string.open_keyboard_to_compose_message);
2226        }
2227    }
2228
2229    @Override
2230    public void onUserInteraction() {
2231        checkPendingNotification();
2232    }
2233
2234    @Override
2235    public void onWindowFocusChanged(boolean hasFocus) {
2236        if (hasFocus) {
2237            checkPendingNotification();
2238        }
2239    }
2240
2241    @Override
2242    public boolean onKeyDown(int keyCode, KeyEvent event) {
2243        switch (keyCode) {
2244            case KeyEvent.KEYCODE_DEL:
2245                if ((mMsgListAdapter != null) && mMsgListView.isFocused()) {
2246                    Cursor cursor;
2247                    try {
2248                        cursor = (Cursor) mMsgListView.getSelectedItem();
2249                    } catch (ClassCastException e) {
2250                        Log.e(TAG, "Unexpected ClassCastException.", e);
2251                        return super.onKeyDown(keyCode, event);
2252                    }
2253
2254                    if (cursor != null) {
2255                        boolean locked = cursor.getInt(COLUMN_MMS_LOCKED) != 0;
2256                        DeleteMessageListener l = new DeleteMessageListener(
2257                                cursor.getLong(COLUMN_ID),
2258                                cursor.getString(COLUMN_MSG_TYPE),
2259                                locked);
2260                        confirmDeleteDialog(l, locked);
2261                        return true;
2262                    }
2263                }
2264                break;
2265            case KeyEvent.KEYCODE_DPAD_CENTER:
2266            case KeyEvent.KEYCODE_ENTER:
2267                if (isPreparedForSending()) {
2268                    confirmSendMessageIfNeeded();
2269                    return true;
2270                }
2271                break;
2272            case KeyEvent.KEYCODE_BACK:
2273                exitComposeMessageActivity(new Runnable() {
2274                    @Override
2275                    public void run() {
2276                        finish();
2277                    }
2278                });
2279                return true;
2280        }
2281
2282        return super.onKeyDown(keyCode, event);
2283    }
2284
2285    private void exitComposeMessageActivity(final Runnable exit) {
2286        // If the message is empty, just quit -- finishing the
2287        // activity will cause an empty draft to be deleted.
2288        if (!mWorkingMessage.isWorthSaving()) {
2289            exit.run();
2290            return;
2291        }
2292
2293        if (isRecipientsEditorVisible() &&
2294                !mRecipientsEditor.hasValidRecipient(mWorkingMessage.requiresMms())) {
2295            MessageUtils.showDiscardDraftConfirmDialog(this, new DiscardDraftListener());
2296            return;
2297        }
2298
2299        mToastForDraftSave = true;
2300        exit.run();
2301    }
2302
2303    private void goToConversationList() {
2304        finish();
2305        startActivity(new Intent(this, ConversationList.class));
2306    }
2307
2308    private void hideRecipientEditor() {
2309        if (mRecipientsEditor != null) {
2310            mRecipientsEditor.removeTextChangedListener(mRecipientsWatcher);
2311            mRecipientsEditor.setVisibility(View.GONE);
2312            hideOrShowTopPanel();
2313        }
2314    }
2315
2316    private boolean isRecipientsEditorVisible() {
2317        return (null != mRecipientsEditor)
2318                    && (View.VISIBLE == mRecipientsEditor.getVisibility());
2319    }
2320
2321    private boolean isSubjectEditorVisible() {
2322        return (null != mSubjectTextEditor)
2323                    && (View.VISIBLE == mSubjectTextEditor.getVisibility());
2324    }
2325
2326    @Override
2327    public void onAttachmentChanged() {
2328        // Have to make sure we're on the UI thread. This function can be called off of the UI
2329        // thread when we're adding multi-attachments
2330        runOnUiThread(new Runnable() {
2331            @Override
2332            public void run() {
2333                drawBottomPanel();
2334                updateSendButtonState();
2335                drawTopPanel(isSubjectEditorVisible());
2336            }
2337        });
2338    }
2339
2340    @Override
2341    public void onProtocolChanged(final boolean mms) {
2342        // Have to make sure we're on the UI thread. This function can be called off of the UI
2343        // thread when we're adding multi-attachments
2344        runOnUiThread(new Runnable() {
2345            @Override
2346            public void run() {
2347                toastConvertInfo(mms);
2348                showSmsOrMmsSendButton(mms);
2349
2350                if (mms) {
2351                    // In the case we went from a long sms with a counter to an mms because
2352                    // the user added an attachment or a subject, hide the counter --
2353                    // it doesn't apply to mms.
2354                    mTextCounter.setVisibility(View.GONE);
2355                }
2356            }
2357        });
2358    }
2359
2360    // Show or hide the Sms or Mms button as appropriate. Return the view so that the caller
2361    // can adjust the enableness and focusability.
2362    private View showSmsOrMmsSendButton(boolean isMms) {
2363        View showButton;
2364        View hideButton;
2365        if (isMms) {
2366            showButton = mSendButtonMms;
2367            hideButton = mSendButtonSms;
2368        } else {
2369            showButton = mSendButtonSms;
2370            hideButton = mSendButtonMms;
2371        }
2372        showButton.setVisibility(View.VISIBLE);
2373        hideButton.setVisibility(View.GONE);
2374
2375        return showButton;
2376    }
2377
2378    Runnable mResetMessageRunnable = new Runnable() {
2379        @Override
2380        public void run() {
2381            resetMessage();
2382        }
2383    };
2384
2385    @Override
2386    public void onPreMessageSent() {
2387        runOnUiThread(mResetMessageRunnable);
2388    }
2389
2390    @Override
2391    public void onMessageSent() {
2392        // If we already have messages in the list adapter, it
2393        // will be auto-requerying; don't thrash another query in.
2394        // TODO: relying on auto-requerying seems unreliable when priming an MMS into the outbox.
2395        // Need to investigate.
2396//        if (mMsgListAdapter.getCount() == 0) {
2397            if (LogTag.VERBOSE) {
2398                log("onMessageSent");
2399            }
2400            startMsgListQuery();
2401//        }
2402
2403        // The thread ID could have changed if this is a new message that we just inserted into the
2404        // database (and looked up or created a thread for it)
2405        MessagingNotification.setCurrentlyDisplayedThreadId(mConversation.getThreadId());
2406    }
2407
2408    @Override
2409    public void onMaxPendingMessagesReached() {
2410        saveDraft(false);
2411
2412        runOnUiThread(new Runnable() {
2413            @Override
2414            public void run() {
2415                Toast.makeText(ComposeMessageActivity.this, R.string.too_many_unsent_mms,
2416                        Toast.LENGTH_LONG).show();
2417            }
2418        });
2419    }
2420
2421    @Override
2422    public void onAttachmentError(final int error) {
2423        runOnUiThread(new Runnable() {
2424            @Override
2425            public void run() {
2426                handleAddAttachmentError(error, R.string.type_picture);
2427                onMessageSent();        // now requery the list of messages
2428            }
2429        });
2430    }
2431
2432    // We don't want to show the "call" option unless there is only one
2433    // recipient and it's a phone number.
2434    private boolean isRecipientCallable() {
2435        ContactList recipients = getRecipients();
2436        return (recipients.size() == 1 && !recipients.containsEmail());
2437    }
2438
2439    private void dialRecipient() {
2440        if (isRecipientCallable()) {
2441            String number = getRecipients().get(0).getNumber();
2442            Intent dialIntent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + number));
2443            startActivity(dialIntent);
2444        }
2445    }
2446
2447    @Override
2448    public boolean onPrepareOptionsMenu(Menu menu) {
2449        super.onPrepareOptionsMenu(menu) ;
2450
2451        menu.clear();
2452
2453        if (isRecipientCallable()) {
2454            MenuItem item = menu.add(0, MENU_CALL_RECIPIENT, 0, R.string.menu_call)
2455                .setIcon(R.drawable.ic_menu_call)
2456                .setTitle(R.string.menu_call);
2457            if (!isRecipientsEditorVisible()) {
2458                // If we're not composing a new message, show the call icon in the actionbar
2459                item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
2460            }
2461        }
2462
2463        if (MmsConfig.getMmsEnabled()) {
2464            if (!isSubjectEditorVisible()) {
2465                menu.add(0, MENU_ADD_SUBJECT, 0, R.string.add_subject).setIcon(
2466                        R.drawable.ic_menu_edit);
2467            }
2468            menu.add(0, MENU_ADD_ATTACHMENT, 0, R.string.add_attachment)
2469                .setIcon(R.drawable.ic_menu_attachment)
2470                .setTitle(R.string.add_attachment)
2471                .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);    // add to actionbar
2472        }
2473
2474        if (isPreparedForSending()) {
2475            menu.add(0, MENU_SEND, 0, R.string.send).setIcon(android.R.drawable.ic_menu_send);
2476        }
2477
2478        if (!mWorkingMessage.hasSlideshow()) {
2479            menu.add(0, MENU_INSERT_SMILEY, 0, R.string.menu_insert_smiley).setIcon(
2480                    R.drawable.ic_menu_emoticons);
2481        }
2482
2483        if (mMsgListAdapter.getCount() > 0) {
2484            // Removed search as part of b/1205708
2485            //menu.add(0, MENU_SEARCH, 0, R.string.menu_search).setIcon(
2486            //        R.drawable.ic_menu_search);
2487            Cursor cursor = mMsgListAdapter.getCursor();
2488            if ((null != cursor) && (cursor.getCount() > 0)) {
2489                menu.add(0, MENU_DELETE_THREAD, 0, R.string.delete_thread).setIcon(
2490                    android.R.drawable.ic_menu_delete);
2491            }
2492        } else {
2493            menu.add(0, MENU_DISCARD, 0, R.string.discard).setIcon(android.R.drawable.ic_menu_delete);
2494        }
2495
2496        buildAddAddressToContactMenuItem(menu);
2497
2498        menu.add(0, MENU_PREFERENCES, 0, R.string.menu_preferences).setIcon(
2499                android.R.drawable.ic_menu_preferences);
2500
2501        if (LogTag.DEBUG_DUMP) {
2502            menu.add(0, MENU_DEBUG_DUMP, 0, R.string.menu_debug_dump);
2503        }
2504
2505        return true;
2506    }
2507
2508    private void buildAddAddressToContactMenuItem(Menu menu) {
2509        // Look for the first recipient we don't have a contact for and create a menu item to
2510        // add the number to contacts.
2511        for (Contact c : getRecipients()) {
2512            if (!c.existsInDatabase() && canAddToContacts(c)) {
2513                Intent intent = ConversationList.createAddContactIntent(c.getNumber());
2514                menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, R.string.menu_add_to_contacts)
2515                    .setIcon(android.R.drawable.ic_menu_add)
2516                    .setIntent(intent);
2517                break;
2518            }
2519        }
2520    }
2521
2522    @Override
2523    public boolean onOptionsItemSelected(MenuItem item) {
2524        switch (item.getItemId()) {
2525            case MENU_ADD_SUBJECT:
2526                showSubjectEditor(true);
2527                mWorkingMessage.setSubject("", true);
2528                updateSendButtonState();
2529                mSubjectTextEditor.requestFocus();
2530                break;
2531            case MENU_ADD_ATTACHMENT:
2532                // Launch the add-attachment list dialog
2533                showAddAttachmentDialog(false);
2534                break;
2535            case MENU_DISCARD:
2536                mWorkingMessage.discard();
2537                finish();
2538                break;
2539            case MENU_SEND:
2540                if (isPreparedForSending()) {
2541                    confirmSendMessageIfNeeded();
2542                }
2543                break;
2544            case MENU_SEARCH:
2545                onSearchRequested();
2546                break;
2547            case MENU_DELETE_THREAD:
2548                confirmDeleteThread(mConversation.getThreadId());
2549                break;
2550
2551            case android.R.id.home:
2552            case MENU_CONVERSATION_LIST:
2553                exitComposeMessageActivity(new Runnable() {
2554                    @Override
2555                    public void run() {
2556                        goToConversationList();
2557                    }
2558                });
2559                break;
2560            case MENU_CALL_RECIPIENT:
2561                dialRecipient();
2562                break;
2563            case MENU_INSERT_SMILEY:
2564                showSmileyDialog();
2565                break;
2566            case MENU_VIEW_CONTACT: {
2567                // View the contact for the first (and only) recipient.
2568                ContactList list = getRecipients();
2569                if (list.size() == 1 && list.get(0).existsInDatabase()) {
2570                    Uri contactUri = list.get(0).getUri();
2571                    Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
2572                    intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
2573                    startActivity(intent);
2574                }
2575                break;
2576            }
2577            case MENU_ADD_ADDRESS_TO_CONTACTS:
2578                mAddContactIntent = item.getIntent();
2579                startActivityForResult(mAddContactIntent, REQUEST_CODE_ADD_CONTACT);
2580                break;
2581            case MENU_PREFERENCES: {
2582                Intent intent = new Intent(this, MessagingPreferenceActivity.class);
2583                startActivityIfNeeded(intent, -1);
2584                break;
2585            }
2586            case MENU_DEBUG_DUMP:
2587                mWorkingMessage.dump();
2588                Conversation.dump();
2589                LogTag.dumpInternalTables(this);
2590                break;
2591        }
2592
2593        return true;
2594    }
2595
2596    private void confirmDeleteThread(long threadId) {
2597        Conversation.startQueryHaveLockedMessages(mBackgroundQueryHandler,
2598                threadId, ConversationList.HAVE_LOCKED_MESSAGES_TOKEN);
2599    }
2600
2601//    static class SystemProperties { // TODO, temp class to get unbundling working
2602//        static int getInt(String s, int value) {
2603//            return value;       // just return the default value or now
2604//        }
2605//    }
2606
2607    private void addAttachment(int type, boolean replace) {
2608        // Calculate the size of the current slide if we're doing a replace so the
2609        // slide size can optionally be used in computing how much room is left for an attachment.
2610        int currentSlideSize = 0;
2611        SlideshowModel slideShow = mWorkingMessage.getSlideshow();
2612        if (replace && slideShow != null) {
2613            SlideModel slide = slideShow.get(0);
2614            currentSlideSize = slide.getSlideSize();
2615        }
2616        switch (type) {
2617            case AttachmentTypeSelectorAdapter.ADD_IMAGE:
2618                MessageUtils.selectImage(this, REQUEST_CODE_ATTACH_IMAGE);
2619                break;
2620
2621            case AttachmentTypeSelectorAdapter.TAKE_PICTURE: {
2622                MessageUtils.capturePicture(this, REQUEST_CODE_TAKE_PICTURE);
2623                break;
2624            }
2625
2626            case AttachmentTypeSelectorAdapter.ADD_VIDEO:
2627                MessageUtils.selectVideo(this, REQUEST_CODE_ATTACH_VIDEO);
2628                break;
2629
2630            case AttachmentTypeSelectorAdapter.RECORD_VIDEO: {
2631                long sizeLimit = computeAttachmentSizeLimit(slideShow, currentSlideSize);
2632                if (sizeLimit > 0) {
2633                    MessageUtils.recordVideo(this, REQUEST_CODE_TAKE_VIDEO, sizeLimit);
2634                } else {
2635                    Toast.makeText(this,
2636                            getString(R.string.message_too_big_for_video),
2637                            Toast.LENGTH_SHORT).show();
2638                }
2639            }
2640            break;
2641
2642            case AttachmentTypeSelectorAdapter.ADD_SOUND:
2643                MessageUtils.selectAudio(this, REQUEST_CODE_ATTACH_SOUND);
2644                break;
2645
2646            case AttachmentTypeSelectorAdapter.RECORD_SOUND:
2647                long sizeLimit = computeAttachmentSizeLimit(slideShow, currentSlideSize);
2648                MessageUtils.recordSound(this, REQUEST_CODE_RECORD_SOUND, sizeLimit);
2649                break;
2650
2651            case AttachmentTypeSelectorAdapter.ADD_SLIDESHOW:
2652                editSlideshow();
2653                break;
2654
2655            default:
2656                break;
2657        }
2658    }
2659
2660    public static long computeAttachmentSizeLimit(SlideshowModel slideShow, int currentSlideSize) {
2661        // Computer attachment size limit. Subtract 1K for some text.
2662        long sizeLimit = MmsConfig.getMaxMessageSize() - SlideshowModel.SLIDESHOW_SLOP;
2663        if (slideShow != null) {
2664            sizeLimit -= slideShow.getCurrentMessageSize();
2665
2666            // We're about to ask the camera to capture some video (or the sound recorder
2667            // to record some audio) which will eventually replace the content on the current
2668            // slide. Since the current slide already has some content (which was subtracted
2669            // out just above) and that content is going to get replaced, we can add the size of the
2670            // current slide into the available space used to capture a video (or audio).
2671            sizeLimit += currentSlideSize;
2672        }
2673        return sizeLimit;
2674    }
2675
2676    private void showAddAttachmentDialog(final boolean replace) {
2677        AlertDialog.Builder builder = new AlertDialog.Builder(this);
2678        builder.setIcon(R.drawable.ic_dialog_attach);
2679        builder.setTitle(R.string.add_attachment);
2680
2681        if (mAttachmentTypeSelectorAdapter == null) {
2682            mAttachmentTypeSelectorAdapter = new AttachmentTypeSelectorAdapter(
2683                    this, AttachmentTypeSelectorAdapter.MODE_WITH_SLIDESHOW);
2684        }
2685        builder.setAdapter(mAttachmentTypeSelectorAdapter, new DialogInterface.OnClickListener() {
2686            @Override
2687            public void onClick(DialogInterface dialog, int which) {
2688                addAttachment(mAttachmentTypeSelectorAdapter.buttonToCommand(which), replace);
2689                dialog.dismiss();
2690            }
2691        });
2692
2693        builder.show();
2694    }
2695
2696    @Override
2697    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
2698        if (LogTag.VERBOSE) {
2699            log("requestCode=" + requestCode + ", resultCode=" + resultCode + ", data=" + data);
2700        }
2701        mWaitingForSubActivity = false;          // We're back!
2702        if (mWorkingMessage.isFakeMmsForDraft()) {
2703            // We no longer have to fake the fact we're an Mms. At this point we are or we aren't,
2704            // based on attachments and other Mms attrs.
2705            mWorkingMessage.removeFakeMmsForDraft();
2706        }
2707
2708        if (requestCode == REQUEST_CODE_PICK) {
2709            mWorkingMessage.asyncDeleteDraftSmsMessage(mConversation);
2710        }
2711
2712        if (requestCode == REQUEST_CODE_ADD_CONTACT) {
2713            // The user might have added a new contact. When we tell contacts to add a contact
2714            // and tap "Done", we're not returned to Messaging. If we back out to return to
2715            // messaging after adding a contact, the resultCode is RESULT_CANCELED. Therefore,
2716            // assume a contact was added and get the contact and force our cached contact to
2717            // get reloaded with the new info (such as contact name). After the
2718            // contact is reloaded, the function onUpdate() in this file will get called
2719            // and it will update the title bar, etc.
2720            if (mAddContactIntent != null) {
2721                String address =
2722                    mAddContactIntent.getStringExtra(ContactsContract.Intents.Insert.EMAIL);
2723                if (address == null) {
2724                    address =
2725                        mAddContactIntent.getStringExtra(ContactsContract.Intents.Insert.PHONE);
2726                }
2727                if (address != null) {
2728                    Contact contact = Contact.get(address, false);
2729                    if (contact != null) {
2730                        contact.reload();
2731                    }
2732                }
2733            }
2734        }
2735
2736        if (resultCode != RESULT_OK){
2737            if (LogTag.VERBOSE) log("bail due to resultCode=" + resultCode);
2738            return;
2739        }
2740
2741        switch (requestCode) {
2742            case REQUEST_CODE_CREATE_SLIDESHOW:
2743                if (data != null) {
2744                    WorkingMessage newMessage = WorkingMessage.load(this, data.getData());
2745                    if (newMessage != null) {
2746                        mWorkingMessage = newMessage;
2747                        mWorkingMessage.setConversation(mConversation);
2748                        MessagingNotification.setCurrentlyDisplayedThreadId(
2749                                mConversation.getThreadId());
2750                        drawTopPanel(false);
2751                        updateSendButtonState();
2752                        invalidateOptionsMenu();
2753                    }
2754                }
2755                break;
2756
2757            case REQUEST_CODE_TAKE_PICTURE: {
2758                // create a file based uri and pass to addImage(). We want to read the JPEG
2759                // data directly from file (using UriImage) instead of decoding it into a Bitmap,
2760                // which takes up too much memory and could easily lead to OOM.
2761                File file = new File(TempFileProvider.getScrapPath(this));
2762                Uri uri = Uri.fromFile(file);
2763
2764                // Remove the old captured picture's thumbnail from the cache
2765                MmsApp.getApplication().getThumbnailManager().removeThumbnail(uri);
2766
2767                addImageAsync(uri, false);
2768                break;
2769            }
2770
2771            case REQUEST_CODE_ATTACH_IMAGE: {
2772                if (data != null) {
2773                    addImageAsync(data.getData(), false);
2774                }
2775                break;
2776            }
2777
2778            case REQUEST_CODE_TAKE_VIDEO:
2779                Uri videoUri = TempFileProvider.renameScrapFile(".3gp", null, this);
2780                // Remove the old captured video's thumbnail from the cache
2781                MmsApp.getApplication().getThumbnailManager().removeThumbnail(videoUri);
2782
2783                addVideoAsync(videoUri, false);      // can handle null videoUri
2784                break;
2785
2786            case REQUEST_CODE_ATTACH_VIDEO:
2787                if (data != null) {
2788                    addVideoAsync(data.getData(), false);
2789                }
2790                break;
2791
2792            case REQUEST_CODE_ATTACH_SOUND: {
2793                Uri uri = (Uri) data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
2794                if (Settings.System.DEFAULT_RINGTONE_URI.equals(uri)) {
2795                    break;
2796                }
2797                addAudio(uri);
2798                break;
2799            }
2800
2801            case REQUEST_CODE_RECORD_SOUND:
2802                if (data != null) {
2803                    addAudio(data.getData());
2804                }
2805                break;
2806
2807            case REQUEST_CODE_ECM_EXIT_DIALOG:
2808                boolean outOfEmergencyMode = data.getBooleanExtra(EXIT_ECM_RESULT, false);
2809                if (outOfEmergencyMode) {
2810                    sendMessage(false);
2811                }
2812                break;
2813
2814            case REQUEST_CODE_PICK:
2815                if (data != null) {
2816                    processPickResult(data);
2817                }
2818                break;
2819
2820            default:
2821                if (LogTag.VERBOSE) log("bail due to unknown requestCode=" + requestCode);
2822                break;
2823        }
2824    }
2825
2826    private void processPickResult(final Intent data) {
2827        // The EXTRA_PHONE_URIS stores the phone's urls that were selected by user in the
2828        // multiple phone picker.
2829        final Parcelable[] uris =
2830            data.getParcelableArrayExtra(Intents.EXTRA_PHONE_URIS);
2831
2832        final int recipientCount = uris != null ? uris.length : 0;
2833
2834        final int recipientLimit = MmsConfig.getRecipientLimit();
2835        if (recipientLimit != Integer.MAX_VALUE && recipientCount > recipientLimit) {
2836            new AlertDialog.Builder(this)
2837                    .setMessage(getString(R.string.too_many_recipients, recipientCount, recipientLimit))
2838                    .setPositiveButton(android.R.string.ok, null)
2839                    .create().show();
2840            return;
2841        }
2842
2843        final Handler handler = new Handler();
2844        final ProgressDialog progressDialog = new ProgressDialog(this);
2845        progressDialog.setTitle(getText(R.string.pick_too_many_recipients));
2846        progressDialog.setMessage(getText(R.string.adding_recipients));
2847        progressDialog.setIndeterminate(true);
2848        progressDialog.setCancelable(false);
2849
2850        final Runnable showProgress = new Runnable() {
2851            @Override
2852            public void run() {
2853                progressDialog.show();
2854            }
2855        };
2856        // Only show the progress dialog if we can not finish off parsing the return data in 1s,
2857        // otherwise the dialog could flicker.
2858        handler.postDelayed(showProgress, 1000);
2859
2860        new Thread(new Runnable() {
2861            @Override
2862            public void run() {
2863                final ContactList list;
2864                 try {
2865                    list = ContactList.blockingGetByUris(uris);
2866                } finally {
2867                    handler.removeCallbacks(showProgress);
2868                    progressDialog.dismiss();
2869                }
2870                // TODO: there is already code to update the contact header widget and recipients
2871                // editor if the contacts change. we can re-use that code.
2872                final Runnable populateWorker = new Runnable() {
2873                    @Override
2874                    public void run() {
2875                        mRecipientsEditor.populate(list);
2876                        updateTitle(list);
2877                    }
2878                };
2879                handler.post(populateWorker);
2880            }
2881        }, "ComoseMessageActivity.processPickResult").start();
2882    }
2883
2884    private final ResizeImageResultCallback mResizeImageCallback = new ResizeImageResultCallback() {
2885        // TODO: make this produce a Uri, that's what we want anyway
2886        @Override
2887        public void onResizeResult(PduPart part, boolean append) {
2888            if (part == null) {
2889                handleAddAttachmentError(WorkingMessage.UNKNOWN_ERROR, R.string.type_picture);
2890                return;
2891            }
2892
2893            Context context = ComposeMessageActivity.this;
2894            PduPersister persister = PduPersister.getPduPersister(context);
2895            int result;
2896
2897            Uri messageUri = mWorkingMessage.saveAsMms(true);
2898            if (messageUri == null) {
2899                result = WorkingMessage.UNKNOWN_ERROR;
2900            } else {
2901                try {
2902                    Uri dataUri = persister.persistPart(part, ContentUris.parseId(messageUri));
2903                    result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, dataUri, append);
2904                    if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2905                        log("ResizeImageResultCallback: dataUri=" + dataUri);
2906                    }
2907                } catch (MmsException e) {
2908                    result = WorkingMessage.UNKNOWN_ERROR;
2909                }
2910            }
2911
2912            handleAddAttachmentError(result, R.string.type_picture);
2913        }
2914    };
2915
2916    private void handleAddAttachmentError(final int error, final int mediaTypeStringId) {
2917        if (error == WorkingMessage.OK) {
2918            return;
2919        }
2920        Log.d(TAG, "handleAddAttachmentError: " + error);
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                // Don't do this anymore. When we're showing chips, we don't want to switch from
3907                // chips to text.
3908//                if (mRecipientsEditor != null) {
3909//                    mRecipientsEditor.populate(recipients);
3910//                }
3911            }
3912        });
3913    }
3914
3915    private void addRecipientsListeners() {
3916        Contact.addListener(this);
3917    }
3918
3919    private void removeRecipientsListeners() {
3920        Contact.removeListener(this);
3921    }
3922
3923    public static Intent createIntent(Context context, long threadId) {
3924        Intent intent = new Intent(context, ComposeMessageActivity.class);
3925
3926        if (threadId > 0) {
3927            intent.setData(Conversation.getUri(threadId));
3928        }
3929
3930        return intent;
3931   }
3932
3933    private String getBody(Uri uri) {
3934        if (uri == null) {
3935            return null;
3936        }
3937        String urlStr = uri.getSchemeSpecificPart();
3938        if (!urlStr.contains("?")) {
3939            return null;
3940        }
3941        urlStr = urlStr.substring(urlStr.indexOf('?') + 1);
3942        String[] params = urlStr.split("&");
3943        for (String p : params) {
3944            if (p.startsWith("body=")) {
3945                try {
3946                    return URLDecoder.decode(p.substring(5), "UTF-8");
3947                } catch (UnsupportedEncodingException e) { }
3948            }
3949        }
3950        return null;
3951    }
3952}
3953