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