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