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