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