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