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