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