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