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