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