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