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