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