ComposeMessageActivity.java revision e7c8092e952d9d5edbc1c0b62e5f872c4d21d2b4
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.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.getMmsEnabled()) {
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.getMmsEnabled() && 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.mDeliveryStatus != MessageItem.DeliveryStatus.NONE || 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
1694    @Override
1695    protected void onResume() {
1696        super.onResume();
1697        startPresencePollingRequest();
1698    }
1699
1700    private void updateSendFailedNotification() {
1701        // updateSendFailedNotificationForThread makes a database call, so do the work off
1702        // of the ui thread.
1703        new Thread(new Runnable() {
1704            public void run() {
1705                MessagingNotification.updateSendFailedNotificationForThread(
1706                        ComposeMessageActivity.this, mThreadId);
1707            }
1708        }).run();
1709    }
1710
1711    @Override
1712    public void onSaveInstanceState(Bundle outState) {
1713        super.onSaveInstanceState(outState);
1714
1715        if (mThreadId > 0L) {
1716            if (LOCAL_LOGV) {
1717                Log.v(TAG, "ONFREEZE: thread_id: " + mThreadId);
1718            }
1719            outState.putLong("thread_id", mThreadId);
1720        }
1721
1722        if (LOCAL_LOGV) {
1723            Log.v(TAG, "ONFREEZE: address: " + mRecipientList.serialize());
1724        }
1725        outState.putString("address", mRecipientList.serialize());
1726
1727        removeSubjectIfEmpty();
1728        if (requiresMms()) {
1729            if (isSubjectEditorVisible()) {
1730                outState.putString("subject", mSubjectTextEditor.getText().toString());
1731            }
1732
1733            if (mMessageUri != null) {
1734                if (LOCAL_LOGV) {
1735                    Log.v(TAG, "ONFREEZE: mMessageUri: " + mMessageUri);
1736                }
1737                outState.putParcelable("msg_uri", mMessageUri);
1738            }
1739        } else {
1740            outState.putString("sms_body", mMsgText.toString());
1741        }
1742
1743        if (mExitOnSent) {
1744            outState.putBoolean("exit_on_sent", mExitOnSent);
1745        }
1746    }
1747
1748    private boolean isEmptyMessage() {
1749        if (requiresMms()) {
1750            return isEmptyMms();
1751        }
1752        return isEmptySms();
1753    }
1754
1755    private boolean isEmptySms() {
1756        return TextUtils.isEmpty(mMsgText);
1757    }
1758
1759    private boolean isEmptyMms() {
1760        return !(hasText() || hasSubject() || hasAttachment());
1761    }
1762
1763    private void removeSubjectIfEmpty() {
1764        // subject editor is visible without any contents.
1765        if ((mMessageState == HAS_SUBJECT) && !hasSubject()) {
1766            convertMessage(false);
1767        }
1768    }
1769
1770    @Override
1771    protected void onPause() {
1772        super.onPause();
1773        cancelPresencePollingRequests();
1774    }
1775
1776    @Override
1777    protected void onStop() {
1778        super.onStop();
1779
1780        if (mMsgListAdapter != null) {
1781            mMsgListAdapter.changeCursor(null);
1782        }
1783
1784        saveDraft();
1785
1786        // Cleanup the BroadcastReceiver.
1787        unregisterReceiver(mHttpProgressReceiver);
1788
1789        cleanupContactInfoCursor();
1790    }
1791
1792    @Override
1793    protected void onDestroy() {
1794        if (TRACE) {
1795            android.os.Debug.stopMethodTracing();
1796        }
1797
1798        super.onDestroy();
1799    }
1800
1801    @Override
1802    public void onConfigurationChanged(Configuration newConfig) {
1803        super.onConfigurationChanged(newConfig);
1804        if (LOCAL_LOGV) {
1805            Log.v(TAG, "onConfigurationChanged: " + newConfig);
1806        }
1807
1808        mIsKeyboardOpen = newConfig.keyboardHidden == KEYBOARDHIDDEN_NO;
1809        mIsLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
1810        onKeyboardStateChanged(mIsKeyboardOpen);
1811    }
1812
1813    private void onKeyboardStateChanged(boolean isKeyboardOpen) {
1814        // If the keyboard is hidden, don't show focus highlights for
1815        // things that cannot receive input.
1816        if (isKeyboardOpen) {
1817            if (mRecipientsEditor != null) {
1818                mRecipientsEditor.setFocusableInTouchMode(true);
1819            }
1820            if (mSubjectTextEditor != null) {
1821                mSubjectTextEditor.setFocusableInTouchMode(true);
1822            }
1823            mTextEditor.setFocusableInTouchMode(true);
1824            mTextEditor.setHint(R.string.type_to_compose_text_enter_to_send);
1825            initFocus();
1826        } else {
1827            if (mRecipientsEditor != null) {
1828                mRecipientsEditor.setFocusable(false);
1829            }
1830            if (mSubjectTextEditor != null) {
1831                mSubjectTextEditor.setFocusable(false);
1832            }
1833            mTextEditor.setFocusable(false);
1834            mTextEditor.setHint(R.string.open_keyboard_to_compose_message);
1835        }
1836    }
1837
1838    @Override
1839    public void onUserInteraction() {
1840        checkPendingNotification();
1841    }
1842
1843    @Override
1844    public void onWindowFocusChanged(boolean hasFocus) {
1845        if (hasFocus) {
1846            checkPendingNotification();
1847        }
1848    }
1849
1850    @Override
1851    public boolean onKeyDown(int keyCode, KeyEvent event) {
1852        switch (keyCode) {
1853            case KeyEvent.KEYCODE_DEL:
1854                if ((mMsgListAdapter != null) && mMsgListView.isFocused()) {
1855                    Cursor cursor;
1856                    try {
1857                        cursor = (Cursor) mMsgListView.getSelectedItem();
1858                    } catch (ClassCastException e) {
1859                        Log.e(TAG, "Unexpected ClassCastException.", e);
1860                        return super.onKeyDown(keyCode, event);
1861                    }
1862
1863                    if (cursor != null) {
1864                        DeleteMessageListener l = new DeleteMessageListener(
1865                                cursor.getLong(COLUMN_ID),
1866                                cursor.getString(COLUMN_MSG_TYPE));
1867                        confirmDeleteDialog(l, false);
1868                        return true;
1869                    }
1870                }
1871                break;
1872            case KeyEvent.KEYCODE_DPAD_CENTER:
1873            case KeyEvent.KEYCODE_ENTER:
1874                if (isPreparedForSending()) {
1875                    confirmSendMessageIfNeeded();
1876                    return true;
1877                }
1878                break;
1879            case KeyEvent.KEYCODE_BACK:
1880                exitComposeMessageActivity(new Runnable() {
1881                    public void run() {
1882                        finish();
1883                    }
1884                });
1885                return true;
1886        }
1887
1888        return super.onKeyDown(keyCode, event);
1889    }
1890
1891    private void exitComposeMessageActivity(final Runnable exit) {
1892        // If the message is empty, just quit -- finishing the
1893        // activity will cause an empty draft to be deleted.
1894        if (isEmptyMessage()) {
1895            exit.run();
1896            return;
1897        }
1898
1899        if (!hasValidRecipient()) {
1900            MessageUtils.showDiscardDraftConfirmDialog(this,
1901                    new DiscardDraftListener());
1902            return;
1903        }
1904
1905        mToastForDraftSave = true;
1906        exit.run();
1907    }
1908
1909    private void goToConversationList() {
1910        finish();
1911        startActivity(new Intent(this, ConversationList.class));
1912    }
1913
1914    private boolean isRecipientsEditorVisible() {
1915        return (null != mRecipientsEditor)
1916                    && (View.VISIBLE == mRecipientsEditor.getVisibility());
1917    }
1918
1919    private boolean isSubjectEditorVisible() {
1920        return (null != mSubjectTextEditor)
1921                    && (View.VISIBLE == mSubjectTextEditor.getVisibility());
1922    }
1923
1924    public void onAttachmentChanged(int newType, int oldType) {
1925        drawBottomPanel(newType);
1926        if (newType > AttachmentEditor.TEXT_ONLY) {
1927            updateState(HAS_ATTACHMENT, true);
1928        } else {
1929            convertMessageIfNeeded(HAS_ATTACHMENT, false);
1930        }
1931        updateSendButtonState();
1932    }
1933
1934    // We don't want to show the "call" option unless there is only one
1935    // recipient and it's a phone number.
1936    private boolean isRecipientCallable() {
1937        return (mRecipientList.size() == 1 && !mRecipientList.containsEmail());
1938    }
1939
1940    private void dialRecipient() {
1941        String number = mRecipientList.getSingleRecipientNumber();
1942        Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + number));
1943        startActivity(dialIntent);
1944    }
1945
1946    @Override
1947    public boolean onPrepareOptionsMenu(Menu menu) {
1948        menu.clear();
1949
1950        if (isRecipientCallable()) {
1951            menu.add(0, MENU_CALL_RECIPIENT, 0, R.string.menu_call).setIcon(
1952                com.android.internal.R.drawable.ic_menu_call);
1953        }
1954
1955        // Only add the "View contact" menu item when there's a single recipient and that
1956        // recipient is someone in contacts.
1957        long personId = getPersonId(mRecipientList.getSingleRecipient());
1958        if (personId > 0) {
1959            menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact).setIcon(
1960                    R.drawable.ic_menu_contact);
1961        }
1962
1963        if (MmsConfig.getMmsEnabled()) {
1964            if (!isSubjectEditorVisible()) {
1965                menu.add(0, MENU_ADD_SUBJECT, 0, R.string.add_subject).setIcon(
1966                        com.android.internal.R.drawable.ic_menu_edit);
1967            }
1968
1969            if ((mAttachmentEditor == null) || (mAttachmentEditor.getAttachmentType() == AttachmentEditor.TEXT_ONLY)) {
1970                menu.add(0, MENU_ADD_ATTACHMENT, 0, R.string.add_attachment).setIcon(
1971                        R.drawable.ic_menu_attachment);
1972            }
1973        }
1974
1975        if (isPreparedForSending()) {
1976            menu.add(0, MENU_SEND, 0, R.string.send).setIcon(android.R.drawable.ic_menu_send);
1977        }
1978
1979        menu.add(0, MENU_INSERT_SMILEY, 0, R.string.menu_insert_smiley).setIcon(
1980                com.android.internal.R.drawable.ic_menu_emoticons);
1981
1982        if (mMsgListAdapter.getCount() > 0) {
1983            // Removed search as part of b/1205708
1984            //menu.add(0, MENU_SEARCH, 0, R.string.menu_search).setIcon(
1985            //        R.drawable.ic_menu_search);
1986            Cursor cursor = mMsgListAdapter.getCursor();
1987            if ((null != cursor) && (cursor.getCount() > 0)) {
1988                menu.add(0, MENU_DELETE_THREAD, 0, R.string.delete_thread).setIcon(
1989                    android.R.drawable.ic_menu_delete);
1990            }
1991        } else {
1992            menu.add(0, MENU_DISCARD, 0, R.string.discard).setIcon(android.R.drawable.ic_menu_delete);
1993        }
1994
1995        menu.add(0, MENU_CONVERSATION_LIST, 0, R.string.all_threads).setIcon(
1996                com.android.internal.R.drawable.ic_menu_friendslist);
1997
1998        buildAddAddressToContactMenuItem(menu);
1999        return true;
2000    }
2001
2002    private void buildAddAddressToContactMenuItem(Menu menu) {
2003        if (mRecipientList.hasValidRecipient()) {
2004            // Look for the first recipient we don't have a contact for and create a menu item to
2005            // add the number to contacts.
2006            Iterator<Recipient> recipientIterator = mRecipientList.iterator();
2007            while (recipientIterator.hasNext()) {
2008                Recipient r = recipientIterator.next();
2009                long personId = getPersonId(r);
2010
2011                if (personId <= 0) {
2012                    Intent intent = ConversationList.createAddContactIntent(r.number);
2013                    menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, R.string.menu_add_to_contacts)
2014                        .setIcon(android.R.drawable.ic_menu_add)
2015                        .setIntent(intent);
2016                    break;
2017                }
2018            }
2019        }
2020    }
2021
2022    private void invalidateRecipientsInCache() {
2023        ContactInfoCache cache = ContactInfoCache.getInstance();
2024        Iterator<Recipient> recipientIterator = mRecipientList.iterator();
2025        while (recipientIterator.hasNext()) {
2026            Recipient r = recipientIterator.next();
2027            cache.invalidateContact(r.number);
2028        }
2029    }
2030
2031    private long getPersonId(Recipient r) {
2032        // The recipient doesn't always have a person_id. This can happen when a user adds
2033        // a contact in the middle of an activity after the recipient has already been loaded.
2034        if (r == null) {
2035            return -1;
2036        }
2037        if (r.person_id > 0) {
2038            return r.person_id;
2039        }
2040        ContactInfoCache.CacheEntry entry = ContactInfoCache.getInstance()
2041            .getContactInfo(this, r.number);
2042        if (entry.person_id > 0) {
2043            return entry.person_id;
2044        }
2045        return -1;
2046    }
2047
2048    @Override
2049    public boolean onOptionsItemSelected(MenuItem item) {
2050        switch (item.getItemId()) {
2051            case MENU_ADD_SUBJECT:
2052                convertMessageIfNeeded(HAS_SUBJECT, true);
2053                showSubjectEditor();
2054                mSubjectTextEditor.requestFocus();
2055                break;
2056            case MENU_ADD_ATTACHMENT:
2057                // Launch the add-attachment list dialog
2058                showAddAttachmentDialog();
2059                break;
2060            case MENU_DISCARD:
2061                discardTemporaryMessage();
2062                finish();
2063                break;
2064            case MENU_SEND:
2065                if (isPreparedForSending()) {
2066                    confirmSendMessageIfNeeded();
2067                }
2068                break;
2069            case MENU_SEARCH:
2070                onSearchRequested();
2071                break;
2072            case MENU_DELETE_THREAD:
2073                DeleteMessageListener l = new DeleteMessageListener(
2074                        getThreadUri(), true);
2075                confirmDeleteDialog(l, true);
2076                break;
2077            case MENU_CONVERSATION_LIST:
2078                exitComposeMessageActivity(new Runnable() {
2079                    public void run() {
2080                        goToConversationList();
2081                    }
2082                });
2083                break;
2084            case MENU_CALL_RECIPIENT:
2085                dialRecipient();
2086                break;
2087            case MENU_INSERT_SMILEY:
2088                showSmileyDialog();
2089                break;
2090            case MENU_VIEW_CONTACT:
2091                // View the contact for the first (and only) recipient.
2092                long personId = getPersonId(mRecipientList.getSingleRecipient());
2093                if (personId > 0) {
2094                    viewContact(personId);
2095                }
2096                break;
2097            case MENU_ADD_ADDRESS_TO_CONTACTS:
2098                return false;   // so the intent attached to the menu item will get launched.
2099        }
2100
2101        return true;
2102    }
2103
2104    private void addAttachment(int type) {
2105        switch (type) {
2106            case AttachmentTypeSelectorAdapter.ADD_IMAGE:
2107                MessageUtils.selectImage(this, REQUEST_CODE_ATTACH_IMAGE);
2108                break;
2109
2110            case AttachmentTypeSelectorAdapter.TAKE_PICTURE: {
2111                Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
2112                startActivityForResult(intent, REQUEST_CODE_TAKE_PICTURE);
2113            }
2114                break;
2115
2116            case AttachmentTypeSelectorAdapter.ADD_VIDEO:
2117                MessageUtils.selectVideo(this, REQUEST_CODE_ATTACH_VIDEO);
2118                break;
2119
2120            case AttachmentTypeSelectorAdapter.RECORD_VIDEO: {
2121                // Set video size limit. Subtract 1K for some text.
2122                long sizeLimit = MmsConfig.getMaxMessageSize() - 1024;
2123                if (mSlideshow != null) {
2124                    sizeLimit -= mSlideshow.getCurrentMessageSize();
2125                }
2126                if (sizeLimit > 0) {
2127                    Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
2128                    intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
2129                    intent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, sizeLimit);
2130                    startActivityForResult(intent, REQUEST_CODE_TAKE_VIDEO);
2131                }
2132                else {
2133                    Toast.makeText(this,
2134                            getString(R.string.message_too_big_for_video),
2135                            Toast.LENGTH_SHORT).show();
2136                }
2137            }
2138                break;
2139
2140            case AttachmentTypeSelectorAdapter.ADD_SOUND:
2141                MessageUtils.selectAudio(this, REQUEST_CODE_ATTACH_SOUND);
2142                break;
2143
2144            case AttachmentTypeSelectorAdapter.RECORD_SOUND:
2145                MessageUtils.recordSound(this, REQUEST_CODE_RECORD_SOUND);
2146                break;
2147
2148            case AttachmentTypeSelectorAdapter.ADD_SLIDESHOW: {
2149                boolean wasSms = !requiresMms();
2150
2151                // SlideshowEditActivity needs mMessageUri to work with.
2152                convertMessageIfNeeded(HAS_ATTACHMENT, true);
2153
2154                if (wasSms) {
2155                    // If we are converting from SMS, make sure the SMS
2156                    // text message gets imported into the first slide.
2157                    TextModel text = mSlideshow.get(0).getText();
2158                    if (text != null) {
2159                        text.setText(mMsgText);
2160                    }
2161                }
2162
2163                Intent intent = new Intent(this, SlideshowEditActivity.class);
2164                intent.setData(mMessageUri);
2165                startActivityForResult(intent, REQUEST_CODE_CREATE_SLIDESHOW);
2166            }
2167                break;
2168
2169            default:
2170                break;
2171        }
2172    }
2173
2174    private void showAddAttachmentDialog() {
2175        AlertDialog.Builder builder = new AlertDialog.Builder(this);
2176        builder.setIcon(R.drawable.ic_dialog_attach);
2177        builder.setTitle(R.string.add_attachment);
2178
2179        AttachmentTypeSelectorAdapter adapter = new AttachmentTypeSelectorAdapter(
2180                this, AttachmentTypeSelectorAdapter.MODE_WITH_SLIDESHOW);
2181
2182        builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
2183            public void onClick(DialogInterface dialog, int which) {
2184                addAttachment(which);
2185            }
2186        });
2187
2188        builder.show();
2189    }
2190
2191    @Override
2192    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
2193        if (LOCAL_LOGV) {
2194            Log.v(TAG, "onActivityResult: requestCode=" + requestCode
2195                    + ", resultCode=" + resultCode + ", data=" + data);
2196        }
2197        mWaitingForSubActivity = false;     // We're back!
2198
2199        if (resultCode != RESULT_OK) {
2200            // Make sure if there was an error that our message
2201            // type remains correct.
2202            convertMessageIfNeeded(HAS_ATTACHMENT, hasAttachment());
2203            return;
2204        }
2205
2206        if (!requiresMms()) {
2207            convertMessage(true);
2208        }
2209
2210        switch(requestCode) {
2211            case REQUEST_CODE_CREATE_SLIDESHOW:
2212                try {
2213                    // Refresh the slideshow model since it may be changed
2214                    // by the slideshow editor.
2215                    mSlideshow = SlideshowModel.createFromMessageUri(this, mMessageUri);
2216                } catch (MmsException e) {
2217                    Log.e(TAG, "Failed to load slideshow from " + mMessageUri);
2218                    Toast.makeText(this, getString(R.string.cannot_load_message),
2219                            Toast.LENGTH_SHORT).show();
2220                    return;
2221                }
2222
2223                // Find the most suitable type for the attachment.
2224                int attachmentType = MessageUtils.getAttachmentType(mSlideshow);
2225                switch (attachmentType) {
2226                    case AttachmentEditor.EMPTY:
2227                        fixEmptySlideshow(mSlideshow);
2228                        attachmentType = AttachmentEditor.TEXT_ONLY;
2229                        // fall-through
2230                    case AttachmentEditor.TEXT_ONLY:
2231                        mAttachmentEditor.setAttachment(mSlideshow, attachmentType);
2232                        convertMessageIfNeeded(HAS_ATTACHMENT, false);
2233                        drawBottomPanel(attachmentType);
2234                        return;
2235                    default:
2236                        mAttachmentEditor.setAttachment(mSlideshow, attachmentType);
2237                        break;
2238                }
2239
2240                drawBottomPanel(attachmentType);
2241                break;
2242
2243            case REQUEST_CODE_TAKE_PICTURE:
2244                Bitmap bitmap = (Bitmap) data.getParcelableExtra("data");
2245
2246                if (bitmap == null) {
2247                    Toast.makeText(this,
2248                            getResourcesString(R.string.failed_to_add_media, getPictureString()),
2249                            Toast.LENGTH_SHORT).show();
2250                    return;
2251                }
2252                addImage(bitmap);
2253                break;
2254
2255            case REQUEST_CODE_ATTACH_IMAGE:
2256                addImage(data.getData());
2257                break;
2258
2259            case REQUEST_CODE_TAKE_VIDEO:
2260            case REQUEST_CODE_ATTACH_VIDEO:
2261                addVideo(data.getData());
2262                break;
2263
2264            case REQUEST_CODE_ATTACH_SOUND:
2265            case REQUEST_CODE_RECORD_SOUND:
2266                Uri uri;
2267                if (requestCode == REQUEST_CODE_ATTACH_SOUND) {
2268                    uri = (Uri) data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
2269                    if (Settings.System.DEFAULT_RINGTONE_URI.equals(uri)) {
2270                        uri = null;
2271                    }
2272                } else {
2273                    uri = data.getData();
2274                }
2275
2276                if (uri == null) {
2277                    convertMessageIfNeeded(HAS_ATTACHMENT, hasAttachment());
2278                    return;
2279                }
2280
2281                try {
2282                    mAttachmentEditor.changeAudio(uri);
2283                    mAttachmentEditor.setAttachment(
2284                            mSlideshow, AttachmentEditor.AUDIO_ATTACHMENT);
2285                } catch (MmsException e) {
2286                    Log.e(TAG, "add audio failed", e);
2287                    Toast.makeText(this,
2288                            getResourcesString(R.string.failed_to_add_media, getAudioString()),
2289                            Toast.LENGTH_SHORT).show();
2290                } catch (UnsupportContentTypeException e) {
2291                    MessageUtils.showErrorDialog(ComposeMessageActivity.this,
2292                            getResourcesString(R.string.unsupported_media_format, getAudioString()),
2293                            getResourcesString(R.string.select_different_media, getAudioString()));
2294                } catch (ExceedMessageSizeException e) {
2295                    MessageUtils.showErrorDialog(ComposeMessageActivity.this,
2296                            getResourcesString(R.string.exceed_message_size_limitation),
2297                            getResourcesString(R.string.failed_to_add_media, getAudioString()));
2298                }
2299                break;
2300
2301            default:
2302                // TODO
2303                break;
2304        }
2305
2306        // Make sure if there was an error that our message type remains correct.
2307        if (!requiresMms()) {
2308            convertMessage(false);
2309        }
2310    }
2311
2312    private void addImage(Bitmap bitmap) {
2313        try {
2314            addImage(MessageUtils.saveBitmapAsPart(this, mMessageUri, bitmap));
2315        } catch (MmsException e) {
2316            handleAddImageFailure(e);
2317        }
2318    }
2319
2320    private final ResizeImageResultCallback mResizeImageCallback = new ResizeImageResultCallback() {
2321        public void onResizeResult(PduPart part) {
2322            Context context = ComposeMessageActivity.this;
2323            Resources r = context.getResources();
2324
2325            if (part == null) {
2326                Toast.makeText(context,
2327                        r.getString(R.string.failed_to_add_media, getPictureString()),
2328                        Toast.LENGTH_SHORT).show();
2329                return;
2330            }
2331
2332            convertMessageIfNeeded(HAS_ATTACHMENT, true);
2333            try {
2334                long messageId = ContentUris.parseId(mMessageUri);
2335                Uri newUri = mPersister.persistPart(part, messageId);
2336                mAttachmentEditor.changeImage(newUri);
2337                mAttachmentEditor.setAttachment(
2338                        mSlideshow, AttachmentEditor.IMAGE_ATTACHMENT);
2339            } catch (MmsException e) {
2340                Toast.makeText(context,
2341                        r.getString(R.string.failed_to_add_media, getPictureString()),
2342                        Toast.LENGTH_SHORT).show();
2343            } catch (UnsupportContentTypeException e) {
2344                MessageUtils.showErrorDialog(context,
2345                        r.getString(R.string.unsupported_media_format, getPictureString()),
2346                        r.getString(R.string.select_different_media, getPictureString()));
2347            } catch (ResolutionException e) {
2348                MessageUtils.showErrorDialog(context,
2349                        r.getString(R.string.failed_to_resize_image),
2350                        r.getString(R.string.resize_image_error_information));
2351            } catch (ExceedMessageSizeException e) {
2352                MessageUtils.showErrorDialog(context,
2353                        r.getString(R.string.exceed_message_size_limitation),
2354                        r.getString(R.string.failed_to_add_media, getPictureString()));
2355            }
2356        }
2357    };
2358
2359    private void addImage(Uri uri) {
2360        try {
2361            mAttachmentEditor.changeImage(uri);
2362            mAttachmentEditor.setAttachment(
2363                    mSlideshow, AttachmentEditor.IMAGE_ATTACHMENT);
2364        } catch (MmsException e) {
2365            handleAddImageFailure(e);
2366        } catch (UnsupportContentTypeException e) {
2367            MessageUtils.showErrorDialog(
2368                    ComposeMessageActivity.this,
2369                    getResourcesString(R.string.unsupported_media_format, getPictureString()),
2370                    getResourcesString(R.string.select_different_media, getPictureString()));
2371        } catch (ResolutionException e) {
2372            MessageUtils.resizeImageAsync(ComposeMessageActivity.this,
2373                    uri, mAttachmentEditorHandler, mResizeImageCallback);
2374        } catch (ExceedMessageSizeException e) {
2375            MessageUtils.showErrorDialog(
2376                    ComposeMessageActivity.this,
2377                    getResourcesString(R.string.exceed_message_size_limitation),
2378                    getResourcesString(R.string.failed_to_add_media, getPictureString()));
2379        }
2380    }
2381
2382    private void handleAddImageFailure(MmsException exception) {
2383        Log.e(TAG, "add image failed", exception);
2384        Toast.makeText(
2385                this,
2386                getResourcesString(R.string.failed_to_add_media, getPictureString()),
2387                Toast.LENGTH_SHORT).show();
2388    }
2389
2390    private void addVideo(Uri uri) {
2391        try {
2392            mAttachmentEditor.changeVideo(uri);
2393            mAttachmentEditor.setAttachment(
2394                    mSlideshow, AttachmentEditor.VIDEO_ATTACHMENT);
2395        } catch (MmsException e) {
2396            Log.e(TAG, "add video failed", e);
2397            Toast.makeText(this,
2398                    getResourcesString(R.string.failed_to_add_media, getVideoString()),
2399                    Toast.LENGTH_SHORT).show();
2400        } catch (UnsupportContentTypeException e) {
2401            MessageUtils.showErrorDialog(ComposeMessageActivity.this,
2402                    getResourcesString(R.string.unsupported_media_format, getVideoString()),
2403                    getResourcesString(R.string.select_different_media, getVideoString()));
2404        } catch (ExceedMessageSizeException e) {
2405            MessageUtils.showErrorDialog(ComposeMessageActivity.this,
2406                    getResourcesString(R.string.exceed_message_size_limitation),
2407                    getResourcesString(R.string.failed_to_add_media, getVideoString()));
2408        }
2409    }
2410
2411    private boolean handleForwardedMessage() {
2412        // If this is a forwarded message, it will have an Intent extra
2413        // indicating so.  If not, bail out.
2414        if (getIntent().getBooleanExtra("forwarded_message", false) == false) {
2415            return false;
2416        }
2417
2418        // If we are forwarding an MMS, mMessageUri will already be set.
2419        if (mMessageUri != null) {
2420            convertMessage(true);
2421        }
2422
2423        return true;
2424    }
2425
2426    private boolean handleSendIntent(Intent intent) {
2427        Bundle extras = intent.getExtras();
2428
2429        if (!Intent.ACTION_SEND.equals(intent.getAction()) || (extras == null)) {
2430            return false;
2431        }
2432
2433        if (extras.containsKey(Intent.EXTRA_STREAM)) {
2434            Uri uri = (Uri)extras.getParcelable(Intent.EXTRA_STREAM);
2435            if (uri != null) {
2436                convertMessage(true);
2437                if (intent.getType().startsWith("image/")) {
2438                    addImage(uri);
2439                } else if (intent.getType().startsWith("video/")) {
2440                    addVideo(uri);
2441                }
2442            }
2443            return true;
2444        } else if (extras.containsKey(Intent.EXTRA_TEXT)) {
2445            mMsgText = extras.getString(Intent.EXTRA_TEXT);
2446            return true;
2447        }
2448
2449        return false;
2450    }
2451
2452    private String getAudioString() {
2453        return getResourcesString(R.string.type_audio);
2454    }
2455
2456    private String getPictureString() {
2457        return getResourcesString(R.string.type_picture);
2458    }
2459
2460    private String getVideoString() {
2461        return getResourcesString(R.string.type_video);
2462    }
2463
2464    private String getResourcesString(int id, String mediaName) {
2465        Resources r = getResources();
2466        return r.getString(id, mediaName);
2467    }
2468
2469    private String getResourcesString(int id) {
2470        Resources r = getResources();
2471        return r.getString(id);
2472    }
2473
2474    private void fixEmptySlideshow(SlideshowModel slideshow) {
2475        if (slideshow.size() == 0) {
2476            SlideModel slide = new SlideModel(slideshow);
2477            slideshow.add(slide);
2478        }
2479
2480        if (!slideshow.get(0).hasText()) {
2481            TextModel text = new TextModel(
2482                    this, ContentType.TEXT_PLAIN, "text_0.txt",
2483                    slideshow.getLayout().getTextRegion());
2484            slideshow.get(0).add(text);
2485        }
2486    }
2487
2488    private void drawBottomPanel(int attachmentType) {
2489        // Reset the counter for text editor.
2490        resetCounter();
2491
2492        switch (attachmentType) {
2493            case AttachmentEditor.EMPTY:
2494                Log.e(TAG, "drawBottomPanel: empty attachment, mMessageState: " + mMessageState);
2495                throw new IllegalArgumentException(
2496                        "Type of the attachment may not be EMPTY.");
2497            case AttachmentEditor.SLIDESHOW_ATTACHMENT:
2498                mBottomPanel.setVisibility(View.GONE);
2499                findViewById(R.id.attachment_editor).requestFocus();
2500                return;
2501            default:
2502                mBottomPanel.setVisibility(View.VISIBLE);
2503                CharSequence text = null;
2504                if (requiresMms()) {
2505                    TextModel tm = mSlideshow.get(0).getText();
2506                    if (tm != null) {
2507                        text = tm.getText();
2508                    }
2509                } else {
2510                    text = mMsgText;
2511                }
2512
2513                if ((text != null) && !text.equals(mTextEditor.getText().toString())) {
2514                    mTextEditor.setText(text);
2515                }
2516        }
2517    }
2518
2519    //==========================================================
2520    // Interface methods
2521    //==========================================================
2522
2523    public void onClick(View v) {
2524        if ((v == mSendButton) && isPreparedForSending()) {
2525            confirmSendMessageIfNeeded();
2526        }
2527    }
2528
2529    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
2530        if (event != null) {
2531            if (!event.isShiftPressed()) {
2532                if (isPreparedForSending()) {
2533                    sendMessage();
2534                }
2535                return true;
2536            }
2537            return false;
2538        }
2539
2540        if (isPreparedForSending()) {
2541            confirmSendMessageIfNeeded();
2542        }
2543        return true;
2544    }
2545
2546    private final TextWatcher mTextEditorWatcher = new TextWatcher() {
2547        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2548        }
2549
2550        public void onTextChanged(CharSequence s, int start, int before, int count) {
2551            // This is a workaround for bug 1609057.  Since onUserInteraction() is
2552            // not called when the user touches the soft keyboard, we pretend it was
2553            // called when textfields changes.  This should be removed when the bug
2554            // is fixed.
2555            onUserInteraction();
2556
2557            if (requiresMms()) {
2558                // Update the content of the text model.
2559                TextModel text = mSlideshow.get(0).getText();
2560                if (text == null) {
2561                    text = new TextModel(
2562                                    ComposeMessageActivity.this,
2563                                    ContentType.TEXT_PLAIN,
2564                                    "text_0.txt",
2565                                    mSlideshow.getLayout().getTextRegion()
2566                           );
2567                    mSlideshow.get(0).add(text);
2568                }
2569
2570                text.setText(s);
2571            } else {
2572                mMsgText = s;
2573            }
2574
2575            updateSendButtonState();
2576
2577            updateCounter(s, start, before, count);
2578        }
2579
2580        public void afterTextChanged(Editable s) {
2581        }
2582    };
2583
2584    //==========================================================
2585    // Private methods
2586    //==========================================================
2587
2588    /**
2589     * Initialize all UI elements from resources.
2590     */
2591    private void initResourceRefs() {
2592        mMsgListView = (MessageListView) findViewById(R.id.history);
2593        mMsgListView.setDivider(null);      // no divider so we look like IM conversation.
2594        mBottomPanel = findViewById(R.id.bottom_panel);
2595        mTextEditor = (EditText) findViewById(R.id.embedded_text_editor);
2596        mTextEditor.setOnEditorActionListener(this);
2597        mTextEditor.addTextChangedListener(mTextEditorWatcher);
2598        mTextCounter = (TextView) findViewById(R.id.text_counter);
2599        mSendButton = (Button) findViewById(R.id.send_button);
2600        mSendButton.setOnClickListener(this);
2601        mTopPanel = findViewById(R.id.recipients_subject_linear);
2602    }
2603
2604    private void confirmDeleteDialog(OnClickListener listener, boolean allMessages) {
2605        AlertDialog.Builder builder = new AlertDialog.Builder(this);
2606        builder.setTitle(R.string.confirm_dialog_title);
2607        builder.setIcon(android.R.drawable.ic_dialog_alert);
2608        builder.setCancelable(true);
2609        builder.setMessage(allMessages
2610                ? R.string.confirm_delete_conversation
2611                : R.string.confirm_delete_message);
2612        builder.setPositiveButton(R.string.yes, listener);
2613        builder.setNegativeButton(R.string.no, null);
2614        builder.show();
2615    }
2616
2617    void undeliveredMessageDialog(long date) {
2618        String body;
2619        LinearLayout dialog = (LinearLayout) LayoutInflater.from(this).inflate(
2620                R.layout.retry_sending_dialog, null);
2621
2622        if (date >= 0) {
2623            body = getString(R.string.undelivered_msg_dialog_body,
2624                    MessageUtils.formatTimeStampString(this, date));
2625        } else {
2626            // FIXME: we can not get sms retry time.
2627            body = getString(R.string.undelivered_sms_dialog_body);
2628        }
2629
2630        ((TextView) dialog.findViewById(R.id.body_text_view)).setText(body);
2631
2632        Toast undeliveredDialog = new Toast(this);
2633        undeliveredDialog.setView(dialog);
2634        undeliveredDialog.setDuration(Toast.LENGTH_LONG);
2635        undeliveredDialog.show();
2636    }
2637
2638    private String deriveAddress(Intent intent) {
2639        Uri recipientUri = intent.getData();
2640        return (recipientUri == null)
2641                ? null : recipientUri.getSchemeSpecificPart();
2642    }
2643
2644    private Uri getThreadUri() {
2645        return ContentUris.withAppendedId(Threads.CONTENT_URI, mThreadId);
2646    }
2647
2648    private void startMsgListQuery() {
2649        if (mThreadId <= 0) {
2650            return;
2651        }
2652
2653        // Cancel any pending queries
2654        mBackgroundQueryHandler.cancelOperation(MESSAGE_LIST_QUERY_TOKEN);
2655        try {
2656            // Kick off the new query
2657            mBackgroundQueryHandler.startQuery(
2658                    MESSAGE_LIST_QUERY_TOKEN, null, getThreadUri(),
2659                    PROJECTION, null, null, null);
2660        } catch (SQLiteException e) {
2661            SqliteWrapper.checkSQLiteException(this, e);
2662        }
2663    }
2664
2665    private void initMessageList() {
2666        if (mMsgListAdapter != null) {
2667            return;
2668        }
2669
2670        // Initialize the list adapter with a null cursor.
2671        mMsgListAdapter = new MessageListAdapter(
2672                this, null, mMsgListView, true, getThreadType());
2673        mMsgListAdapter.setOnDataSetChangedListener(mDataSetChangedListener);
2674        mMsgListAdapter.setMsgListItemHandler(mMessageListItemHandler);
2675        mMsgListView.setAdapter(mMsgListAdapter);
2676        mMsgListView.setItemsCanFocus(false);
2677        mMsgListView.setVisibility(View.VISIBLE);
2678        mMsgListView.setOnCreateContextMenuListener(mMsgListMenuCreateListener);
2679        mMsgListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
2680            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
2681                ((MessageListItem) view).onMessageListItemClick();
2682            }
2683        });
2684    }
2685
2686    private void loadDraft() {
2687        // If we have no associated thread ID, there can't be a draft.
2688        if (mThreadId <= 0) {
2689            return;
2690        }
2691
2692        // If we already have text, don't stomp on it with the draft.
2693        if (!TextUtils.isEmpty(mMsgText)) {
2694            return;
2695        }
2696
2697        // If we know there is no draft, don't bother to look for one.
2698        if (!DraftCache.getInstance().hasDraft(mThreadId)) {
2699            return;
2700        }
2701
2702        // Try to load an SMS draft; if one does not exist,
2703        // load an MMS draft.
2704        mMsgText = readTemporarySmsMessage(mThreadId);
2705        if (TextUtils.isEmpty(mMsgText)) {
2706            if (readTemporaryMmsMessage(mThreadId)) {
2707                convertMessage(true);
2708            } else {
2709                Log.e(TAG, "no SMS or MMS drafts in thread " + mThreadId);
2710                return;
2711            }
2712        }
2713    }
2714
2715    private void asyncDelete(Uri uri, String selection, String[] selectionArgs) {
2716        if (LOCAL_LOGV) Log.v(TAG, "asyncDelete " + uri);
2717        mBackgroundQueryHandler.startDelete(0, null, uri, selection, selectionArgs);
2718    }
2719
2720    private void saveDraft() {
2721        // Convert back to SMS if we were only in MMS mode because there was
2722        // a subject and it is empty.
2723        removeSubjectIfEmpty();
2724
2725        // If we are in MMS mode but mMessageUri is null, the message has already
2726        // been discarded.  Just bail out early.
2727        if (requiresMms() && mMessageUri == null && !mWaitingForSubActivity) {
2728            return;
2729        }
2730
2731        // Throw the message out if it's empty, unless we're in the middle
2732        // of creating a slideshow or some other subactivity -- don't discard
2733        // the draft behind the subactivity's back.
2734        if (isEmptyMessage() && !mWaitingForSubActivity) {
2735            discardTemporaryMessage();
2736            DraftCache.getInstance().setDraftState(mThreadId, false);
2737            return;
2738        }
2739
2740        // If the user hasn't typed in a recipient and has managed to
2741        // get away from us (e.g. by pressing HOME), we don't have any
2742        // choice but to save an anonymous draft.  Fall through to the
2743        // normal case but set up an anonymous thread first.
2744        if (!hasValidRecipient()) {
2745            setThreadId(getOrCreateThreadId(new String[] {}));
2746        }
2747
2748        boolean savedAsDraft = false;
2749        if (requiresMms()) {
2750            if (isEmptyMms() && !mWaitingForSubActivity) {
2751                asyncDelete(mMessageUri, null, null);
2752            } else {
2753                asyncUpdateTemporaryMmsMessage(mRecipientList.getToNumbers());
2754                savedAsDraft = true;
2755            }
2756        } else {
2757            if (isEmptySms()) {
2758                asyncDeleteTemporarySmsMessage(mThreadId);
2759            } else {
2760                asyncUpdateTemporarySmsMessage(mRecipientList.getToNumbers(),
2761                        mMsgText.toString());
2762                savedAsDraft = true;
2763            }
2764        }
2765
2766        DraftCache.getInstance().setDraftState(mThreadId, savedAsDraft);
2767
2768        if (mToastForDraftSave && savedAsDraft) {
2769            Toast.makeText(this, R.string.message_saved_as_draft,
2770                    Toast.LENGTH_SHORT).show();
2771        }
2772    }
2773
2774    private static final String[] MMS_DRAFT_PROJECTION = {
2775        Mms._ID,        // 0
2776        Mms.SUBJECT     // 1
2777    };
2778
2779    private static final int MMS_ID_INDEX       = 0;
2780    private static final int MMS_SUBJECT_INDEX  = 1;
2781
2782    private boolean readTemporaryMmsMessage(long threadId) {
2783        Cursor cursor;
2784
2785        if (mMessageUri != null) {
2786            if (LOCAL_LOGV) {
2787                Log.v(TAG, "readTemporaryMmsMessage: already has message url " + mMessageUri);
2788            }
2789            return true;
2790        }
2791
2792        final String selection = Mms.THREAD_ID + " = " + threadId;
2793        cursor = SqliteWrapper.query(this, mContentResolver,
2794                Mms.Draft.CONTENT_URI, MMS_DRAFT_PROJECTION,
2795                selection, null, null);
2796
2797        try {
2798            if ((cursor.getCount() == 1) && cursor.moveToFirst()) {
2799                mMessageUri = ContentUris.withAppendedId(Mms.Draft.CONTENT_URI,
2800                        cursor.getLong(MMS_ID_INDEX));
2801
2802                mSubject = cursor.getString(MMS_SUBJECT_INDEX);
2803                if (!TextUtils.isEmpty(mSubject)) {
2804                    updateState(HAS_SUBJECT, true);
2805                }
2806                return true;
2807            }
2808        } finally {
2809            cursor.close();
2810        }
2811
2812        return false;
2813    }
2814
2815
2816    private Uri createTemporaryMmsMessage() throws MmsException {
2817        SendReq sendReq = new SendReq();
2818        fillMessageHeaders(sendReq);
2819        PduBody pb = mSlideshow.toPduBody();
2820        sendReq.setBody(pb);
2821        Uri res = mPersister.persist(sendReq, Mms.Draft.CONTENT_URI);
2822        mSlideshow.sync(pb);
2823        return res;
2824    }
2825
2826    private void asyncUpdateTemporaryMmsMessage(final String[] dests) {
2827        // PduPersister makes database calls and is known to ANR. Do the work on a
2828        // background thread.
2829        final SendReq sendReq = new SendReq();
2830        fillMessageHeaders(sendReq);
2831
2832        new Thread(new Runnable() {
2833            public void run() {
2834                setThreadId(getOrCreateThreadId(dests));
2835                DraftCache.getInstance().setDraftState(mThreadId, true);
2836                updateTemporaryMmsMessage(mMessageUri, mPersister,
2837                        mSlideshow, sendReq);
2838            }
2839        }).start();
2840
2841        // Be paranoid and delete any SMS drafts that might be lying around.
2842        asyncDeleteTemporarySmsMessage(mThreadId);
2843    }
2844
2845    public static void updateTemporaryMmsMessage(Uri uri, PduPersister persister,
2846            SlideshowModel slideshow, SendReq sendReq) {
2847        persister.updateHeaders(uri, sendReq);
2848        final PduBody pb = slideshow.toPduBody();
2849
2850        try {
2851            persister.updateParts(uri, pb);
2852        } catch (MmsException e) {
2853            Log.e(TAG, "updateTemporaryMmsMessage: cannot update message " + uri);
2854        }
2855
2856        slideshow.sync(pb);
2857    }
2858
2859    private static final String[] SMS_BODY_PROJECTION = { Sms._ID, Sms.BODY };
2860    private static final String SMS_DRAFT_WHERE = Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT;
2861
2862    /**
2863     * Reads a draft message for the given thread ID from the database,
2864     * if there is one, deletes it from the database, and returns it.
2865     * @return The draft message or an empty string.
2866     */
2867    private String readTemporarySmsMessage(long thread_id) {
2868        // If it's an invalid thread, don't bother.
2869        if (thread_id <= 0) {
2870            return "";
2871        }
2872
2873        Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id);
2874        String body = "";
2875
2876        Cursor c = SqliteWrapper.query(this, mContentResolver,
2877                        thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null);
2878
2879        if (c != null) {
2880            try {
2881                if (c.moveToFirst()) {
2882                    body = c.getString(1);
2883                }
2884            } finally {
2885                c.close();
2886            }
2887        }
2888
2889        // Clean out drafts for this thread -- if the recipient set changes,
2890        // we will lose track of the original draft and be unable to delete
2891        // it later.  The message will be re-saved if necessary upon exit of
2892        // the activity.
2893        SqliteWrapper.delete(this, mContentResolver, thread_uri, SMS_DRAFT_WHERE, null);
2894
2895        return body;
2896    }
2897
2898    private void asyncUpdateTemporarySmsMessage(final String[] dests, final String contents) {
2899        new Thread(new Runnable() {
2900            public void run() {
2901                setThreadId(getOrCreateThreadId(dests));
2902                DraftCache.getInstance().setDraftState(mThreadId, true);
2903                updateTemporarySmsMessage(mThreadId, contents);
2904            }
2905        }).start();
2906    }
2907
2908    private void updateTemporarySmsMessage(long thread_id, String contents) {
2909        // If we don't have a valid thread, there's nothing to do.
2910        if (thread_id <= 0) {
2911            return;
2912        }
2913
2914        // Don't bother saving an empty message.
2915        if (TextUtils.isEmpty(contents)) {
2916            // But delete the old temporary message if it's there.
2917            deleteTemporarySmsMessage(thread_id);
2918            return;
2919        }
2920
2921        Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id);
2922        Cursor c = SqliteWrapper.query(this, mContentResolver,
2923                thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null);
2924
2925        try {
2926            if (c.moveToFirst()) {
2927                ContentValues values = new ContentValues(1);
2928                values.put(Sms.BODY, contents);
2929                SqliteWrapper.update(this, mContentResolver, thread_uri, values,
2930                        SMS_DRAFT_WHERE, null);
2931            } else {
2932                ContentValues values = new ContentValues(3);
2933                values.put(Sms.THREAD_ID, thread_id);
2934                values.put(Sms.BODY, contents);
2935                values.put(Sms.TYPE, Sms.MESSAGE_TYPE_DRAFT);
2936                SqliteWrapper.insert(this, mContentResolver, Sms.CONTENT_URI, values);
2937                asyncDeleteTemporaryMmsMessage(thread_id);
2938            }
2939        } finally {
2940            c.close();
2941        }
2942    }
2943
2944    private void asyncDeleteTemporarySmsMessage(long threadId) {
2945        if (threadId > 0) {
2946            asyncDelete(ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
2947                SMS_DRAFT_WHERE, null);
2948        }
2949    }
2950
2951    private void deleteTemporarySmsMessage(long threadId) {
2952        SqliteWrapper.delete(this, mContentResolver,
2953                ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId),
2954                SMS_DRAFT_WHERE, null);
2955    }
2956
2957    private void asyncDeleteTemporaryMmsMessage(long threadId) {
2958        final String where = Mms.THREAD_ID + " = " + threadId;
2959        asyncDelete(Mms.Draft.CONTENT_URI, where, null);
2960    }
2961
2962    private void abandonDraftsAndFinish() {
2963        // If we are in MMS mode, first convert the message to SMS,
2964        // which will cause the MMS draft to be deleted.
2965        if (mMessageUri != null) {
2966            convertMessage(false);
2967        }
2968        // Now get rid of the SMS text to inhibit saving of a draft.
2969        mMsgText = "";
2970        finish();
2971    }
2972
2973    private String[] fillMessageHeaders(SendReq sendReq) {
2974        // Set the numbers in the 'TO' field.
2975        String[] dests = mRecipientList.getToNumbers();
2976        EncodedStringValue[] encodedNumbers = encodeStrings(dests);
2977        if (encodedNumbers != null) {
2978            sendReq.setTo(encodedNumbers);
2979        }
2980
2981        // Set the numbers in the 'BCC' field.
2982        encodedNumbers = encodeStrings(mRecipientList.getBccNumbers());
2983        if (encodedNumbers != null) {
2984            sendReq.setBcc(encodedNumbers);
2985        }
2986
2987        // Set the subject of the message.
2988        String subject = (mSubjectTextEditor == null)
2989                ? "" : mSubjectTextEditor.getText().toString();
2990        sendReq.setSubject(new EncodedStringValue(subject));
2991
2992        // Update the 'date' field of the message before sending it.
2993        sendReq.setDate(System.currentTimeMillis() / 1000L);
2994
2995        return dests;
2996    }
2997
2998    private boolean hasRecipient() {
2999        return hasValidRecipient() || hasInvalidRecipient();
3000    }
3001
3002    private boolean hasValidRecipient() {
3003        // If someone is in the recipient list, or if a valid recipient is
3004        // currently in the recipients editor, we have recipients.
3005        return (mRecipientList.hasValidRecipient())
3006                 || ((mRecipientsEditor != null)
3007                         && Recipient.isValid(mRecipientsEditor.getText().toString()));
3008    }
3009
3010    private boolean hasInvalidRecipient() {
3011        return (mRecipientList.hasInvalidRecipient())
3012                 || ((mRecipientsEditor != null)
3013                         && !TextUtils.isEmpty(mRecipientsEditor.getText().toString())
3014                         && !Recipient.isValid(mRecipientsEditor.getText().toString()));
3015    }
3016
3017    private boolean hasText() {
3018        return mTextEditor.length() > 0;
3019    }
3020
3021    private boolean hasSubject() {
3022        return (null != mSubjectTextEditor)
3023                && !TextUtils.isEmpty(mSubjectTextEditor.getText().toString());
3024    }
3025
3026    private boolean isPreparedForSending() {
3027        return hasRecipient() && (hasAttachment() || hasText());
3028    }
3029
3030    private long getOrCreateThreadId(String[] numbers) {
3031        HashSet<String> recipients = new HashSet<String>();
3032        recipients.addAll(Arrays.asList(numbers));
3033        return Threads.getOrCreateThreadId(this, recipients);
3034    }
3035
3036
3037    private void sendMessage() {
3038        // Need this for both SMS and MMS.
3039        final String[] dests = mRecipientList.getToNumbers();
3040
3041        // removeSubjectIfEmpty will convert a message that is solely an MMS
3042        // message because it has an empty subject back into an SMS message.
3043        // It doesn't notify the user of the conversion.
3044        removeSubjectIfEmpty();
3045        if (requiresMms()) {
3046            // Make local copies of the bits we need for sending a message,
3047            // because we will be doing it off of the main thread, which will
3048            // immediately continue on to resetting some of this state.
3049            final Uri mmsUri = mMessageUri;
3050            final PduPersister persister = mPersister;
3051            final SlideshowModel slideshow = mSlideshow;
3052            final SendReq sendReq = new SendReq();
3053            fillMessageHeaders(sendReq);
3054
3055            // Make sure the text in slide 0 is no longer holding onto a reference to the text
3056            // in the message text box.
3057            slideshow.prepareForSend();
3058
3059            // Do the dirty work of sending the message off of the main UI thread.
3060            new Thread(new Runnable() {
3061                public void run() {
3062                    sendMmsWorker(dests, mmsUri, persister, slideshow, sendReq);
3063                }
3064            }).start();
3065        } else {
3066            // Same rules apply as above.
3067            final String msgText = mMsgText.toString();
3068            new Thread(new Runnable() {
3069                public void run() {
3070                    sendSmsWorker(dests, msgText);
3071                }
3072            }).start();
3073        }
3074
3075        if (mExitOnSent) {
3076            // If we are supposed to exit after a message is sent,
3077            // clear out the text and URIs to inhibit saving of any
3078            // drafts and call finish().
3079            mMsgText = "";
3080            mMessageUri = null;
3081            finish();
3082        } else {
3083            // Otherwise, reset the UI to be ready for the next message.
3084            resetMessage();
3085        }
3086    }
3087
3088    /**
3089     * Do the actual work of sending a message.  Runs outside of the main thread.
3090     */
3091    private void sendSmsWorker(String[] dests, String msgText) {
3092        // Make sure we are still using the correct thread ID for our
3093        // recipient set.
3094        long threadId = getOrCreateThreadId(dests);
3095
3096        MessageSender sender = new SmsMessageSender(this, dests, msgText, threadId);
3097        try {
3098            sender.sendMessage(threadId);
3099            setThreadId(threadId);
3100            startMsgListQuery();
3101        } catch (Exception e) {
3102            Log.e(TAG, "Failed to send SMS message, threadId=" + threadId, e);
3103        }
3104    }
3105
3106    private void sendMmsWorker(String[] dests, Uri mmsUri, PduPersister persister,
3107                               SlideshowModel slideshow, SendReq sendReq) {
3108        // Make sure we are still using the correct thread ID for our
3109        // recipient set.
3110        long threadId = getOrCreateThreadId(dests);
3111
3112        if (LOCAL_LOGV) Log.v(TAG, "sendMmsWorker: update temporary MMS message " + mmsUri);
3113
3114        // Sync the MMS message in progress to disk.
3115        updateTemporaryMmsMessage(mmsUri, persister, slideshow, sendReq);
3116        // Be paranoid and clean any draft SMS up.
3117        deleteTemporarySmsMessage(threadId);
3118
3119        MessageSender sender = new MmsMessageSender(this, mmsUri);
3120        try {
3121            if (!sender.sendMessage(threadId)) {
3122                // The message was sent through SMS protocol, we should
3123                // delete the copy which was previously saved in MMS drafts.
3124                SqliteWrapper.delete(this, mContentResolver, mmsUri, null, null);
3125            }
3126
3127            setThreadId(threadId);
3128            startMsgListQuery();
3129        } catch (Exception e) {
3130            Log.e(TAG, "Failed to send message: " + mmsUri + ", threadId=" + threadId, e);
3131        }
3132    }
3133
3134    private void resetMessage() {
3135        // Make the attachment editor hide its view before we destroy it.
3136        if (mAttachmentEditor != null) {
3137            mAttachmentEditor.hideView();
3138        }
3139
3140        // Focus to the text editor.
3141        mTextEditor.requestFocus();
3142
3143        // We have to remove the text change listener while the text editor gets cleared and
3144        // we subsequently turn the message back into SMS. When the listener is listening while
3145        // doing the clearing, it's fighting to update its counts and itself try and turn
3146        // the message one way or the other.
3147        mTextEditor.removeTextChangedListener(mTextEditorWatcher);
3148
3149        // RECIPIENTS_REQUIRE_MMS is the only state flag that is valid
3150        // when starting a new message, so preserve only that.
3151        mMessageState &= RECIPIENTS_REQUIRE_MMS;
3152
3153        // Clear the text box.
3154        TextKeyListener.clear(mTextEditor.getText());
3155
3156        // Clear out the slideshow and message URI.  New ones will be
3157        // created if we are starting a new message as MMS.
3158        mSlideshow = null;
3159        mMessageUri = null;
3160
3161        // Empty out text.
3162        mMsgText = "";
3163
3164        // Convert back to SMS if necessary, or if we still need to
3165        // be in MMS mode, reset the MMS components.
3166        if (!requiresMms()) {
3167            // Start a new message as an SMS.
3168            convertMessage(false);
3169        } else {
3170            // Start a new message as an MMS.
3171            resetMmsComponents();
3172        }
3173
3174        drawBottomPanel(AttachmentEditor.TEXT_ONLY);
3175
3176        // "Or not", in this case.
3177        updateSendButtonState();
3178
3179        // Hide the recipients editor.
3180        if (mRecipientsEditor != null) {
3181            mRecipientsEditor.setVisibility(View.GONE);
3182            hideTopPanelIfNecessary();
3183        }
3184
3185        // Our changes are done. Let the listener respond to text changes once again.
3186        mTextEditor.addTextChangedListener(mTextEditorWatcher);
3187
3188        // Close the soft on-screen keyboard if we're in landscape mode so the user can see the
3189        // conversation.
3190        if (mIsLandscape) {
3191            InputMethodManager inputMethodManager =
3192                (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
3193
3194            inputMethodManager.hideSoftInputFromWindow(mTextEditor.getWindowToken(), 0);
3195        }
3196   }
3197
3198    private void updateSendButtonState() {
3199        boolean enable = false;
3200        if (isPreparedForSending()) {
3201            // When the type of attachment is slideshow, we should
3202            // also hide the 'Send' button since the slideshow view
3203            // already has a 'Send' button embedded.
3204            if ((mAttachmentEditor == null) ||
3205                (mAttachmentEditor.getAttachmentType() != AttachmentEditor.SLIDESHOW_ATTACHMENT)) {
3206                enable = true;
3207            } else {
3208                mAttachmentEditor.setCanSend(true);
3209            }
3210        } else if (null != mAttachmentEditor){
3211            mAttachmentEditor.setCanSend(false);
3212        }
3213
3214        mSendButton.setEnabled(enable);
3215        mSendButton.setFocusable(enable);
3216    }
3217
3218    private long getMessageDate(Uri uri) {
3219        if (uri != null) {
3220            Cursor cursor = SqliteWrapper.query(this, mContentResolver,
3221                    uri, new String[] { Mms.DATE }, null, null, null);
3222            if (cursor != null) {
3223                try {
3224                    if ((cursor.getCount() == 1) && cursor.moveToFirst()) {
3225                        return cursor.getLong(0) * 1000L;
3226                    }
3227                } finally {
3228                    cursor.close();
3229                }
3230            }
3231        }
3232        return NO_DATE_FOR_DIALOG;
3233    }
3234
3235    private void setSubjectFromIntent(Intent intent) {
3236        String subject = intent.getStringExtra("subject");
3237        if ( !TextUtils.isEmpty(subject) ) {
3238            mSubject = subject;
3239        }
3240    }
3241
3242    private void setThreadId(long threadId) {
3243        mThreadId = threadId;
3244    }
3245
3246    private void initActivityState(Bundle savedInstanceState, Intent intent) {
3247        if (savedInstanceState != null) {
3248            setThreadId(savedInstanceState.getLong("thread_id", 0));
3249            mMessageUri = (Uri) savedInstanceState.getParcelable("msg_uri");
3250            mExternalAddress = savedInstanceState.getString("address");
3251            mExitOnSent = savedInstanceState.getBoolean("exit_on_sent", false);
3252            mSubject = savedInstanceState.getString("subject");
3253            mMsgText = savedInstanceState.getString("sms_body");
3254        } else {
3255            setThreadId(intent.getLongExtra("thread_id", 0));
3256            mMessageUri = (Uri) intent.getParcelableExtra("msg_uri");
3257            if ((mMessageUri == null) && (mThreadId == 0)) {
3258                // If we haven't been given a thread id or a URI in the extras,
3259                // get it out of the intent.
3260                Uri uri = intent.getData();
3261                if ((uri != null) && (uri.getPathSegments().size() >= 2)) {
3262                    try {
3263                        setThreadId(Long.parseLong(uri.getPathSegments().get(1)));
3264                    } catch (NumberFormatException exception) {
3265                        Log.e(TAG, "Thread ID must be a Long.");
3266                    }
3267                }
3268            }
3269            mExternalAddress = intent.getStringExtra("address");
3270            mExitOnSent = intent.getBooleanExtra("exit_on_sent", false);
3271            mMsgText = intent.getStringExtra("sms_body");
3272
3273            setSubjectFromIntent(intent);
3274        }
3275
3276        if (!TextUtils.isEmpty(mSubject)) {
3277            updateState(HAS_SUBJECT, true);
3278        }
3279
3280        // If there was not a body already, start with a blank one.
3281        if (mMsgText == null) {
3282            mMsgText = "";
3283        }
3284
3285        if (mExternalAddress == null) {
3286            if (mThreadId > 0L) {
3287                mExternalAddress = MessageUtils.getAddressByThreadId(this, mThreadId);
3288            } else {
3289                mExternalAddress = deriveAddress(intent);
3290                // Even if we end up creating a thread here and the user
3291                // discards the message, we will clean it up later when we
3292                // delete obsolete threads.
3293                if (!TextUtils.isEmpty(mExternalAddress)) {
3294                    setThreadId(getOrCreateThreadId(new String[] { mExternalAddress }));
3295                }
3296            }
3297        }
3298    }
3299
3300    private int getThreadType() {
3301        boolean isMms = (mMessageUri != null) || requiresMms();
3302
3303        return (!isMms
3304                && (mRecipientList != null)
3305                && (mRecipientList.size() > 1))
3306                ? Threads.BROADCAST_THREAD
3307                : Threads.COMMON_THREAD;
3308    }
3309
3310    private void updateWindowTitle() {
3311        StringBuilder sb = new StringBuilder();
3312        Iterator<Recipient> iter = mRecipientList.iterator();
3313        while (iter.hasNext()) {
3314            Recipient r = iter.next();
3315            sb.append(r.nameAndNumber).append(", ");
3316        }
3317
3318        ContactInfoCache cache = ContactInfoCache.getInstance();
3319        String[] values = mRecipientList.getBccNumbers();
3320        if (values.length > 0) {
3321            sb.append("Bcc: ");
3322            for (String v : values) {
3323                sb.append(cache.getContactName(this, v)).append(", ");
3324            }
3325        }
3326
3327        if (sb.length() > 0) {
3328            // Delete the trailing ", " characters.
3329            int tail = sb.length() - 2;
3330            setTitle(sb.delete(tail, tail + 2).toString());
3331        } else {
3332            setTitle(getString(R.string.compose_title));
3333        }
3334    }
3335
3336    private void initFocus() {
3337        if (!mIsKeyboardOpen) {
3338            return;
3339        }
3340
3341        // If the recipients editor is visible, there is nothing in it,
3342        // and the text editor is not already focused, focus the
3343        // recipients editor.
3344        if (isRecipientsEditorVisible() && TextUtils.isEmpty(mRecipientsEditor.getText())
3345                                        && !mTextEditor.isFocused()) {
3346            mRecipientsEditor.requestFocus();
3347            return;
3348        }
3349
3350        // If we decided not to focus the recipients editor, focus the text editor.
3351        mTextEditor.requestFocus();
3352    }
3353
3354    private final MessageListAdapter.OnDataSetChangedListener
3355                    mDataSetChangedListener = new MessageListAdapter.OnDataSetChangedListener() {
3356        public void onDataSetChanged(MessageListAdapter adapter) {
3357            mPossiblePendingNotification = true;
3358        }
3359    };
3360
3361    private void checkPendingNotification() {
3362        if (mPossiblePendingNotification && hasWindowFocus()) {
3363            markAsRead(mThreadId);
3364            mPossiblePendingNotification = false;
3365        }
3366    }
3367
3368    private void markAsRead(long threadId) {
3369        if (threadId <= 0) {
3370            return;
3371        }
3372
3373        Uri threadUri = ContentUris.withAppendedId(Threads.CONTENT_URI, threadId);
3374        ContentValues values = new ContentValues(1);
3375        values.put("read", 1);
3376        String where = "read = 0";
3377
3378        mBackgroundQueryHandler.startUpdate(MARK_AS_READ_TOKEN, null,
3379                                            threadUri, values, where, null);
3380    }
3381
3382    private final class BackgroundQueryHandler extends AsyncQueryHandler {
3383        public BackgroundQueryHandler(ContentResolver contentResolver) {
3384            super(contentResolver);
3385        }
3386
3387        @Override
3388        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
3389            switch(token) {
3390                case MESSAGE_LIST_QUERY_TOKEN:
3391                    mMsgListAdapter.changeCursor(cursor);
3392
3393                    // Once we have completed the query for the message history, if
3394                    // there is nothing in the cursor and we are not composing a new
3395                    // message, we must be editing a draft in a new conversation.
3396                    // Show the recipients editor to give the user a chance to add
3397                    // more people before the conversation begins.
3398                    if (cursor.getCount() == 0 && !isRecipientsEditorVisible()) {
3399                        initRecipientsEditor();
3400                    }
3401
3402                    // FIXME: freshing layout changes the focused view to an unexpected
3403                    // one, set it back to TextEditor forcely.
3404                    mTextEditor.requestFocus();
3405
3406                    return;
3407
3408                case CALLER_ID_QUERY_TOKEN:
3409                case EMAIL_CONTACT_QUERY_TOKEN:
3410                    cleanupContactInfoCursor();
3411                    mContactInfoCursor = cursor;
3412                    updateContactInfo();
3413                    startPresencePollingRequest();
3414                    return;
3415
3416            }
3417        }
3418
3419        @Override
3420        protected void onDeleteComplete(int token, Object cookie, int result) {
3421            switch(token) {
3422            case DELETE_MESSAGE_TOKEN:
3423            case DELETE_CONVERSATION_TOKEN:
3424                // Update the notification for new messages since they
3425                // may be deleted.
3426                MessagingNotification.updateNewMessageIndicator(
3427                        ComposeMessageActivity.this);
3428                // Update the notification for failed messages since they
3429                // may be deleted.
3430                updateSendFailedNotification();
3431                break;
3432            }
3433
3434            if (token == DELETE_CONVERSATION_TOKEN) {
3435                ComposeMessageActivity.this.abandonDraftsAndFinish();
3436            }
3437        }
3438
3439        @Override
3440        protected void onUpdateComplete(int token, Object cookie, int result) {
3441            switch(token) {
3442            case MARK_AS_READ_TOKEN:
3443                MessagingNotification.updateAllNotifications(ComposeMessageActivity.this);
3444                break;
3445            }
3446        }
3447    }
3448
3449    private void showSmileyDialog() {
3450        if (mSmileyDialog == null) {
3451            int[] icons = SmileyParser.DEFAULT_SMILEY_RES_IDS;
3452            String[] names = getResources().getStringArray(
3453                    SmileyParser.DEFAULT_SMILEY_NAMES);
3454            final String[] texts = getResources().getStringArray(
3455                    SmileyParser.DEFAULT_SMILEY_TEXTS);
3456
3457            final int N = names.length;
3458
3459            List<Map<String, ?>> entries = new ArrayList<Map<String, ?>>();
3460            for (int i = 0; i < N; i++) {
3461                // We might have different ASCII for the same icon, skip it if
3462                // the icon is already added.
3463                boolean added = false;
3464                for (int j = 0; j < i; j++) {
3465                    if (icons[i] == icons[j]) {
3466                        added = true;
3467                        break;
3468                    }
3469                }
3470                if (!added) {
3471                    HashMap<String, Object> entry = new HashMap<String, Object>();
3472
3473                    entry. put("icon", icons[i]);
3474                    entry. put("name", names[i]);
3475                    entry.put("text", texts[i]);
3476
3477                    entries.add(entry);
3478                }
3479            }
3480
3481            final SimpleAdapter a = new SimpleAdapter(
3482                    this,
3483                    entries,
3484                    R.layout.smiley_menu_item,
3485                    new String[] {"icon", "name", "text"},
3486                    new int[] {R.id.smiley_icon, R.id.smiley_name, R.id.smiley_text});
3487            SimpleAdapter.ViewBinder viewBinder = new SimpleAdapter.ViewBinder() {
3488                public boolean setViewValue(View view, Object data, String textRepresentation) {
3489                    if (view instanceof ImageView) {
3490                        Drawable img = getResources().getDrawable((Integer)data);
3491                        ((ImageView)view).setImageDrawable(img);
3492                        return true;
3493                    }
3494                    return false;
3495                }
3496            };
3497            a.setViewBinder(viewBinder);
3498
3499            AlertDialog.Builder b = new AlertDialog.Builder(this);
3500
3501            b.setTitle(getString(R.string.menu_insert_smiley));
3502
3503            b.setCancelable(true);
3504            b.setAdapter(a, new DialogInterface.OnClickListener() {
3505                public final void onClick(DialogInterface dialog, int which) {
3506                    HashMap<String, Object> item = (HashMap<String, Object>) a.getItem(which);
3507                    mTextEditor.append((String)item.get("text"));
3508                }
3509            });
3510
3511            mSmileyDialog = b.create();
3512        }
3513
3514        mSmileyDialog.show();
3515    }
3516
3517    private void cleanupContactInfoCursor() {
3518        if (mContactInfoCursor != null) {
3519            mContactInfoCursor.close();
3520        }
3521    }
3522
3523    private void cancelPresencePollingRequests() {
3524        mPresencePollingHandler.removeMessages(REFRESH_PRESENCE);
3525    }
3526
3527    private void startPresencePollingRequest() {
3528        mPresencePollingHandler.sendEmptyMessageDelayed(REFRESH_PRESENCE,
3529                60 * 1000); // refresh every minute
3530    }
3531
3532    private void startQueryForContactInfo() {
3533        String number = mRecipientList.getSingleRecipientNumber();
3534        cancelPresencePollingRequests();    // make sure there are no outstanding polling requests
3535        if (TextUtils.isEmpty(number)) {
3536            setPresenceIcon(0);
3537            startPresencePollingRequest();
3538            return;
3539        }
3540
3541        mContactInfoSelectionArgs[0] = number;
3542
3543        if (Mms.isEmailAddress(number)) {
3544            // Cancel any pending queries
3545            mBackgroundQueryHandler.cancelOperation(EMAIL_CONTACT_QUERY_TOKEN);
3546
3547            mBackgroundQueryHandler.startQuery(EMAIL_CONTACT_QUERY_TOKEN, null,
3548                    METHOD_WITH_PRESENCE_URI,
3549                    EMAIL_QUERY_PROJECTION,
3550                    METHOD_LOOKUP,
3551                    mContactInfoSelectionArgs,
3552                    null);
3553        } else {
3554            // Cancel any pending queries
3555            mBackgroundQueryHandler.cancelOperation(CALLER_ID_QUERY_TOKEN);
3556
3557            mBackgroundQueryHandler.startQuery(CALLER_ID_QUERY_TOKEN, null,
3558                    PHONES_WITH_PRESENCE_URI,
3559                    CALLER_ID_PROJECTION,
3560                    NUMBER_LOOKUP,
3561                    mContactInfoSelectionArgs,
3562                    null);
3563        }
3564    }
3565
3566    private void updateContactInfo() {
3567        boolean updated = false;
3568        if (mContactInfoCursor != null && mContactInfoCursor.moveToFirst()) {
3569            mPresenceStatus = mContactInfoCursor.getInt(PRESENCE_STATUS_COLUMN);
3570            if (mPresenceStatus != Contacts.People.OFFLINE) {
3571                int presenceIcon = Presence.getPresenceIconResourceId(mPresenceStatus);
3572                setPresenceIcon(presenceIcon);
3573                updated = true;
3574            }
3575        }
3576        if (!updated) {
3577            setPresenceIcon(0);
3578        }
3579    }
3580
3581}
3582
3583
3584
3585
3586
3587