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