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