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