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