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 java.io.File;
30import java.io.FileInputStream;
31import java.io.FileOutputStream;
32import java.io.IOException;
33import java.io.InputStream;
34import java.io.UnsupportedEncodingException;
35import java.net.URLDecoder;
36import java.util.ArrayList;
37import java.util.HashMap;
38import java.util.HashSet;
39import java.util.List;
40import java.util.Map;
41import java.util.regex.Pattern;
42
43import android.app.ActionBar;
44import android.app.Activity;
45import android.app.AlertDialog;
46import android.app.ProgressDialog;
47import android.content.ActivityNotFoundException;
48import android.content.BroadcastReceiver;
49import android.content.ClipData;
50import android.content.ClipboardManager;
51import android.content.ContentResolver;
52import android.content.ContentUris;
53import android.content.ContentValues;
54import android.content.Context;
55import android.content.DialogInterface;
56import android.content.DialogInterface.OnClickListener;
57import android.content.Intent;
58import android.content.IntentFilter;
59import android.content.res.Configuration;
60import android.content.res.Resources;
61import android.database.Cursor;
62import android.database.sqlite.SQLiteException;
63import android.database.sqlite.SqliteWrapper;
64import android.drm.DrmStore;
65import android.graphics.drawable.Drawable;
66import android.media.RingtoneManager;
67import android.net.Uri;
68import android.os.AsyncTask;
69import android.os.Bundle;
70import android.os.Environment;
71import android.os.Handler;
72import android.os.Message;
73import android.os.Parcelable;
74import android.os.SystemProperties;
75import android.provider.ContactsContract;
76import android.provider.ContactsContract.QuickContact;
77import android.provider.Telephony;
78import android.provider.ContactsContract.CommonDataKinds.Email;
79import android.provider.ContactsContract.CommonDataKinds.Phone;
80import android.provider.ContactsContract.Contacts;
81import android.provider.ContactsContract.Intents;
82import android.provider.MediaStore.Images;
83import android.provider.MediaStore.Video;
84import android.provider.Settings;
85import android.provider.Telephony.Mms;
86import android.provider.Telephony.Sms;
87import android.telephony.PhoneNumberUtils;
88import android.telephony.SmsMessage;
89import android.text.Editable;
90import android.text.InputFilter;
91import android.text.InputFilter.LengthFilter;
92import android.text.SpannableString;
93import android.text.Spanned;
94import android.text.TextUtils;
95import android.text.TextWatcher;
96import android.text.method.TextKeyListener;
97import android.text.style.URLSpan;
98import android.text.util.Linkify;
99import android.util.Log;
100import android.view.ContextMenu;
101import android.view.ContextMenu.ContextMenuInfo;
102import android.view.KeyEvent;
103import android.view.Menu;
104import android.view.MenuItem;
105import android.view.View;
106import android.view.View.OnCreateContextMenuListener;
107import android.view.View.OnKeyListener;
108import android.view.ViewStub;
109import android.view.WindowManager;
110import android.view.inputmethod.InputMethodManager;
111import android.webkit.MimeTypeMap;
112import android.widget.AdapterView;
113import android.widget.EditText;
114import android.widget.ImageButton;
115import android.widget.ImageView;
116import android.widget.ListView;
117import android.widget.SimpleAdapter;
118import android.widget.TextView;
119import android.widget.Toast;
120
121import com.android.internal.telephony.TelephonyIntents;
122import com.android.internal.telephony.TelephonyProperties;
123import com.android.mms.LogTag;
124import com.android.mms.MmsApp;
125import com.android.mms.MmsConfig;
126import com.android.mms.R;
127import com.android.mms.TempFileProvider;
128import com.android.mms.data.Contact;
129import com.android.mms.data.ContactList;
130import com.android.mms.data.Conversation;
131import com.android.mms.data.Conversation.ConversationQueryHandler;
132import com.android.mms.data.WorkingMessage;
133import com.android.mms.data.WorkingMessage.MessageStatusListener;
134import com.android.mms.drm.DrmUtils;
135import com.android.mms.model.SlideModel;
136import com.android.mms.model.SlideshowModel;
137import com.android.mms.transaction.MessagingNotification;
138import com.android.mms.ui.MessageListView.OnSizeChangedListener;
139import com.android.mms.ui.MessageUtils.ResizeImageResultCallback;
140import com.android.mms.ui.RecipientsEditor.RecipientContextMenuInfo;
141import com.android.mms.util.DraftCache;
142import com.android.mms.util.PhoneNumberFormatter;
143import com.android.mms.util.SendingProgressTokenManager;
144import com.android.mms.util.SmileyParser;
145import com.android.mms.widget.MmsWidgetProvider;
146import com.google.android.mms.ContentType;
147import com.google.android.mms.MmsException;
148import com.google.android.mms.pdu.EncodedStringValue;
149import com.google.android.mms.pdu.PduBody;
150import com.google.android.mms.pdu.PduPart;
151import com.google.android.mms.pdu.PduPersister;
152import com.google.android.mms.pdu.SendReq;
153
154/**
155 * This is the main UI for:
156 * 1. Composing a new message;
157 * 2. Viewing/managing message history of a conversation.
158 *
159 * This activity can handle following parameters from the intent
160 * by which it's launched.
161 * thread_id long Identify the conversation to be viewed. When creating a
162 *         new message, this parameter shouldn't be present.
163 * msg_uri Uri The message which should be opened for editing in the editor.
164 * address String The addresses of the recipients in current conversation.
165 * exit_on_sent boolean Exit this activity after the message is sent.
166 */
167public class ComposeMessageActivity extends Activity
168        implements View.OnClickListener, TextView.OnEditorActionListener,
169        MessageStatusListener, Contact.UpdateListener {
170    public static final int REQUEST_CODE_ATTACH_IMAGE     = 100;
171    public static final int REQUEST_CODE_TAKE_PICTURE     = 101;
172    public static final int REQUEST_CODE_ATTACH_VIDEO     = 102;
173    public static final int REQUEST_CODE_TAKE_VIDEO       = 103;
174    public static final int REQUEST_CODE_ATTACH_SOUND     = 104;
175    public static final int REQUEST_CODE_RECORD_SOUND     = 105;
176    public static final int REQUEST_CODE_CREATE_SLIDESHOW = 106;
177    public static final int REQUEST_CODE_ECM_EXIT_DIALOG  = 107;
178    public static final int REQUEST_CODE_ADD_CONTACT      = 108;
179    public static final int REQUEST_CODE_PICK             = 109;
180
181    private static final String TAG = "Mms/compose";
182
183    private static final boolean DEBUG = false;
184    private static final boolean TRACE = false;
185    private static final boolean LOCAL_LOGV = false;
186
187    // Menu ID
188    private static final int MENU_ADD_SUBJECT           = 0;
189    private static final int MENU_DELETE_THREAD         = 1;
190    private static final int MENU_ADD_ATTACHMENT        = 2;
191    private static final int MENU_DISCARD               = 3;
192    private static final int MENU_SEND                  = 4;
193    private static final int MENU_CALL_RECIPIENT        = 5;
194    private static final int MENU_CONVERSATION_LIST     = 6;
195    private static final int MENU_DEBUG_DUMP            = 7;
196
197    // Context menu ID
198    private static final int MENU_VIEW_CONTACT          = 12;
199    private static final int MENU_ADD_TO_CONTACTS       = 13;
200
201    private static final int MENU_EDIT_MESSAGE          = 14;
202    private static final int MENU_VIEW_SLIDESHOW        = 16;
203    private static final int MENU_VIEW_MESSAGE_DETAILS  = 17;
204    private static final int MENU_DELETE_MESSAGE        = 18;
205    private static final int MENU_SEARCH                = 19;
206    private static final int MENU_DELIVERY_REPORT       = 20;
207    private static final int MENU_FORWARD_MESSAGE       = 21;
208    private static final int MENU_CALL_BACK             = 22;
209    private static final int MENU_SEND_EMAIL            = 23;
210    private static final int MENU_COPY_MESSAGE_TEXT     = 24;
211    private static final int MENU_COPY_TO_SDCARD        = 25;
212    private static final int MENU_INSERT_SMILEY         = 26;
213    private static final int MENU_ADD_ADDRESS_TO_CONTACTS = 27;
214    private static final int MENU_LOCK_MESSAGE          = 28;
215    private static final int MENU_UNLOCK_MESSAGE        = 29;
216    private static final int MENU_SAVE_RINGTONE         = 30;
217    private static final int MENU_PREFERENCES           = 31;
218    private static final int MENU_GROUP_PARTICIPANTS    = 32;
219
220    private static final int RECIPIENTS_MAX_LENGTH = 312;
221
222    private static final int MESSAGE_LIST_QUERY_TOKEN = 9527;
223    private static final int MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN = 9528;
224
225    private static final int DELETE_MESSAGE_TOKEN  = 9700;
226
227    private static final int CHARS_REMAINING_BEFORE_COUNTER_SHOWN = 10;
228
229    private static final long NO_DATE_FOR_DIALOG = -1L;
230
231    private static final String KEY_EXIT_ON_SENT = "exit_on_sent";
232    private static final String KEY_FORWARDED_MESSAGE = "forwarded_message";
233
234    private static final String EXIT_ECM_RESULT = "exit_ecm_result";
235
236    // When the conversation has a lot of messages and a new message is sent, the list is scrolled
237    // so the user sees the just sent message. If we have to scroll the list more than 20 items,
238    // then a scroll shortcut is invoked to move the list near the end before scrolling.
239    private static final int MAX_ITEMS_TO_INVOKE_SCROLL_SHORTCUT = 20;
240
241    // Any change in height in the message list view greater than this threshold will not
242    // cause a smooth scroll. Instead, we jump the list directly to the desired position.
243    private static final int SMOOTH_SCROLL_THRESHOLD = 200;
244
245    // To reduce janky interaction when message history + draft loads and keyboard opening
246    // query the messages + draft after the keyboard opens. This controls that behavior.
247    private static final boolean DEFER_LOADING_MESSAGES_AND_DRAFT = true;
248
249    // The max amount of delay before we force load messages and draft.
250    // 500ms is determined empirically. We want keyboard to have a chance to be shown before
251    // we force loading. However, there is at least one use case where the keyboard never shows
252    // even if we tell it to (turning off and on the screen). So we need to force load the
253    // messages+draft after the max delay.
254    private static final int LOADING_MESSAGES_AND_DRAFT_MAX_DELAY_MS = 500;
255
256    private ContentResolver mContentResolver;
257
258    private BackgroundQueryHandler mBackgroundQueryHandler;
259
260    private Conversation mConversation;     // Conversation we are working in
261
262    // When mSendDiscreetMode is true, this activity only allows a user to type in and send
263    // a single sms, send the message, and then exits. The message history and menus are hidden.
264    private boolean mSendDiscreetMode;
265    private boolean mForwardMessageMode;
266
267    private View mTopPanel;                 // View containing the recipient and subject editors
268    private View mBottomPanel;              // View containing the text editor, send button, ec.
269    private EditText mTextEditor;           // Text editor to type your message into
270    private TextView mTextCounter;          // Shows the number of characters used in text editor
271    private TextView mSendButtonMms;        // Press to send mms
272    private ImageButton mSendButtonSms;     // Press to send sms
273    private EditText mSubjectTextEditor;    // Text editor for MMS subject
274
275    private AttachmentEditor mAttachmentEditor;
276    private View mAttachmentEditorScrollView;
277
278    private MessageListView mMsgListView;        // ListView for messages in this conversation
279    public MessageListAdapter mMsgListAdapter;  // and its corresponding ListAdapter
280
281    private RecipientsEditor mRecipientsEditor;  // UI control for editing recipients
282    private ImageButton mRecipientsPicker;       // UI control for recipients picker
283
284    // For HW keyboard, 'mIsKeyboardOpen' indicates if the HW keyboard is open.
285    // For SW keyboard, 'mIsKeyboardOpen' should always be true.
286    private boolean mIsKeyboardOpen;
287    private boolean mIsLandscape;                // Whether we're in landscape mode
288
289    private boolean mToastForDraftSave;   // Whether to notify the user that a draft is being saved
290
291    private boolean mSentMessage;       // true if the user has sent a message while in this
292                                        // activity. On a new compose message case, when the first
293                                        // message is sent is a MMS w/ attachment, the list blanks
294                                        // for a second before showing the sent message. But we'd
295                                        // think the message list is empty, thus show the recipients
296                                        // editor thinking it's a draft message. This flag should
297                                        // help clarify the situation.
298
299    private WorkingMessage mWorkingMessage;         // The message currently being composed.
300
301    private AlertDialog mSmileyDialog;
302
303    private boolean mWaitingForSubActivity;
304    private int mLastRecipientCount;            // Used for warning the user on too many recipients.
305    private AttachmentTypeSelectorAdapter mAttachmentTypeSelectorAdapter;
306
307    private boolean mSendingMessage;    // Indicates the current message is sending, and shouldn't send again.
308
309    private Intent mAddContactIntent;   // Intent used to add a new contact
310
311    private Uri mTempMmsUri;            // Only used as a temporary to hold a slideshow uri
312    private long mTempThreadId;         // Only used as a temporary to hold a threadId
313
314    private AsyncDialog mAsyncDialog;   // Used for background tasks.
315
316    private String mDebugRecipients;
317    private int mLastSmoothScrollPosition;
318    private boolean mScrollOnSend;      // Flag that we need to scroll the list to the end.
319
320    private int mSavedScrollPosition = -1;  // we save the ListView's scroll position in onPause(),
321                                            // so we can remember it after re-entering the activity.
322                                            // If the value >= 0, then we jump to that line. If the
323                                            // value is maxint, then we jump to the end.
324    private long mLastMessageId;
325
326    /**
327     * Whether this activity is currently running (i.e. not paused)
328     */
329    private boolean mIsRunning;
330
331    // we may call loadMessageAndDraft() from a few different places. This is used to make
332    // sure we only load message+draft once.
333    private boolean mMessagesAndDraftLoaded;
334
335    // whether we should load the draft. For example, after attaching a photo and coming back
336    // in onActivityResult(), we should not load the draft because that will mess up the draft
337    // state of mWorkingMessage. Also, if we are handling a Send or Forward Message Intent,
338    // we should not load the draft.
339    private boolean mShouldLoadDraft;
340
341    private Handler mHandler = new Handler();
342
343    // keys for extras and icicles
344    public final static String THREAD_ID = "thread_id";
345    private final static String RECIPIENTS = "recipients";
346
347    @SuppressWarnings("unused")
348    public static void log(String logMsg) {
349        Thread current = Thread.currentThread();
350        long tid = current.getId();
351        StackTraceElement[] stack = current.getStackTrace();
352        String methodName = stack[3].getMethodName();
353        // Prepend current thread ID and name of calling method to the message.
354        logMsg = "[" + tid + "] [" + methodName + "] " + logMsg;
355        Log.d(TAG, logMsg);
356    }
357
358    //==========================================================
359    // Inner classes
360    //==========================================================
361
362    private void editSlideshow() {
363        // The user wants to edit the slideshow. That requires us to persist the slideshow to
364        // disk as a PDU in saveAsMms. This code below does that persisting in a background
365        // task. If the task takes longer than a half second, a progress dialog is displayed.
366        // Once the PDU persisting is done, another runnable on the UI thread get executed to start
367        // the SlideshowEditActivity.
368        getAsyncDialog().runAsync(new Runnable() {
369            @Override
370            public void run() {
371                // This runnable gets run in a background thread.
372                mTempMmsUri = mWorkingMessage.saveAsMms(false);
373            }
374        }, new Runnable() {
375            @Override
376            public void run() {
377                // Once the above background thread is complete, this runnable is run
378                // on the UI thread.
379                if (mTempMmsUri == null) {
380                    return;
381                }
382                Intent intent = new Intent(ComposeMessageActivity.this,
383                        SlideshowEditActivity.class);
384                intent.setData(mTempMmsUri);
385                startActivityForResult(intent, REQUEST_CODE_CREATE_SLIDESHOW);
386            }
387        }, R.string.building_slideshow_title);
388    }
389
390    private final Handler mAttachmentEditorHandler = new Handler() {
391        @Override
392        public void handleMessage(Message msg) {
393            switch (msg.what) {
394                case AttachmentEditor.MSG_EDIT_SLIDESHOW: {
395                    editSlideshow();
396                    break;
397                }
398                case AttachmentEditor.MSG_SEND_SLIDESHOW: {
399                    if (isPreparedForSending()) {
400                        ComposeMessageActivity.this.confirmSendMessageIfNeeded();
401                    }
402                    break;
403                }
404                case AttachmentEditor.MSG_VIEW_IMAGE:
405                case AttachmentEditor.MSG_PLAY_VIDEO:
406                case AttachmentEditor.MSG_PLAY_AUDIO:
407                case AttachmentEditor.MSG_PLAY_SLIDESHOW:
408                    viewMmsMessageAttachment(msg.what);
409                    break;
410
411                case AttachmentEditor.MSG_REPLACE_IMAGE:
412                case AttachmentEditor.MSG_REPLACE_VIDEO:
413                case AttachmentEditor.MSG_REPLACE_AUDIO:
414                    showAddAttachmentDialog(true);
415                    break;
416
417                case AttachmentEditor.MSG_REMOVE_ATTACHMENT:
418                    mWorkingMessage.removeAttachment(true);
419                    break;
420
421                default:
422                    break;
423            }
424        }
425    };
426
427
428    private void viewMmsMessageAttachment(final int requestCode) {
429        SlideshowModel slideshow = mWorkingMessage.getSlideshow();
430        if (slideshow == null) {
431            throw new IllegalStateException("mWorkingMessage.getSlideshow() == null");
432        }
433        if (slideshow.isSimple()) {
434            MessageUtils.viewSimpleSlideshow(this, slideshow);
435        } else {
436            // The user wants to view the slideshow. That requires us to persist the slideshow to
437            // disk as a PDU in saveAsMms. This code below does that persisting in a background
438            // task. If the task takes longer than a half second, a progress dialog is displayed.
439            // Once the PDU persisting is done, another runnable on the UI thread get executed to
440            // start the SlideshowActivity.
441            getAsyncDialog().runAsync(new Runnable() {
442                @Override
443                public void run() {
444                    // This runnable gets run in a background thread.
445                    mTempMmsUri = mWorkingMessage.saveAsMms(false);
446                }
447            }, new Runnable() {
448                @Override
449                public void run() {
450                    // Once the above background thread is complete, this runnable is run
451                    // on the UI thread.
452                    if (mTempMmsUri == null) {
453                        return;
454                    }
455                    MessageUtils.launchSlideshowActivity(ComposeMessageActivity.this, mTempMmsUri,
456                            requestCode);
457                }
458            }, R.string.building_slideshow_title);
459        }
460    }
461
462
463    private final Handler mMessageListItemHandler = new Handler() {
464        @Override
465        public void handleMessage(Message msg) {
466            MessageItem msgItem = (MessageItem) msg.obj;
467            if (msgItem != null) {
468                switch (msg.what) {
469                    case MessageListItem.MSG_LIST_DETAILS:
470                        showMessageDetails(msgItem);
471                        break;
472
473                    case MessageListItem.MSG_LIST_EDIT:
474                        editMessageItem(msgItem);
475                        drawBottomPanel();
476                        break;
477
478                    case MessageListItem.MSG_LIST_PLAY:
479                        switch (msgItem.mAttachmentType) {
480                            case WorkingMessage.IMAGE:
481                            case WorkingMessage.VIDEO:
482                            case WorkingMessage.AUDIO:
483                            case WorkingMessage.SLIDESHOW:
484                                MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this,
485                                        msgItem.mMessageUri, msgItem.mSlideshow,
486                                        getAsyncDialog());
487                                break;
488                        }
489                        break;
490
491                    default:
492                        Log.w(TAG, "Unknown message: " + msg.what);
493                        return;
494                }
495            }
496        }
497    };
498
499    private boolean showMessageDetails(MessageItem msgItem) {
500        Cursor cursor = mMsgListAdapter.getCursorForItem(msgItem);
501        if (cursor == null) {
502            return false;
503        }
504        String messageDetails = MessageUtils.getMessageDetails(
505                ComposeMessageActivity.this, cursor, msgItem.mMessageSize);
506        new AlertDialog.Builder(ComposeMessageActivity.this)
507                .setTitle(R.string.message_details_title)
508                .setMessage(messageDetails)
509                .setCancelable(true)
510                .show();
511        return true;
512    }
513
514    private final OnKeyListener mSubjectKeyListener = new OnKeyListener() {
515        @Override
516        public boolean onKey(View v, int keyCode, KeyEvent event) {
517            if (event.getAction() != KeyEvent.ACTION_DOWN) {
518                return false;
519            }
520
521            // When the subject editor is empty, press "DEL" to hide the input field.
522            if ((keyCode == KeyEvent.KEYCODE_DEL) && (mSubjectTextEditor.length() == 0)) {
523                showSubjectEditor(false);
524                mWorkingMessage.setSubject(null, true);
525                return true;
526            }
527            return false;
528        }
529    };
530
531    /**
532     * Return the messageItem associated with the type ("mms" or "sms") and message id.
533     * @param type Type of the message: "mms" or "sms"
534     * @param msgId Message id of the message. This is the _id of the sms or pdu row and is
535     * stored in the MessageItem
536     * @param createFromCursorIfNotInCache true if the item is not found in the MessageListAdapter's
537     * cache and the code can create a new MessageItem based on the position of the current cursor.
538     * If false, the function returns null if the MessageItem isn't in the cache.
539     * @return MessageItem or null if not found and createFromCursorIfNotInCache is false
540     */
541    private MessageItem getMessageItem(String type, long msgId,
542            boolean createFromCursorIfNotInCache) {
543        return mMsgListAdapter.getCachedMessageItem(type, msgId,
544                createFromCursorIfNotInCache ? mMsgListAdapter.getCursor() : null);
545    }
546
547    private boolean isCursorValid() {
548        // Check whether the cursor is valid or not.
549        Cursor cursor = mMsgListAdapter.getCursor();
550        if (cursor.isClosed() || cursor.isBeforeFirst() || cursor.isAfterLast()) {
551            Log.e(TAG, "Bad cursor.", new RuntimeException());
552            return false;
553        }
554        return true;
555    }
556
557    private void resetCounter() {
558        mTextCounter.setText("");
559        mTextCounter.setVisibility(View.GONE);
560    }
561
562    private void updateCounter(CharSequence text, int start, int before, int count) {
563        WorkingMessage workingMessage = mWorkingMessage;
564        if (workingMessage.requiresMms()) {
565            // If we're not removing text (i.e. no chance of converting back to SMS
566            // because of this change) and we're in MMS mode, just bail out since we
567            // then won't have to calculate the length unnecessarily.
568            final boolean textRemoved = (before > count);
569            if (!textRemoved) {
570                showSmsOrMmsSendButton(workingMessage.requiresMms());
571                return;
572            }
573        }
574
575        int[] params = SmsMessage.calculateLength(text, false);
576            /* SmsMessage.calculateLength returns an int[4] with:
577             *   int[0] being the number of SMS's required,
578             *   int[1] the number of code units used,
579             *   int[2] is the number of code units remaining until the next message.
580             *   int[3] is the encoding type that should be used for the message.
581             */
582        int msgCount = params[0];
583        int remainingInCurrentMessage = params[2];
584
585        if (!MmsConfig.getMultipartSmsEnabled()) {
586            // The provider doesn't support multi-part sms's so as soon as the user types
587            // an sms longer than one segment, we have to turn the message into an mms.
588            mWorkingMessage.setLengthRequiresMms(msgCount > 1, true);
589        } else {
590            int threshold = MmsConfig.getSmsToMmsTextThreshold();
591            mWorkingMessage.setLengthRequiresMms(threshold > 0 && msgCount > threshold, true);
592        }
593
594        // Show the counter only if:
595        // - We are not in MMS mode
596        // - We are going to send more than one message OR we are getting close
597        boolean showCounter = false;
598        if (!workingMessage.requiresMms() &&
599                (msgCount > 1 ||
600                 remainingInCurrentMessage <= CHARS_REMAINING_BEFORE_COUNTER_SHOWN)) {
601            showCounter = true;
602        }
603
604        showSmsOrMmsSendButton(workingMessage.requiresMms());
605
606        if (showCounter) {
607            // Update the remaining characters and number of messages required.
608            String counterText = msgCount > 1 ? remainingInCurrentMessage + " / " + msgCount
609                    : String.valueOf(remainingInCurrentMessage);
610            mTextCounter.setText(counterText);
611            mTextCounter.setVisibility(View.VISIBLE);
612        } else {
613            mTextCounter.setVisibility(View.GONE);
614        }
615    }
616
617    @Override
618    public void startActivityForResult(Intent intent, int requestCode)
619    {
620        // requestCode >= 0 means the activity in question is a sub-activity.
621        if (requestCode >= 0) {
622            mWaitingForSubActivity = true;
623        }
624        // The camera and other activities take a long time to hide the keyboard so we pre-hide
625        // it here. However, if we're opening up the quick contact window while typing, don't
626        // mess with the keyboard.
627        if (mIsKeyboardOpen && !QuickContact.ACTION_QUICK_CONTACT.equals(intent.getAction())) {
628            hideKeyboard();
629        }
630
631        super.startActivityForResult(intent, requestCode);
632    }
633
634    private void showConvertToMmsToast() {
635        Toast.makeText(this, R.string.converting_to_picture_message, Toast.LENGTH_SHORT).show();
636    }
637
638    private class DeleteMessageListener implements OnClickListener {
639        private final MessageItem mMessageItem;
640
641        public DeleteMessageListener(MessageItem messageItem) {
642            mMessageItem = messageItem;
643        }
644
645        @Override
646        public void onClick(DialogInterface dialog, int whichButton) {
647            dialog.dismiss();
648
649            new AsyncTask<Void, Void, Void>() {
650                protected Void doInBackground(Void... none) {
651                    if (mMessageItem.isMms()) {
652                        WorkingMessage.removeThumbnailsFromCache(mMessageItem.getSlideshow());
653
654                        MmsApp.getApplication().getPduLoaderManager()
655                            .removePdu(mMessageItem.mMessageUri);
656                        // Delete the message *after* we've removed the thumbnails because we
657                        // need the pdu and slideshow for removeThumbnailsFromCache to work.
658                    }
659                    Boolean deletingLastItem = false;
660                    Cursor cursor = mMsgListAdapter != null ? mMsgListAdapter.getCursor() : null;
661                    if (cursor != null) {
662                        cursor.moveToLast();
663                        long msgId = cursor.getLong(COLUMN_ID);
664                        deletingLastItem = msgId == mMessageItem.mMsgId;
665                    }
666                    mBackgroundQueryHandler.startDelete(DELETE_MESSAGE_TOKEN,
667                            deletingLastItem, mMessageItem.mMessageUri,
668                            mMessageItem.mLocked ? null : "locked=0", null);
669                    return null;
670                }
671            }.execute();
672        }
673    }
674
675    private class DiscardDraftListener implements OnClickListener {
676        @Override
677        public void onClick(DialogInterface dialog, int whichButton) {
678            mWorkingMessage.discard();
679            dialog.dismiss();
680            finish();
681        }
682    }
683
684    private class SendIgnoreInvalidRecipientListener implements OnClickListener {
685        @Override
686        public void onClick(DialogInterface dialog, int whichButton) {
687            sendMessage(true);
688            dialog.dismiss();
689        }
690    }
691
692    private class CancelSendingListener implements OnClickListener {
693        @Override
694        public void onClick(DialogInterface dialog, int whichButton) {
695            if (isRecipientsEditorVisible()) {
696                mRecipientsEditor.requestFocus();
697            }
698            dialog.dismiss();
699        }
700    }
701
702    private void confirmSendMessageIfNeeded() {
703        if (!isRecipientsEditorVisible()) {
704            sendMessage(true);
705            return;
706        }
707
708        boolean isMms = mWorkingMessage.requiresMms();
709        if (mRecipientsEditor.hasInvalidRecipient(isMms)) {
710            if (mRecipientsEditor.hasValidRecipient(isMms)) {
711                String title = getResourcesString(R.string.has_invalid_recipient,
712                        mRecipientsEditor.formatInvalidNumbers(isMms));
713                new AlertDialog.Builder(this)
714                    .setTitle(title)
715                    .setMessage(R.string.invalid_recipient_message)
716                    .setPositiveButton(R.string.try_to_send,
717                            new SendIgnoreInvalidRecipientListener())
718                    .setNegativeButton(R.string.no, new CancelSendingListener())
719                    .show();
720            } else {
721                new AlertDialog.Builder(this)
722                    .setTitle(R.string.cannot_send_message)
723                    .setMessage(R.string.cannot_send_message_reason)
724                    .setPositiveButton(R.string.yes, new CancelSendingListener())
725                    .show();
726            }
727        } else {
728            // The recipients editor is still open. Make sure we use what's showing there
729            // as the destination.
730            ContactList contacts = mRecipientsEditor.constructContactsFromInput(false);
731            mDebugRecipients = contacts.serialize();
732            sendMessage(true);
733        }
734    }
735
736    private final TextWatcher mRecipientsWatcher = new TextWatcher() {
737        @Override
738        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
739        }
740
741        @Override
742        public void onTextChanged(CharSequence s, int start, int before, int count) {
743            // This is a workaround for bug 1609057.  Since onUserInteraction() is
744            // not called when the user touches the soft keyboard, we pretend it was
745            // called when textfields changes.  This should be removed when the bug
746            // is fixed.
747            onUserInteraction();
748        }
749
750        @Override
751        public void afterTextChanged(Editable s) {
752            // Bug 1474782 describes a situation in which we send to
753            // the wrong recipient.  We have been unable to reproduce this,
754            // but the best theory we have so far is that the contents of
755            // mRecipientList somehow become stale when entering
756            // ComposeMessageActivity via onNewIntent().  This assertion is
757            // meant to catch one possible path to that, of a non-visible
758            // mRecipientsEditor having its TextWatcher fire and refreshing
759            // mRecipientList with its stale contents.
760            if (!isRecipientsEditorVisible()) {
761                IllegalStateException e = new IllegalStateException(
762                        "afterTextChanged called with invisible mRecipientsEditor");
763                // Make sure the crash is uploaded to the service so we
764                // can see if this is happening in the field.
765                Log.w(TAG,
766                     "RecipientsWatcher: afterTextChanged called with invisible mRecipientsEditor");
767                return;
768            }
769
770            List<String> numbers = mRecipientsEditor.getNumbers();
771            mWorkingMessage.setWorkingRecipients(numbers);
772            boolean multiRecipients = numbers != null && numbers.size() > 1;
773            mMsgListAdapter.setIsGroupConversation(multiRecipients);
774            mWorkingMessage.setHasMultipleRecipients(multiRecipients, true);
775            mWorkingMessage.setHasEmail(mRecipientsEditor.containsEmail(), true);
776
777            checkForTooManyRecipients();
778
779            // Walk backwards in the text box, skipping spaces.  If the last
780            // character is a comma, update the title bar.
781            for (int pos = s.length() - 1; pos >= 0; pos--) {
782                char c = s.charAt(pos);
783                if (c == ' ')
784                    continue;
785
786                if (c == ',') {
787                    ContactList contacts = mRecipientsEditor.constructContactsFromInput(false);
788                    updateTitle(contacts);
789                }
790
791                break;
792            }
793
794            // If we have gone to zero recipients, disable send button.
795            updateSendButtonState();
796        }
797    };
798
799    private void checkForTooManyRecipients() {
800        final int recipientLimit = MmsConfig.getRecipientLimit();
801        if (recipientLimit != Integer.MAX_VALUE) {
802            final int recipientCount = recipientCount();
803            boolean tooMany = recipientCount > recipientLimit;
804
805            if (recipientCount != mLastRecipientCount) {
806                // Don't warn the user on every character they type when they're over the limit,
807                // only when the actual # of recipients changes.
808                mLastRecipientCount = recipientCount;
809                if (tooMany) {
810                    String tooManyMsg = getString(R.string.too_many_recipients, recipientCount,
811                            recipientLimit);
812                    Toast.makeText(ComposeMessageActivity.this,
813                            tooManyMsg, Toast.LENGTH_LONG).show();
814                }
815            }
816        }
817    }
818
819    private final OnCreateContextMenuListener mRecipientsMenuCreateListener =
820        new OnCreateContextMenuListener() {
821        @Override
822        public void onCreateContextMenu(ContextMenu menu, View v,
823                ContextMenuInfo menuInfo) {
824            if (menuInfo != null) {
825                Contact c = ((RecipientContextMenuInfo) menuInfo).recipient;
826                RecipientsMenuClickListener l = new RecipientsMenuClickListener(c);
827
828                menu.setHeaderTitle(c.getName());
829
830                if (c.existsInDatabase()) {
831                    menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact)
832                            .setOnMenuItemClickListener(l);
833                } else if (canAddToContacts(c)){
834                    menu.add(0, MENU_ADD_TO_CONTACTS, 0, R.string.menu_add_to_contacts)
835                            .setOnMenuItemClickListener(l);
836                }
837            }
838        }
839    };
840
841    private final class RecipientsMenuClickListener implements MenuItem.OnMenuItemClickListener {
842        private final Contact mRecipient;
843
844        RecipientsMenuClickListener(Contact recipient) {
845            mRecipient = recipient;
846        }
847
848        @Override
849        public boolean onMenuItemClick(MenuItem item) {
850            switch (item.getItemId()) {
851                // Context menu handlers for the recipients editor.
852                case MENU_VIEW_CONTACT: {
853                    Uri contactUri = mRecipient.getUri();
854                    Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
855                    intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
856                    startActivity(intent);
857                    return true;
858                }
859                case MENU_ADD_TO_CONTACTS: {
860                    mAddContactIntent = ConversationList.createAddContactIntent(
861                            mRecipient.getNumber());
862                    ComposeMessageActivity.this.startActivityForResult(mAddContactIntent,
863                            REQUEST_CODE_ADD_CONTACT);
864                    return true;
865                }
866            }
867            return false;
868        }
869    }
870
871    private boolean canAddToContacts(Contact contact) {
872        // There are some kind of automated messages, like STK messages, that we don't want
873        // to add to contacts. These names begin with special characters, like, "*Info".
874        final String name = contact.getName();
875        if (!TextUtils.isEmpty(contact.getNumber())) {
876            char c = contact.getNumber().charAt(0);
877            if (isSpecialChar(c)) {
878                return false;
879            }
880        }
881        if (!TextUtils.isEmpty(name)) {
882            char c = name.charAt(0);
883            if (isSpecialChar(c)) {
884                return false;
885            }
886        }
887        if (!(Mms.isEmailAddress(name) ||
888                Telephony.Mms.isPhoneNumber(name) ||
889                contact.isMe())) {
890            return false;
891        }
892        return true;
893    }
894
895    private boolean isSpecialChar(char c) {
896        return c == '*' || c == '%' || c == '$';
897    }
898
899    private void addPositionBasedMenuItems(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
900        AdapterView.AdapterContextMenuInfo info;
901
902        try {
903            info = (AdapterView.AdapterContextMenuInfo) menuInfo;
904        } catch (ClassCastException e) {
905            Log.e(TAG, "bad menuInfo");
906            return;
907        }
908        final int position = info.position;
909
910        addUriSpecificMenuItems(menu, v, position);
911    }
912
913    private Uri getSelectedUriFromMessageList(ListView listView, int position) {
914        // If the context menu was opened over a uri, get that uri.
915        MessageListItem msglistItem = (MessageListItem) listView.getChildAt(position);
916        if (msglistItem == null) {
917            // FIXME: Should get the correct view. No such interface in ListView currently
918            // to get the view by position. The ListView.getChildAt(position) cannot
919            // get correct view since the list doesn't create one child for each item.
920            // And if setSelection(position) then getSelectedView(),
921            // cannot get corrent view when in touch mode.
922            return null;
923        }
924
925        TextView textView;
926        CharSequence text = null;
927        int selStart = -1;
928        int selEnd = -1;
929
930        //check if message sender is selected
931        textView = (TextView) msglistItem.findViewById(R.id.text_view);
932        if (textView != null) {
933            text = textView.getText();
934            selStart = textView.getSelectionStart();
935            selEnd = textView.getSelectionEnd();
936        }
937
938        // Check that some text is actually selected, rather than the cursor
939        // just being placed within the TextView.
940        if (selStart != selEnd) {
941            int min = Math.min(selStart, selEnd);
942            int max = Math.max(selStart, selEnd);
943
944            URLSpan[] urls = ((Spanned) text).getSpans(min, max,
945                                                        URLSpan.class);
946
947            if (urls.length == 1) {
948                return Uri.parse(urls[0].getURL());
949            }
950        }
951
952        //no uri was selected
953        return null;
954    }
955
956    private void addUriSpecificMenuItems(ContextMenu menu, View v, int position) {
957        Uri uri = getSelectedUriFromMessageList((ListView) v, position);
958
959        if (uri != null) {
960            Intent intent = new Intent(null, uri);
961            intent.addCategory(Intent.CATEGORY_SELECTED_ALTERNATIVE);
962            menu.addIntentOptions(0, 0, 0,
963                    new android.content.ComponentName(this, ComposeMessageActivity.class),
964                    null, intent, 0, null);
965        }
966    }
967
968    private final void addCallAndContactMenuItems(
969            ContextMenu menu, MsgListMenuClickListener l, MessageItem msgItem) {
970        if (TextUtils.isEmpty(msgItem.mBody)) {
971            return;
972        }
973        SpannableString msg = new SpannableString(msgItem.mBody);
974        Linkify.addLinks(msg, Linkify.ALL);
975        ArrayList<String> uris =
976            MessageUtils.extractUris(msg.getSpans(0, msg.length(), URLSpan.class));
977
978        // Remove any dupes so they don't get added to the menu multiple times
979        HashSet<String> collapsedUris = new HashSet<String>();
980        for (String uri : uris) {
981            collapsedUris.add(uri.toLowerCase());
982        }
983        for (String uriString : collapsedUris) {
984            String prefix = null;
985            int sep = uriString.indexOf(":");
986            if (sep >= 0) {
987                prefix = uriString.substring(0, sep);
988                uriString = uriString.substring(sep + 1);
989            }
990            Uri contactUri = null;
991            boolean knownPrefix = true;
992            if ("mailto".equalsIgnoreCase(prefix))  {
993                contactUri = getContactUriForEmail(uriString);
994            } else if ("tel".equalsIgnoreCase(prefix)) {
995                contactUri = getContactUriForPhoneNumber(uriString);
996            } else {
997                knownPrefix = false;
998            }
999            if (knownPrefix && contactUri == null) {
1000                Intent intent = ConversationList.createAddContactIntent(uriString);
1001
1002                String addContactString = getString(R.string.menu_add_address_to_contacts,
1003                        uriString);
1004                menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, addContactString)
1005                    .setOnMenuItemClickListener(l)
1006                    .setIntent(intent);
1007            }
1008        }
1009    }
1010
1011    private Uri getContactUriForEmail(String emailAddress) {
1012        Cursor cursor = SqliteWrapper.query(this, getContentResolver(),
1013                Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(emailAddress)),
1014                new String[] { Email.CONTACT_ID, Contacts.DISPLAY_NAME }, null, null, null);
1015
1016        if (cursor != null) {
1017            try {
1018                while (cursor.moveToNext()) {
1019                    String name = cursor.getString(1);
1020                    if (!TextUtils.isEmpty(name)) {
1021                        return ContentUris.withAppendedId(Contacts.CONTENT_URI, cursor.getLong(0));
1022                    }
1023                }
1024            } finally {
1025                cursor.close();
1026            }
1027        }
1028        return null;
1029    }
1030
1031    private Uri getContactUriForPhoneNumber(String phoneNumber) {
1032        Contact contact = Contact.get(phoneNumber, false);
1033        if (contact.existsInDatabase()) {
1034            return contact.getUri();
1035        }
1036        return null;
1037    }
1038
1039    private final OnCreateContextMenuListener mMsgListMenuCreateListener =
1040        new OnCreateContextMenuListener() {
1041        @Override
1042        public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
1043            if (!isCursorValid()) {
1044                return;
1045            }
1046            Cursor cursor = mMsgListAdapter.getCursor();
1047            String type = cursor.getString(COLUMN_MSG_TYPE);
1048            long msgId = cursor.getLong(COLUMN_ID);
1049
1050            addPositionBasedMenuItems(menu, v, menuInfo);
1051
1052            MessageItem msgItem = mMsgListAdapter.getCachedMessageItem(type, msgId, cursor);
1053            if (msgItem == null) {
1054                Log.e(TAG, "Cannot load message item for type = " + type
1055                        + ", msgId = " + msgId);
1056                return;
1057            }
1058
1059            menu.setHeaderTitle(R.string.message_options);
1060
1061            MsgListMenuClickListener l = new MsgListMenuClickListener(msgItem);
1062
1063            // It is unclear what would make most sense for copying an MMS message
1064            // to the clipboard, so we currently do SMS only.
1065            if (msgItem.isSms()) {
1066                // Message type is sms. Only allow "edit" if the message has a single recipient
1067                if (getRecipients().size() == 1 &&
1068                        (msgItem.mBoxId == Sms.MESSAGE_TYPE_OUTBOX ||
1069                                msgItem.mBoxId == Sms.MESSAGE_TYPE_FAILED)) {
1070                    menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit)
1071                    .setOnMenuItemClickListener(l);
1072                }
1073
1074                menu.add(0, MENU_COPY_MESSAGE_TEXT, 0, R.string.copy_message_text)
1075                .setOnMenuItemClickListener(l);
1076            }
1077
1078            addCallAndContactMenuItems(menu, l, msgItem);
1079
1080            // Forward is not available for undownloaded messages.
1081            if (msgItem.isDownloaded() && (msgItem.isSms() || isForwardable(msgId))) {
1082                menu.add(0, MENU_FORWARD_MESSAGE, 0, R.string.menu_forward)
1083                        .setOnMenuItemClickListener(l);
1084            }
1085
1086            if (msgItem.isMms()) {
1087                switch (msgItem.mBoxId) {
1088                    case Mms.MESSAGE_BOX_INBOX:
1089                        break;
1090                    case Mms.MESSAGE_BOX_OUTBOX:
1091                        // Since we currently break outgoing messages to multiple
1092                        // recipients into one message per recipient, only allow
1093                        // editing a message for single-recipient conversations.
1094                        if (getRecipients().size() == 1) {
1095                            menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit)
1096                                    .setOnMenuItemClickListener(l);
1097                        }
1098                        break;
1099                }
1100                switch (msgItem.mAttachmentType) {
1101                    case WorkingMessage.TEXT:
1102                        break;
1103                    case WorkingMessage.VIDEO:
1104                    case WorkingMessage.IMAGE:
1105                        if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) {
1106                            menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard)
1107                            .setOnMenuItemClickListener(l);
1108                        }
1109                        break;
1110                    case WorkingMessage.SLIDESHOW:
1111                    default:
1112                        menu.add(0, MENU_VIEW_SLIDESHOW, 0, R.string.view_slideshow)
1113                        .setOnMenuItemClickListener(l);
1114                        if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) {
1115                            menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard)
1116                            .setOnMenuItemClickListener(l);
1117                        }
1118                        if (isDrmRingtoneWithRights(msgItem.mMsgId)) {
1119                            menu.add(0, MENU_SAVE_RINGTONE, 0,
1120                                    getDrmMimeMenuStringRsrc(msgItem.mMsgId))
1121                            .setOnMenuItemClickListener(l);
1122                        }
1123                        break;
1124                }
1125            }
1126
1127            if (msgItem.mLocked) {
1128                menu.add(0, MENU_UNLOCK_MESSAGE, 0, R.string.menu_unlock)
1129                    .setOnMenuItemClickListener(l);
1130            } else {
1131                menu.add(0, MENU_LOCK_MESSAGE, 0, R.string.menu_lock)
1132                    .setOnMenuItemClickListener(l);
1133            }
1134
1135            menu.add(0, MENU_VIEW_MESSAGE_DETAILS, 0, R.string.view_message_details)
1136                .setOnMenuItemClickListener(l);
1137
1138            if (msgItem.mDeliveryStatus != MessageItem.DeliveryStatus.NONE || msgItem.mReadReport) {
1139                menu.add(0, MENU_DELIVERY_REPORT, 0, R.string.view_delivery_report)
1140                        .setOnMenuItemClickListener(l);
1141            }
1142
1143            menu.add(0, MENU_DELETE_MESSAGE, 0, R.string.delete_message)
1144                .setOnMenuItemClickListener(l);
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 (msgItem.isFailedMessage() && 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        // When the message being edited is the only message in the conversation, the delete
1162        // below does something subtle. The trigger "delete_obsolete_threads_pdu" sees that a
1163        // thread contains no messages and silently deletes the thread. Meanwhile, the mConversation
1164        // object still holds onto the old thread_id and code thinks there's a backing thread in
1165        // the DB when it really has been deleted. Here we try and notice that situation and
1166        // clear out the thread_id. Later on, when Conversation.ensureThreadId() is called, we'll
1167        // create a new thread if necessary.
1168        synchronized(mConversation) {
1169            if (mConversation.getMessageCount() <= 1) {
1170                mConversation.clearThreadId();
1171                MessagingNotification.setCurrentlyDisplayedThreadId(
1172                    MessagingNotification.THREAD_NONE);
1173            }
1174        }
1175        // Delete the old undelivered SMS and load its content.
1176        Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgItem.mMsgId);
1177        SqliteWrapper.delete(ComposeMessageActivity.this,
1178                mContentResolver, uri, null, null);
1179
1180        mWorkingMessage.setText(msgItem.mBody);
1181    }
1182
1183    private void editMmsMessageItem(MessageItem msgItem) {
1184        // Load the selected message in as the working message.
1185        WorkingMessage newWorkingMessage = WorkingMessage.load(this, msgItem.mMessageUri);
1186        if (newWorkingMessage == null) {
1187            return;
1188        }
1189
1190        // Discard the current message in progress.
1191        mWorkingMessage.discard();
1192
1193        mWorkingMessage = newWorkingMessage;
1194        mWorkingMessage.setConversation(mConversation);
1195
1196        drawTopPanel(false);
1197
1198        // WorkingMessage.load() above only loads the slideshow. Set the
1199        // subject here because we already know what it is and avoid doing
1200        // another DB lookup in load() just to get it.
1201        mWorkingMessage.setSubject(msgItem.mSubject, false);
1202
1203        if (mWorkingMessage.hasSubject()) {
1204            showSubjectEditor(true);
1205        }
1206    }
1207
1208    private void copyToClipboard(String str) {
1209        ClipboardManager clipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
1210        clipboard.setPrimaryClip(ClipData.newPlainText(null, str));
1211    }
1212
1213    private void forwardMessage(final MessageItem msgItem) {
1214        mTempThreadId = 0;
1215        // The user wants to forward the message. If the message is an mms message, we need to
1216        // persist the pdu to disk. This is done in a background task.
1217        // If the task takes longer than a half second, a progress dialog is displayed.
1218        // Once the PDU persisting is done, another runnable on the UI thread get executed to start
1219        // the ForwardMessageActivity.
1220        getAsyncDialog().runAsync(new Runnable() {
1221            @Override
1222            public void run() {
1223                // This runnable gets run in a background thread.
1224                if (msgItem.mType.equals("mms")) {
1225                    SendReq sendReq = new SendReq();
1226                    String subject = getString(R.string.forward_prefix);
1227                    if (msgItem.mSubject != null) {
1228                        subject += msgItem.mSubject;
1229                    }
1230                    sendReq.setSubject(new EncodedStringValue(subject));
1231                    sendReq.setBody(msgItem.mSlideshow.makeCopy());
1232
1233                    mTempMmsUri = null;
1234                    try {
1235                        PduPersister persister =
1236                                PduPersister.getPduPersister(ComposeMessageActivity.this);
1237                        // Copy the parts of the message here.
1238                        mTempMmsUri = persister.persist(sendReq, Mms.Draft.CONTENT_URI, true,
1239                                MessagingPreferenceActivity
1240                                    .getIsGroupMmsEnabled(ComposeMessageActivity.this), null);
1241                        mTempThreadId = MessagingNotification.getThreadId(
1242                                ComposeMessageActivity.this, mTempMmsUri);
1243                    } catch (MmsException e) {
1244                        Log.e(TAG, "Failed to copy message: " + msgItem.mMessageUri);
1245                        Toast.makeText(ComposeMessageActivity.this,
1246                                R.string.cannot_save_message, Toast.LENGTH_SHORT).show();
1247                        return;
1248                    }
1249                }
1250            }
1251        }, new Runnable() {
1252            @Override
1253            public void run() {
1254                // Once the above background thread is complete, this runnable is run
1255                // on the UI thread.
1256                Intent intent = createIntent(ComposeMessageActivity.this, 0);
1257
1258                intent.putExtra(KEY_EXIT_ON_SENT, true);
1259                intent.putExtra(KEY_FORWARDED_MESSAGE, true);
1260                if (mTempThreadId > 0) {
1261                    intent.putExtra(THREAD_ID, mTempThreadId);
1262                }
1263
1264                if (msgItem.mType.equals("sms")) {
1265                    intent.putExtra("sms_body", msgItem.mBody);
1266                } else {
1267                    intent.putExtra("msg_uri", mTempMmsUri);
1268                    String subject = getString(R.string.forward_prefix);
1269                    if (msgItem.mSubject != null) {
1270                        subject += msgItem.mSubject;
1271                    }
1272                    intent.putExtra("subject", subject);
1273                }
1274                // ForwardMessageActivity is simply an alias in the manifest for
1275                // ComposeMessageActivity. We have to make an alias because ComposeMessageActivity
1276                // launch flags specify singleTop. When we forward a message, we want to start a
1277                // separate ComposeMessageActivity. The only way to do that is to override the
1278                // singleTop flag, which is impossible to do in code. By creating an alias to the
1279                // activity, without the singleTop flag, we can launch a separate
1280                // ComposeMessageActivity to edit the forward message.
1281                intent.setClassName(ComposeMessageActivity.this,
1282                        "com.android.mms.ui.ForwardMessageActivity");
1283                startActivity(intent);
1284            }
1285        }, R.string.building_slideshow_title);
1286    }
1287
1288    /**
1289     * Context menu handlers for the message list view.
1290     */
1291    private final class MsgListMenuClickListener implements MenuItem.OnMenuItemClickListener {
1292        private MessageItem mMsgItem;
1293
1294        public MsgListMenuClickListener(MessageItem msgItem) {
1295            mMsgItem = msgItem;
1296        }
1297
1298        @Override
1299        public boolean onMenuItemClick(MenuItem item) {
1300            if (mMsgItem == null) {
1301                return false;
1302            }
1303
1304            switch (item.getItemId()) {
1305                case MENU_EDIT_MESSAGE:
1306                    editMessageItem(mMsgItem);
1307                    drawBottomPanel();
1308                    return true;
1309
1310                case MENU_COPY_MESSAGE_TEXT:
1311                    copyToClipboard(mMsgItem.mBody);
1312                    return true;
1313
1314                case MENU_FORWARD_MESSAGE:
1315                    forwardMessage(mMsgItem);
1316                    return true;
1317
1318                case MENU_VIEW_SLIDESHOW:
1319                    MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this,
1320                            ContentUris.withAppendedId(Mms.CONTENT_URI, mMsgItem.mMsgId), null,
1321                            getAsyncDialog());
1322                    return true;
1323
1324                case MENU_VIEW_MESSAGE_DETAILS:
1325                    return showMessageDetails(mMsgItem);
1326
1327                case MENU_DELETE_MESSAGE: {
1328                    DeleteMessageListener l = new DeleteMessageListener(mMsgItem);
1329                    confirmDeleteDialog(l, mMsgItem.mLocked);
1330                    return true;
1331                }
1332                case MENU_DELIVERY_REPORT:
1333                    showDeliveryReport(mMsgItem.mMsgId, mMsgItem.mType);
1334                    return true;
1335
1336                case MENU_COPY_TO_SDCARD: {
1337                    int resId = copyMedia(mMsgItem.mMsgId) ? R.string.copy_to_sdcard_success :
1338                        R.string.copy_to_sdcard_fail;
1339                    Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show();
1340                    return true;
1341                }
1342
1343                case MENU_SAVE_RINGTONE: {
1344                    int resId = getDrmMimeSavedStringRsrc(mMsgItem.mMsgId,
1345                            saveRingtone(mMsgItem.mMsgId));
1346                    Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show();
1347                    return true;
1348                }
1349
1350                case MENU_LOCK_MESSAGE: {
1351                    lockMessage(mMsgItem, true);
1352                    return true;
1353                }
1354
1355                case MENU_UNLOCK_MESSAGE: {
1356                    lockMessage(mMsgItem, false);
1357                    return true;
1358                }
1359
1360                default:
1361                    return false;
1362            }
1363        }
1364    }
1365
1366    private void lockMessage(MessageItem msgItem, boolean locked) {
1367        Uri uri;
1368        if ("sms".equals(msgItem.mType)) {
1369            uri = Sms.CONTENT_URI;
1370        } else {
1371            uri = Mms.CONTENT_URI;
1372        }
1373        final Uri lockUri = ContentUris.withAppendedId(uri, msgItem.mMsgId);
1374
1375        final ContentValues values = new ContentValues(1);
1376        values.put("locked", locked ? 1 : 0);
1377
1378        new Thread(new Runnable() {
1379            @Override
1380            public void run() {
1381                getContentResolver().update(lockUri,
1382                        values, null, null);
1383            }
1384        }, "ComposeMessageActivity.lockMessage").start();
1385    }
1386
1387    /**
1388     * Looks to see if there are any valid parts of the attachment that can be copied to a SD card.
1389     * @param msgId
1390     */
1391    private boolean haveSomethingToCopyToSDCard(long msgId) {
1392        PduBody body = null;
1393        try {
1394            body = SlideshowModel.getPduBody(this,
1395                        ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1396        } catch (MmsException e) {
1397            Log.e(TAG, "haveSomethingToCopyToSDCard can't load pdu body: " + msgId);
1398        }
1399        if (body == null) {
1400            return false;
1401        }
1402
1403        boolean result = false;
1404        int partNum = body.getPartsNum();
1405        for(int i = 0; i < partNum; i++) {
1406            PduPart part = body.getPart(i);
1407            String type = new String(part.getContentType());
1408
1409            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1410                log("[CMA] haveSomethingToCopyToSDCard: part[" + i + "] contentType=" + type);
1411            }
1412
1413            if (ContentType.isImageType(type) || ContentType.isVideoType(type) ||
1414                    ContentType.isAudioType(type) || DrmUtils.isDrmType(type)) {
1415                result = true;
1416                break;
1417            }
1418        }
1419        return result;
1420    }
1421
1422    /**
1423     * Copies media from an Mms to the DrmProvider
1424     * @param msgId
1425     */
1426    private boolean saveRingtone(long msgId) {
1427        boolean result = true;
1428        PduBody body = null;
1429        try {
1430            body = SlideshowModel.getPduBody(this,
1431                        ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1432        } catch (MmsException e) {
1433            Log.e(TAG, "copyToDrmProvider can't load pdu body: " + msgId);
1434        }
1435        if (body == null) {
1436            return false;
1437        }
1438
1439        int partNum = body.getPartsNum();
1440        for(int i = 0; i < partNum; i++) {
1441            PduPart part = body.getPart(i);
1442            String type = new String(part.getContentType());
1443
1444            if (DrmUtils.isDrmType(type)) {
1445                // All parts (but there's probably only a single one) have to be successful
1446                // for a valid result.
1447                result &= copyPart(part, Long.toHexString(msgId));
1448            }
1449        }
1450        return result;
1451    }
1452
1453    /**
1454     * Returns true if any part is drm'd audio with ringtone rights.
1455     * @param msgId
1456     * @return true if one of the parts is drm'd audio with rights to save as a ringtone.
1457     */
1458    private boolean isDrmRingtoneWithRights(long msgId) {
1459        PduBody body = null;
1460        try {
1461            body = SlideshowModel.getPduBody(this,
1462                        ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1463        } catch (MmsException e) {
1464            Log.e(TAG, "isDrmRingtoneWithRights can't load pdu body: " + msgId);
1465        }
1466        if (body == null) {
1467            return false;
1468        }
1469
1470        int partNum = body.getPartsNum();
1471        for (int i = 0; i < partNum; i++) {
1472            PduPart part = body.getPart(i);
1473            String type = new String(part.getContentType());
1474
1475            if (DrmUtils.isDrmType(type)) {
1476                String mimeType = MmsApp.getApplication().getDrmManagerClient()
1477                        .getOriginalMimeType(part.getDataUri());
1478                if (ContentType.isAudioType(mimeType) && DrmUtils.haveRightsForAction(part.getDataUri(),
1479                        DrmStore.Action.RINGTONE)) {
1480                    return true;
1481                }
1482            }
1483        }
1484        return false;
1485    }
1486
1487    /**
1488     * Returns true if all drm'd parts are forwardable.
1489     * @param msgId
1490     * @return true if all drm'd parts are forwardable.
1491     */
1492    private boolean isForwardable(long msgId) {
1493        PduBody body = null;
1494        try {
1495            body = SlideshowModel.getPduBody(this,
1496                        ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1497        } catch (MmsException e) {
1498            Log.e(TAG, "getDrmMimeType can't load pdu body: " + msgId);
1499        }
1500        if (body == null) {
1501            return false;
1502        }
1503
1504        int partNum = body.getPartsNum();
1505        for (int i = 0; i < partNum; i++) {
1506            PduPart part = body.getPart(i);
1507            String type = new String(part.getContentType());
1508
1509            if (DrmUtils.isDrmType(type) && !DrmUtils.haveRightsForAction(part.getDataUri(),
1510                        DrmStore.Action.TRANSFER)) {
1511                    return false;
1512            }
1513        }
1514        return true;
1515    }
1516
1517    private int getDrmMimeMenuStringRsrc(long msgId) {
1518        if (isDrmRingtoneWithRights(msgId)) {
1519            return R.string.save_ringtone;
1520        }
1521        return 0;
1522    }
1523
1524    private int getDrmMimeSavedStringRsrc(long msgId, boolean success) {
1525        if (isDrmRingtoneWithRights(msgId)) {
1526            return success ? R.string.saved_ringtone : R.string.saved_ringtone_fail;
1527        }
1528        return 0;
1529    }
1530
1531    /**
1532     * Copies media from an Mms to the "download" directory on the SD card. If any of the parts
1533     * are audio types, drm'd or not, they're copied to the "Ringtones" directory.
1534     * @param msgId
1535     */
1536    private boolean copyMedia(long msgId) {
1537        boolean result = true;
1538        PduBody body = null;
1539        try {
1540            body = SlideshowModel.getPduBody(this,
1541                        ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1542        } catch (MmsException e) {
1543            Log.e(TAG, "copyMedia can't load pdu body: " + msgId);
1544        }
1545        if (body == null) {
1546            return false;
1547        }
1548
1549        int partNum = body.getPartsNum();
1550        for(int i = 0; i < partNum; i++) {
1551            PduPart part = body.getPart(i);
1552
1553            // all parts have to be successful for a valid result.
1554            result &= copyPart(part, Long.toHexString(msgId));
1555        }
1556        return result;
1557    }
1558
1559    private boolean copyPart(PduPart part, String fallback) {
1560        Uri uri = part.getDataUri();
1561        String type = new String(part.getContentType());
1562        boolean isDrm = DrmUtils.isDrmType(type);
1563        if (isDrm) {
1564            type = MmsApp.getApplication().getDrmManagerClient()
1565                    .getOriginalMimeType(part.getDataUri());
1566        }
1567        if (!ContentType.isImageType(type) && !ContentType.isVideoType(type) &&
1568                !ContentType.isAudioType(type)) {
1569            return true;    // we only save pictures, videos, and sounds. Skip the text parts,
1570                            // the app (smil) parts, and other type that we can't handle.
1571                            // Return true to pretend that we successfully saved the part so
1572                            // the whole save process will be counted a success.
1573        }
1574        InputStream input = null;
1575        FileOutputStream fout = null;
1576        try {
1577            input = mContentResolver.openInputStream(uri);
1578            if (input instanceof FileInputStream) {
1579                FileInputStream fin = (FileInputStream) input;
1580
1581                byte[] location = part.getName();
1582                if (location == null) {
1583                    location = part.getFilename();
1584                }
1585                if (location == null) {
1586                    location = part.getContentLocation();
1587                }
1588
1589                String fileName;
1590                if (location == null) {
1591                    // Use fallback name.
1592                    fileName = fallback;
1593                } else {
1594                    // For locally captured videos, fileName can end up being something like this:
1595                    //      /mnt/sdcard/Android/data/com.android.mms/cache/.temp1.3gp
1596                    fileName = new String(location);
1597                }
1598                File originalFile = new File(fileName);
1599                fileName = originalFile.getName();  // Strip the full path of where the "part" is
1600                                                    // stored down to just the leaf filename.
1601
1602                // Depending on the location, there may be an
1603                // extension already on the name or not. If we've got audio, put the attachment
1604                // in the Ringtones directory.
1605                String dir = Environment.getExternalStorageDirectory() + "/"
1606                                + (ContentType.isAudioType(type) ? Environment.DIRECTORY_RINGTONES :
1607                                    Environment.DIRECTORY_DOWNLOADS)  + "/";
1608                String extension;
1609                int index;
1610                if ((index = fileName.lastIndexOf('.')) == -1) {
1611                    extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
1612                } else {
1613                    extension = fileName.substring(index + 1, fileName.length());
1614                    fileName = fileName.substring(0, index);
1615                }
1616                if (isDrm) {
1617                    extension += DrmUtils.getConvertExtension(type);
1618                }
1619                // Remove leading periods. The gallery ignores files starting with a period.
1620                fileName = fileName.replaceAll("^.", "");
1621
1622                File file = getUniqueDestination(dir + fileName, extension);
1623
1624                // make sure the path is valid and directories created for this file.
1625                File parentFile = file.getParentFile();
1626                if (!parentFile.exists() && !parentFile.mkdirs()) {
1627                    Log.e(TAG, "[MMS] copyPart: mkdirs for " + parentFile.getPath() + " failed!");
1628                    return false;
1629                }
1630
1631                fout = new FileOutputStream(file);
1632
1633                byte[] buffer = new byte[8000];
1634                int size = 0;
1635                while ((size=fin.read(buffer)) != -1) {
1636                    fout.write(buffer, 0, size);
1637                }
1638
1639                // Notify other applications listening to scanner events
1640                // that a media file has been added to the sd card
1641                sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
1642                        Uri.fromFile(file)));
1643            }
1644        } catch (IOException e) {
1645            // Ignore
1646            Log.e(TAG, "IOException caught while opening or reading stream", e);
1647            return false;
1648        } finally {
1649            if (null != input) {
1650                try {
1651                    input.close();
1652                } catch (IOException e) {
1653                    // Ignore
1654                    Log.e(TAG, "IOException caught while closing stream", e);
1655                    return false;
1656                }
1657            }
1658            if (null != fout) {
1659                try {
1660                    fout.close();
1661                } catch (IOException e) {
1662                    // Ignore
1663                    Log.e(TAG, "IOException caught while closing stream", e);
1664                    return false;
1665                }
1666            }
1667        }
1668        return true;
1669    }
1670
1671    private File getUniqueDestination(String base, String extension) {
1672        File file = new File(base + "." + extension);
1673
1674        for (int i = 2; file.exists(); i++) {
1675            file = new File(base + "_" + i + "." + extension);
1676        }
1677        return file;
1678    }
1679
1680    private void showDeliveryReport(long messageId, String type) {
1681        Intent intent = new Intent(this, DeliveryReportActivity.class);
1682        intent.putExtra("message_id", messageId);
1683        intent.putExtra("message_type", type);
1684
1685        startActivity(intent);
1686    }
1687
1688    private final IntentFilter mHttpProgressFilter = new IntentFilter(PROGRESS_STATUS_ACTION);
1689
1690    private final BroadcastReceiver mHttpProgressReceiver = new BroadcastReceiver() {
1691        @Override
1692        public void onReceive(Context context, Intent intent) {
1693            if (PROGRESS_STATUS_ACTION.equals(intent.getAction())) {
1694                long token = intent.getLongExtra("token",
1695                                    SendingProgressTokenManager.NO_TOKEN);
1696                if (token != mConversation.getThreadId()) {
1697                    return;
1698                }
1699
1700                int progress = intent.getIntExtra("progress", 0);
1701                switch (progress) {
1702                    case PROGRESS_START:
1703                        setProgressBarVisibility(true);
1704                        break;
1705                    case PROGRESS_ABORT:
1706                    case PROGRESS_COMPLETE:
1707                        setProgressBarVisibility(false);
1708                        break;
1709                    default:
1710                        setProgress(100 * progress);
1711                }
1712            }
1713        }
1714    };
1715
1716    private static ContactList sEmptyContactList;
1717
1718    private ContactList getRecipients() {
1719        // If the recipients editor is visible, the conversation has
1720        // not really officially 'started' yet.  Recipients will be set
1721        // on the conversation once it has been saved or sent.  In the
1722        // meantime, let anyone who needs the recipient list think it
1723        // is empty rather than giving them a stale one.
1724        if (isRecipientsEditorVisible()) {
1725            if (sEmptyContactList == null) {
1726                sEmptyContactList = new ContactList();
1727            }
1728            return sEmptyContactList;
1729        }
1730        return mConversation.getRecipients();
1731    }
1732
1733    private void updateTitle(ContactList list) {
1734        String title = null;
1735        String subTitle = null;
1736        int cnt = list.size();
1737        switch (cnt) {
1738            case 0: {
1739                String recipient = null;
1740                if (mRecipientsEditor != null) {
1741                    recipient = mRecipientsEditor.getText().toString();
1742                }
1743                title = TextUtils.isEmpty(recipient) ? getString(R.string.new_message) : recipient;
1744                break;
1745            }
1746            case 1: {
1747                title = list.get(0).getName();      // get name returns the number if there's no
1748                                                    // name available.
1749                String number = list.get(0).getNumber();
1750                if (!title.equals(number)) {
1751                    subTitle = PhoneNumberUtils.formatNumber(number, number,
1752                            MmsApp.getApplication().getCurrentCountryIso());
1753                }
1754                break;
1755            }
1756            default: {
1757                // Handle multiple recipients
1758                title = list.formatNames(", ");
1759                subTitle = getResources().getQuantityString(R.plurals.recipient_count, cnt, cnt);
1760                break;
1761            }
1762        }
1763        mDebugRecipients = list.serialize();
1764
1765        ActionBar actionBar = getActionBar();
1766        actionBar.setTitle(title);
1767        actionBar.setSubtitle(subTitle);
1768    }
1769
1770    // Get the recipients editor ready to be displayed onscreen.
1771    private void initRecipientsEditor() {
1772        if (isRecipientsEditorVisible()) {
1773            return;
1774        }
1775        // Must grab the recipients before the view is made visible because getRecipients()
1776        // returns empty recipients when the editor is visible.
1777        ContactList recipients = getRecipients();
1778
1779        ViewStub stub = (ViewStub)findViewById(R.id.recipients_editor_stub);
1780        if (stub != null) {
1781            View stubView = stub.inflate();
1782            mRecipientsEditor = (RecipientsEditor) stubView.findViewById(R.id.recipients_editor);
1783            mRecipientsPicker = (ImageButton) stubView.findViewById(R.id.recipients_picker);
1784        } else {
1785            mRecipientsEditor = (RecipientsEditor)findViewById(R.id.recipients_editor);
1786            mRecipientsEditor.setVisibility(View.VISIBLE);
1787            mRecipientsPicker = (ImageButton)findViewById(R.id.recipients_picker);
1788        }
1789        mRecipientsPicker.setOnClickListener(this);
1790
1791        mRecipientsEditor.setAdapter(new ChipsRecipientAdapter(this));
1792        mRecipientsEditor.populate(recipients);
1793        mRecipientsEditor.setOnCreateContextMenuListener(mRecipientsMenuCreateListener);
1794        mRecipientsEditor.addTextChangedListener(mRecipientsWatcher);
1795        // TODO : Remove the max length limitation due to the multiple phone picker is added and the
1796        // user is able to select a large number of recipients from the Contacts. The coming
1797        // potential issue is that it is hard for user to edit a recipient from hundred of
1798        // recipients in the editor box. We may redesign the editor box UI for this use case.
1799        // mRecipientsEditor.setFilters(new InputFilter[] {
1800        //         new InputFilter.LengthFilter(RECIPIENTS_MAX_LENGTH) });
1801
1802        mRecipientsEditor.setOnSelectChipRunnable(new Runnable() {
1803            @Override
1804            public void run() {
1805                // After the user selects an item in the pop-up contacts list, move the
1806                // focus to the text editor if there is only one recipient.  This helps
1807                // the common case of selecting one recipient and then typing a message,
1808                // but avoids annoying a user who is trying to add five recipients and
1809                // keeps having focus stolen away.
1810                if (mRecipientsEditor.getRecipientCount() == 1) {
1811                    // if we're in extract mode then don't request focus
1812                    final InputMethodManager inputManager = (InputMethodManager)
1813                        getSystemService(Context.INPUT_METHOD_SERVICE);
1814                    if (inputManager == null || !inputManager.isFullscreenMode()) {
1815                        mTextEditor.requestFocus();
1816                    }
1817                }
1818            }
1819        });
1820
1821        mRecipientsEditor.setOnFocusChangeListener(new View.OnFocusChangeListener() {
1822            @Override
1823            public void onFocusChange(View v, boolean hasFocus) {
1824                if (!hasFocus) {
1825                    RecipientsEditor editor = (RecipientsEditor) v;
1826                    ContactList contacts = editor.constructContactsFromInput(false);
1827                    updateTitle(contacts);
1828                }
1829            }
1830        });
1831
1832        PhoneNumberFormatter.setPhoneNumberFormattingTextWatcher(this, mRecipientsEditor);
1833
1834        mTopPanel.setVisibility(View.VISIBLE);
1835    }
1836
1837    //==========================================================
1838    // Activity methods
1839    //==========================================================
1840
1841    public static boolean cancelFailedToDeliverNotification(Intent intent, Context context) {
1842        if (MessagingNotification.isFailedToDeliver(intent)) {
1843            // Cancel any failed message notifications
1844            MessagingNotification.cancelNotification(context,
1845                        MessagingNotification.MESSAGE_FAILED_NOTIFICATION_ID);
1846            return true;
1847        }
1848        return false;
1849    }
1850
1851    public static boolean cancelFailedDownloadNotification(Intent intent, Context context) {
1852        if (MessagingNotification.isFailedToDownload(intent)) {
1853            // Cancel any failed download notifications
1854            MessagingNotification.cancelNotification(context,
1855                        MessagingNotification.DOWNLOAD_FAILED_NOTIFICATION_ID);
1856            return true;
1857        }
1858        return false;
1859    }
1860
1861    @Override
1862    protected void onCreate(Bundle savedInstanceState) {
1863        super.onCreate(savedInstanceState);
1864
1865        resetConfiguration(getResources().getConfiguration());
1866
1867        setContentView(R.layout.compose_message_activity);
1868        setProgressBarVisibility(false);
1869
1870        // Initialize members for UI elements.
1871        initResourceRefs();
1872
1873        mContentResolver = getContentResolver();
1874        mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver);
1875
1876        initialize(savedInstanceState, 0);
1877
1878        if (TRACE) {
1879            android.os.Debug.startMethodTracing("compose");
1880        }
1881    }
1882
1883    private void showSubjectEditor(boolean show) {
1884        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1885            log("" + show);
1886        }
1887
1888        if (mSubjectTextEditor == null) {
1889            // Don't bother to initialize the subject editor if
1890            // we're just going to hide it.
1891            if (show == false) {
1892                return;
1893            }
1894            mSubjectTextEditor = (EditText)findViewById(R.id.subject);
1895            mSubjectTextEditor.setFilters(new InputFilter[] {
1896                    new LengthFilter(MmsConfig.getMaxSubjectLength())});
1897        }
1898
1899        mSubjectTextEditor.setOnKeyListener(show ? mSubjectKeyListener : null);
1900
1901        if (show) {
1902            mSubjectTextEditor.addTextChangedListener(mSubjectEditorWatcher);
1903        } else {
1904            mSubjectTextEditor.removeTextChangedListener(mSubjectEditorWatcher);
1905        }
1906
1907        mSubjectTextEditor.setText(mWorkingMessage.getSubject());
1908        mSubjectTextEditor.setVisibility(show ? View.VISIBLE : View.GONE);
1909        hideOrShowTopPanel();
1910    }
1911
1912    private void hideOrShowTopPanel() {
1913        boolean anySubViewsVisible = (isSubjectEditorVisible() || isRecipientsEditorVisible());
1914        mTopPanel.setVisibility(anySubViewsVisible ? View.VISIBLE : View.GONE);
1915    }
1916
1917    public void initialize(Bundle savedInstanceState, long originalThreadId) {
1918        // Create a new empty working message.
1919        mWorkingMessage = WorkingMessage.createEmpty(this);
1920
1921        // Read parameters or previously saved state of this activity. This will load a new
1922        // mConversation
1923        initActivityState(savedInstanceState);
1924
1925        if (LogTag.SEVERE_WARNING && originalThreadId != 0 &&
1926                originalThreadId == mConversation.getThreadId()) {
1927            LogTag.warnPossibleRecipientMismatch("ComposeMessageActivity.initialize: " +
1928                    " threadId didn't change from: " + originalThreadId, this);
1929        }
1930
1931        log("savedInstanceState = " + savedInstanceState +
1932            " intent = " + getIntent() +
1933            " mConversation = " + mConversation);
1934
1935        if (cancelFailedToDeliverNotification(getIntent(), this)) {
1936            // Show a pop-up dialog to inform user the message was
1937            // failed to deliver.
1938            undeliveredMessageDialog(getMessageDate(null));
1939        }
1940        cancelFailedDownloadNotification(getIntent(), this);
1941
1942        // Set up the message history ListAdapter
1943        initMessageList();
1944
1945        mShouldLoadDraft = true;
1946
1947        // Load the draft for this thread, if we aren't already handling
1948        // existing data, such as a shared picture or forwarded message.
1949        boolean isForwardedMessage = false;
1950        // We don't attempt to handle the Intent.ACTION_SEND when saveInstanceState is non-null.
1951        // saveInstanceState is non-null when this activity is killed. In that case, we already
1952        // handled the attachment or the send, so we don't try and parse the intent again.
1953        if (savedInstanceState == null && (handleSendIntent() || handleForwardedMessage())) {
1954            mShouldLoadDraft = false;
1955        }
1956
1957        // Let the working message know what conversation it belongs to
1958        mWorkingMessage.setConversation(mConversation);
1959
1960        // Show the recipients editor if we don't have a valid thread. Hide it otherwise.
1961        if (mConversation.getThreadId() <= 0) {
1962            // Hide the recipients editor so the call to initRecipientsEditor won't get
1963            // short-circuited.
1964            hideRecipientEditor();
1965            initRecipientsEditor();
1966        } else {
1967            hideRecipientEditor();
1968        }
1969
1970        updateSendButtonState();
1971
1972        drawTopPanel(false);
1973        if (!mShouldLoadDraft) {
1974            // We're not loading a draft, so we can draw the bottom panel immediately.
1975            drawBottomPanel();
1976        }
1977
1978        onKeyboardStateChanged(mIsKeyboardOpen);
1979
1980        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1981            log("update title, mConversation=" + mConversation.toString());
1982        }
1983
1984        updateTitle(mConversation.getRecipients());
1985
1986        if (isForwardedMessage && isRecipientsEditorVisible()) {
1987            // The user is forwarding the message to someone. Put the focus on the
1988            // recipient editor rather than in the message editor.
1989            mRecipientsEditor.requestFocus();
1990        }
1991
1992        mMsgListAdapter.setIsGroupConversation(mConversation.getRecipients().size() > 1);
1993    }
1994
1995    @Override
1996    protected void onNewIntent(Intent intent) {
1997        super.onNewIntent(intent);
1998
1999        setIntent(intent);
2000
2001        Conversation conversation = null;
2002        mSentMessage = false;
2003
2004        // If we have been passed a thread_id, use that to find our
2005        // conversation.
2006
2007        // Note that originalThreadId might be zero but if this is a draft and we save the
2008        // draft, ensureThreadId gets called async from WorkingMessage.asyncUpdateDraftSmsMessage
2009        // the thread will get a threadId behind the UI thread's back.
2010        long originalThreadId = mConversation.getThreadId();
2011        long threadId = intent.getLongExtra(THREAD_ID, 0);
2012        Uri intentUri = intent.getData();
2013
2014        boolean sameThread = false;
2015        if (threadId > 0) {
2016            conversation = Conversation.get(this, threadId, false);
2017        } else {
2018            if (mConversation.getThreadId() == 0) {
2019                // We've got a draft. Make sure the working recipients are synched
2020                // to the conversation so when we compare conversations later in this function,
2021                // the compare will work.
2022                mWorkingMessage.syncWorkingRecipients();
2023            }
2024            // Get the "real" conversation based on the intentUri. The intentUri might specify
2025            // the conversation by a phone number or by a thread id. We'll typically get a threadId
2026            // based uri when the user pulls down a notification while in ComposeMessageActivity and
2027            // we end up here in onNewIntent. mConversation can have a threadId of zero when we're
2028            // working on a draft. When a new message comes in for that same recipient, a
2029            // conversation will get created behind CMA's back when the message is inserted into
2030            // the database and the corresponding entry made in the threads table. The code should
2031            // use the real conversation as soon as it can rather than finding out the threadId
2032            // when sending with "ensureThreadId".
2033            conversation = Conversation.get(this, intentUri, false);
2034        }
2035
2036        if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2037            log("onNewIntent: data=" + intentUri + ", thread_id extra is " + threadId +
2038                    ", new conversation=" + conversation + ", mConversation=" + mConversation);
2039        }
2040
2041        // this is probably paranoid to compare both thread_ids and recipient lists,
2042        // but we want to make double sure because this is a last minute fix for Froyo
2043        // and the previous code checked thread ids only.
2044        // (we cannot just compare thread ids because there is a case where mConversation
2045        // has a stale/obsolete thread id (=1) that could collide against the new thread_id(=1),
2046        // even though the recipient lists are different)
2047        sameThread = ((conversation.getThreadId() == mConversation.getThreadId() ||
2048                mConversation.getThreadId() == 0) &&
2049                conversation.equals(mConversation));
2050
2051        if (sameThread) {
2052            log("onNewIntent: same conversation");
2053            if (mConversation.getThreadId() == 0) {
2054                mConversation = conversation;
2055                mWorkingMessage.setConversation(mConversation);
2056                updateThreadIdIfRunning();
2057                invalidateOptionsMenu();
2058            }
2059        } else {
2060            if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2061                log("onNewIntent: different conversation");
2062            }
2063            saveDraft(false);    // if we've got a draft, save it first
2064
2065            initialize(null, originalThreadId);
2066        }
2067        loadMessagesAndDraft(0);
2068    }
2069
2070    private void sanityCheckConversation() {
2071        if (mWorkingMessage.getConversation() != mConversation) {
2072            LogTag.warnPossibleRecipientMismatch(
2073                    "ComposeMessageActivity: mWorkingMessage.mConversation=" +
2074                    mWorkingMessage.getConversation() + ", mConversation=" +
2075                    mConversation + ", MISMATCH!", this);
2076        }
2077    }
2078
2079    @Override
2080    protected void onRestart() {
2081        super.onRestart();
2082
2083        // hide the compose panel to reduce jank when re-entering this activity.
2084        // if we don't hide it here, the compose panel will flash before the keyboard shows
2085        // (when keyboard is suppose to be shown).
2086        hideBottomPanel();
2087
2088        if (mWorkingMessage.isDiscarded()) {
2089            // If the message isn't worth saving, don't resurrect it. Doing so can lead to
2090            // a situation where a new incoming message gets the old thread id of the discarded
2091            // draft. This activity can end up displaying the recipients of the old message with
2092            // the contents of the new message. Recognize that dangerous situation and bail out
2093            // to the ConversationList where the user can enter this in a clean manner.
2094            if (mWorkingMessage.isWorthSaving()) {
2095                if (LogTag.VERBOSE) {
2096                    log("onRestart: mWorkingMessage.unDiscard()");
2097                }
2098                mWorkingMessage.unDiscard();    // it was discarded in onStop().
2099
2100                sanityCheckConversation();
2101            } else if (isRecipientsEditorVisible() && recipientCount() > 0) {
2102                if (LogTag.VERBOSE) {
2103                    log("onRestart: goToConversationList");
2104                }
2105                goToConversationList();
2106            }
2107        }
2108    }
2109
2110    @Override
2111    protected void onStart() {
2112        super.onStart();
2113
2114        initFocus();
2115
2116        // Register a BroadcastReceiver to listen on HTTP I/O process.
2117        registerReceiver(mHttpProgressReceiver, mHttpProgressFilter);
2118
2119        // figure out whether we need to show the keyboard or not.
2120        // if there is draft to be loaded for 'mConversation', we'll show the keyboard;
2121        // otherwise we hide the keyboard. In any event, delay loading
2122        // message history and draft (controlled by DEFER_LOADING_MESSAGES_AND_DRAFT).
2123        int mode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
2124
2125        if (DraftCache.getInstance().hasDraft(mConversation.getThreadId())) {
2126            mode |= WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
2127        } else if (mConversation.getThreadId() <= 0) {
2128            // For composing a new message, bring up the softkeyboard so the user can
2129            // immediately enter recipients. This call won't do anything on devices with
2130            // a hard keyboard.
2131            mode |= WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
2132        } else {
2133            mode |= WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
2134        }
2135
2136        getWindow().setSoftInputMode(mode);
2137
2138        // reset mMessagesAndDraftLoaded
2139        mMessagesAndDraftLoaded = false;
2140
2141        if (!DEFER_LOADING_MESSAGES_AND_DRAFT) {
2142            loadMessagesAndDraft(1);
2143        } else {
2144            // HACK: force load messages+draft after max delay, if it's not already loaded.
2145            // this is to work around when coming out of sleep mode. WindowManager behaves
2146            // strangely and hides the keyboard when it should be shown, or sometimes initially
2147            // shows it when we want to hide it. In that case, we never get the onSizeChanged()
2148            // callback w/ keyboard shown, so we wouldn't know to load the messages+draft.
2149            mHandler.postDelayed(new Runnable() {
2150                public void run() {
2151                    loadMessagesAndDraft(2);
2152                }
2153            }, LOADING_MESSAGES_AND_DRAFT_MAX_DELAY_MS);
2154        }
2155
2156        // Update the fasttrack info in case any of the recipients' contact info changed
2157        // while we were paused. This can happen, for example, if a user changes or adds
2158        // an avatar associated with a contact.
2159        mWorkingMessage.syncWorkingRecipients();
2160
2161        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2162            log("update title, mConversation=" + mConversation.toString());
2163        }
2164
2165        updateTitle(mConversation.getRecipients());
2166
2167        ActionBar actionBar = getActionBar();
2168        actionBar.setDisplayHomeAsUpEnabled(true);
2169    }
2170
2171    public void loadMessageContent() {
2172        // Don't let any markAsRead DB updates occur before we've loaded the messages for
2173        // the thread. Unblocking occurs when we're done querying for the conversation
2174        // items.
2175        mConversation.blockMarkAsRead(true);
2176        mConversation.markAsRead();         // dismiss any notifications for this convo
2177        startMsgListQuery();
2178        updateSendFailedNotification();
2179    }
2180
2181    /**
2182     * Load message history and draft. This method should be called from main thread.
2183     * @param debugFlag shows where this is being called from
2184     */
2185    private void loadMessagesAndDraft(int debugFlag) {
2186        if (!mSendDiscreetMode && !mMessagesAndDraftLoaded) {
2187            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2188                Log.v(TAG, "### CMA.loadMessagesAndDraft: flag=" + debugFlag);
2189            }
2190            loadMessageContent();
2191            boolean drawBottomPanel = true;
2192            if (mShouldLoadDraft) {
2193                if (loadDraft()) {
2194                    drawBottomPanel = false;
2195                }
2196            }
2197            if (drawBottomPanel) {
2198                drawBottomPanel();
2199            }
2200            mMessagesAndDraftLoaded = true;
2201        }
2202    }
2203
2204    private void updateSendFailedNotification() {
2205        final long threadId = mConversation.getThreadId();
2206        if (threadId <= 0)
2207            return;
2208
2209        // updateSendFailedNotificationForThread makes a database call, so do the work off
2210        // of the ui thread.
2211        new Thread(new Runnable() {
2212            @Override
2213            public void run() {
2214                MessagingNotification.updateSendFailedNotificationForThread(
2215                        ComposeMessageActivity.this, threadId);
2216            }
2217        }, "ComposeMessageActivity.updateSendFailedNotification").start();
2218    }
2219
2220    @Override
2221    public void onSaveInstanceState(Bundle outState) {
2222        super.onSaveInstanceState(outState);
2223
2224        outState.putString(RECIPIENTS, getRecipients().serialize());
2225
2226        mWorkingMessage.writeStateToBundle(outState);
2227
2228        if (mSendDiscreetMode) {
2229            outState.putBoolean(KEY_EXIT_ON_SENT, mSendDiscreetMode);
2230        }
2231        if (mForwardMessageMode) {
2232            outState.putBoolean(KEY_FORWARDED_MESSAGE, mForwardMessageMode);
2233        }
2234    }
2235
2236    @Override
2237    protected void onResume() {
2238        super.onResume();
2239
2240        // OLD: get notified of presence updates to update the titlebar.
2241        // NEW: we are using ContactHeaderWidget which displays presence, but updating presence
2242        //      there is out of our control.
2243        //Contact.startPresenceObserver();
2244
2245        addRecipientsListeners();
2246
2247        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2248            log("update title, mConversation=" + mConversation.toString());
2249        }
2250
2251        // There seems to be a bug in the framework such that setting the title
2252        // here gets overwritten to the original title.  Do this delayed as a
2253        // workaround.
2254        mMessageListItemHandler.postDelayed(new Runnable() {
2255            @Override
2256            public void run() {
2257                ContactList recipients = isRecipientsEditorVisible() ?
2258                        mRecipientsEditor.constructContactsFromInput(false) : getRecipients();
2259                updateTitle(recipients);
2260            }
2261        }, 100);
2262
2263        mIsRunning = true;
2264        updateThreadIdIfRunning();
2265        mConversation.markAsRead();
2266    }
2267
2268    @Override
2269    protected void onPause() {
2270        super.onPause();
2271
2272        if (DEBUG) {
2273            Log.v(TAG, "onPause: setCurrentlyDisplayedThreadId: " +
2274                        MessagingNotification.THREAD_NONE);
2275        }
2276        MessagingNotification.setCurrentlyDisplayedThreadId(MessagingNotification.THREAD_NONE);
2277
2278        // OLD: stop getting notified of presence updates to update the titlebar.
2279        // NEW: we are using ContactHeaderWidget which displays presence, but updating presence
2280        //      there is out of our control.
2281        //Contact.stopPresenceObserver();
2282
2283        removeRecipientsListeners();
2284
2285        // remove any callback to display a progress spinner
2286        if (mAsyncDialog != null) {
2287            mAsyncDialog.clearPendingProgressDialog();
2288        }
2289
2290        // Remember whether the list is scrolled to the end when we're paused so we can rescroll
2291        // to the end when resumed.
2292        if (mMsgListAdapter != null &&
2293                mMsgListView.getLastVisiblePosition() >= mMsgListAdapter.getCount() - 1) {
2294            mSavedScrollPosition = Integer.MAX_VALUE;
2295        } else {
2296            mSavedScrollPosition = mMsgListView.getFirstVisiblePosition();
2297        }
2298        if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2299            Log.v(TAG, "onPause: mSavedScrollPosition=" + mSavedScrollPosition);
2300        }
2301
2302        mConversation.markAsRead();
2303        mIsRunning = false;
2304    }
2305
2306    @Override
2307    protected void onStop() {
2308        super.onStop();
2309
2310        // No need to do the querying when finished this activity
2311        mBackgroundQueryHandler.cancelOperation(MESSAGE_LIST_QUERY_TOKEN);
2312
2313        // Allow any blocked calls to update the thread's read status.
2314        mConversation.blockMarkAsRead(false);
2315
2316        if (mMsgListAdapter != null) {
2317            // Close the cursor in the ListAdapter if the activity stopped.
2318            Cursor cursor = mMsgListAdapter.getCursor();
2319
2320            if (cursor != null && !cursor.isClosed()) {
2321                cursor.close();
2322            }
2323
2324            mMsgListAdapter.changeCursor(null);
2325            mMsgListAdapter.cancelBackgroundLoading();
2326        }
2327
2328        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2329            log("save draft");
2330        }
2331        saveDraft(true);
2332
2333        // set 'mShouldLoadDraft' to true, so when coming back to ComposeMessageActivity, we would
2334        // load the draft, unless we are coming back to the activity after attaching a photo, etc,
2335        // in which case we should set 'mShouldLoadDraft' to false.
2336        mShouldLoadDraft = true;
2337
2338        // Cleanup the BroadcastReceiver.
2339        unregisterReceiver(mHttpProgressReceiver);
2340    }
2341
2342    @Override
2343    protected void onDestroy() {
2344        if (TRACE) {
2345            android.os.Debug.stopMethodTracing();
2346        }
2347
2348        super.onDestroy();
2349    }
2350
2351    @Override
2352    public void onConfigurationChanged(Configuration newConfig) {
2353        super.onConfigurationChanged(newConfig);
2354
2355        if (resetConfiguration(newConfig)) {
2356            // Have to re-layout the attachment editor because we have different layouts
2357            // depending on whether we're portrait or landscape.
2358            drawTopPanel(isSubjectEditorVisible());
2359        }
2360        if (LOCAL_LOGV) {
2361            Log.v(TAG, "CMA.onConfigurationChanged: " + newConfig +
2362                    ", mIsKeyboardOpen=" + mIsKeyboardOpen);
2363        }
2364        onKeyboardStateChanged(mIsKeyboardOpen);
2365    }
2366
2367    // returns true if landscape/portrait configuration has changed
2368    private boolean resetConfiguration(Configuration config) {
2369        mIsKeyboardOpen = config.keyboardHidden == KEYBOARDHIDDEN_NO;
2370        boolean isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE;
2371        if (mIsLandscape != isLandscape) {
2372            mIsLandscape = isLandscape;
2373            return true;
2374        }
2375        return false;
2376    }
2377
2378    private void onKeyboardStateChanged(boolean isKeyboardOpen) {
2379        // If the keyboard is hidden, don't show focus highlights for
2380        // things that cannot receive input.
2381        if (isKeyboardOpen) {
2382            if (mRecipientsEditor != null) {
2383                mRecipientsEditor.setFocusableInTouchMode(true);
2384            }
2385            if (mSubjectTextEditor != null) {
2386                mSubjectTextEditor.setFocusableInTouchMode(true);
2387            }
2388            mTextEditor.setFocusableInTouchMode(true);
2389            mTextEditor.setHint(R.string.type_to_compose_text_enter_to_send);
2390        } else {
2391            if (mRecipientsEditor != null) {
2392                mRecipientsEditor.setFocusable(false);
2393            }
2394            if (mSubjectTextEditor != null) {
2395                mSubjectTextEditor.setFocusable(false);
2396            }
2397            mTextEditor.setFocusable(false);
2398            mTextEditor.setHint(R.string.open_keyboard_to_compose_message);
2399        }
2400    }
2401
2402    @Override
2403    public boolean onKeyDown(int keyCode, KeyEvent event) {
2404        switch (keyCode) {
2405            case KeyEvent.KEYCODE_DEL:
2406                if ((mMsgListAdapter != null) && mMsgListView.isFocused()) {
2407                    Cursor cursor;
2408                    try {
2409                        cursor = (Cursor) mMsgListView.getSelectedItem();
2410                    } catch (ClassCastException e) {
2411                        Log.e(TAG, "Unexpected ClassCastException.", e);
2412                        return super.onKeyDown(keyCode, event);
2413                    }
2414
2415                    if (cursor != null) {
2416                        String type = cursor.getString(COLUMN_MSG_TYPE);
2417                        long msgId = cursor.getLong(COLUMN_ID);
2418                        MessageItem msgItem = mMsgListAdapter.getCachedMessageItem(type, msgId,
2419                                cursor);
2420                        if (msgItem != null) {
2421                            DeleteMessageListener l = new DeleteMessageListener(msgItem);
2422                            confirmDeleteDialog(l, msgItem.mLocked);
2423                        }
2424                        return true;
2425                    }
2426                }
2427                break;
2428            case KeyEvent.KEYCODE_DPAD_CENTER:
2429            case KeyEvent.KEYCODE_ENTER:
2430                if (isPreparedForSending()) {
2431                    confirmSendMessageIfNeeded();
2432                    return true;
2433                }
2434                break;
2435            case KeyEvent.KEYCODE_BACK:
2436                exitComposeMessageActivity(new Runnable() {
2437                    @Override
2438                    public void run() {
2439                        finish();
2440                    }
2441                });
2442                return true;
2443        }
2444
2445        return super.onKeyDown(keyCode, event);
2446    }
2447
2448    private void exitComposeMessageActivity(final Runnable exit) {
2449        // If the message is empty, just quit -- finishing the
2450        // activity will cause an empty draft to be deleted.
2451        if (!mWorkingMessage.isWorthSaving()) {
2452            exit.run();
2453            return;
2454        }
2455
2456        if (isRecipientsEditorVisible() &&
2457                !mRecipientsEditor.hasValidRecipient(mWorkingMessage.requiresMms())) {
2458            MessageUtils.showDiscardDraftConfirmDialog(this, new DiscardDraftListener());
2459            return;
2460        }
2461
2462        mToastForDraftSave = true;
2463        exit.run();
2464    }
2465
2466    private void goToConversationList() {
2467        finish();
2468        startActivity(new Intent(this, ConversationList.class));
2469    }
2470
2471    private void hideRecipientEditor() {
2472        if (mRecipientsEditor != null) {
2473            mRecipientsEditor.removeTextChangedListener(mRecipientsWatcher);
2474            mRecipientsEditor.setVisibility(View.GONE);
2475            hideOrShowTopPanel();
2476        }
2477    }
2478
2479    private boolean isRecipientsEditorVisible() {
2480        return (null != mRecipientsEditor)
2481                    && (View.VISIBLE == mRecipientsEditor.getVisibility());
2482    }
2483
2484    private boolean isSubjectEditorVisible() {
2485        return (null != mSubjectTextEditor)
2486                    && (View.VISIBLE == mSubjectTextEditor.getVisibility());
2487    }
2488
2489    @Override
2490    public void onAttachmentChanged() {
2491        // Have to make sure we're on the UI thread. This function can be called off of the UI
2492        // thread when we're adding multi-attachments
2493        runOnUiThread(new Runnable() {
2494            @Override
2495            public void run() {
2496                drawBottomPanel();
2497                updateSendButtonState();
2498                drawTopPanel(isSubjectEditorVisible());
2499            }
2500        });
2501    }
2502
2503    @Override
2504    public void onProtocolChanged(final boolean convertToMms) {
2505        // Have to make sure we're on the UI thread. This function can be called off of the UI
2506        // thread when we're adding multi-attachments
2507        runOnUiThread(new Runnable() {
2508            @Override
2509            public void run() {
2510                showSmsOrMmsSendButton(convertToMms);
2511
2512                if (convertToMms) {
2513                    // In the case we went from a long sms with a counter to an mms because
2514                    // the user added an attachment or a subject, hide the counter --
2515                    // it doesn't apply to mms.
2516                    mTextCounter.setVisibility(View.GONE);
2517
2518                    showConvertToMmsToast();
2519                }
2520            }
2521        });
2522    }
2523
2524    // Show or hide the Sms or Mms button as appropriate. Return the view so that the caller
2525    // can adjust the enableness and focusability.
2526    private View showSmsOrMmsSendButton(boolean isMms) {
2527        View showButton;
2528        View hideButton;
2529        if (isMms) {
2530            showButton = mSendButtonMms;
2531            hideButton = mSendButtonSms;
2532        } else {
2533            showButton = mSendButtonSms;
2534            hideButton = mSendButtonMms;
2535        }
2536        showButton.setVisibility(View.VISIBLE);
2537        hideButton.setVisibility(View.GONE);
2538
2539        return showButton;
2540    }
2541
2542    Runnable mResetMessageRunnable = new Runnable() {
2543        @Override
2544        public void run() {
2545            resetMessage();
2546        }
2547    };
2548
2549    @Override
2550    public void onPreMessageSent() {
2551        runOnUiThread(mResetMessageRunnable);
2552    }
2553
2554    @Override
2555    public void onMessageSent() {
2556        // This callback can come in on any thread; put it on the main thread to avoid
2557        // concurrency problems
2558        runOnUiThread(new Runnable() {
2559            @Override
2560            public void run() {
2561                // If we already have messages in the list adapter, it
2562                // will be auto-requerying; don't thrash another query in.
2563                // TODO: relying on auto-requerying seems unreliable when priming an MMS into the
2564                // outbox. Need to investigate.
2565//                if (mMsgListAdapter.getCount() == 0) {
2566                    if (LogTag.VERBOSE) {
2567                        log("onMessageSent");
2568                    }
2569                    startMsgListQuery();
2570//                }
2571
2572                // The thread ID could have changed if this is a new message that we just inserted
2573                // into the database (and looked up or created a thread for it)
2574                updateThreadIdIfRunning();
2575            }
2576        });
2577    }
2578
2579    @Override
2580    public void onMaxPendingMessagesReached() {
2581        saveDraft(false);
2582
2583        runOnUiThread(new Runnable() {
2584            @Override
2585            public void run() {
2586                Toast.makeText(ComposeMessageActivity.this, R.string.too_many_unsent_mms,
2587                        Toast.LENGTH_LONG).show();
2588            }
2589        });
2590    }
2591
2592    @Override
2593    public void onAttachmentError(final int error) {
2594        runOnUiThread(new Runnable() {
2595            @Override
2596            public void run() {
2597                handleAddAttachmentError(error, R.string.type_picture);
2598                onMessageSent();        // now requery the list of messages
2599            }
2600        });
2601    }
2602
2603    // We don't want to show the "call" option unless there is only one
2604    // recipient and it's a phone number.
2605    private boolean isRecipientCallable() {
2606        ContactList recipients = getRecipients();
2607        return (recipients.size() == 1 && !recipients.containsEmail());
2608    }
2609
2610    private void dialRecipient() {
2611        if (isRecipientCallable()) {
2612            String number = getRecipients().get(0).getNumber();
2613            Intent dialIntent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + number));
2614            startActivity(dialIntent);
2615        }
2616    }
2617
2618    @Override
2619    public boolean onPrepareOptionsMenu(Menu menu) {
2620        super.onPrepareOptionsMenu(menu) ;
2621
2622        menu.clear();
2623
2624        if (mSendDiscreetMode && !mForwardMessageMode) {
2625            // When we're in send-a-single-message mode from the lock screen, don't show
2626            // any menus.
2627            return true;
2628        }
2629
2630        if (isRecipientCallable()) {
2631            MenuItem item = menu.add(0, MENU_CALL_RECIPIENT, 0, R.string.menu_call)
2632                .setIcon(R.drawable.ic_menu_call)
2633                .setTitle(R.string.menu_call);
2634            if (!isRecipientsEditorVisible()) {
2635                // If we're not composing a new message, show the call icon in the actionbar
2636                item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
2637            }
2638        }
2639
2640        if (MmsConfig.getMmsEnabled()) {
2641            if (!isSubjectEditorVisible()) {
2642                menu.add(0, MENU_ADD_SUBJECT, 0, R.string.add_subject).setIcon(
2643                        R.drawable.ic_menu_edit);
2644            }
2645            if (!mWorkingMessage.hasAttachment()) {
2646                menu.add(0, MENU_ADD_ATTACHMENT, 0, R.string.add_attachment)
2647                        .setIcon(R.drawable.ic_menu_attachment)
2648                    .setTitle(R.string.add_attachment)
2649                        .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);    // add to actionbar
2650            }
2651        }
2652
2653        if (isPreparedForSending()) {
2654            menu.add(0, MENU_SEND, 0, R.string.send).setIcon(android.R.drawable.ic_menu_send);
2655        }
2656
2657        if (!mWorkingMessage.hasSlideshow()) {
2658            menu.add(0, MENU_INSERT_SMILEY, 0, R.string.menu_insert_smiley).setIcon(
2659                    R.drawable.ic_menu_emoticons);
2660        }
2661
2662        if (getRecipients().size() > 1) {
2663            menu.add(0, MENU_GROUP_PARTICIPANTS, 0, R.string.menu_group_participants);
2664        }
2665
2666        if (mMsgListAdapter.getCount() > 0) {
2667            // Removed search as part of b/1205708
2668            //menu.add(0, MENU_SEARCH, 0, R.string.menu_search).setIcon(
2669            //        R.drawable.ic_menu_search);
2670            Cursor cursor = mMsgListAdapter.getCursor();
2671            if ((null != cursor) && (cursor.getCount() > 0)) {
2672                menu.add(0, MENU_DELETE_THREAD, 0, R.string.delete_thread).setIcon(
2673                    android.R.drawable.ic_menu_delete);
2674            }
2675        } else {
2676            menu.add(0, MENU_DISCARD, 0, R.string.discard).setIcon(android.R.drawable.ic_menu_delete);
2677        }
2678
2679        buildAddAddressToContactMenuItem(menu);
2680
2681        menu.add(0, MENU_PREFERENCES, 0, R.string.menu_preferences).setIcon(
2682                android.R.drawable.ic_menu_preferences);
2683
2684        if (LogTag.DEBUG_DUMP) {
2685            menu.add(0, MENU_DEBUG_DUMP, 0, R.string.menu_debug_dump);
2686        }
2687
2688        return true;
2689    }
2690
2691    private void buildAddAddressToContactMenuItem(Menu menu) {
2692        // bug #7087793: for group of recipients, remove "Add to People" action. Rely on
2693        // individually creating contacts for unknown phone numbers by touching the individual
2694        // sender's avatars, one at a time
2695        ContactList contacts = getRecipients();
2696        if (contacts.size() != 1) {
2697            return;
2698        }
2699
2700        // if we don't have a contact for the recipient, create a menu item to add the number
2701        // to contacts.
2702        Contact c = contacts.get(0);
2703        if (!c.existsInDatabase() && canAddToContacts(c)) {
2704            Intent intent = ConversationList.createAddContactIntent(c.getNumber());
2705            menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, R.string.menu_add_to_contacts)
2706                .setIcon(android.R.drawable.ic_menu_add)
2707                .setIntent(intent);
2708        }
2709    }
2710
2711    @Override
2712    public boolean onOptionsItemSelected(MenuItem item) {
2713        switch (item.getItemId()) {
2714            case MENU_ADD_SUBJECT:
2715                showSubjectEditor(true);
2716                mWorkingMessage.setSubject("", true);
2717                updateSendButtonState();
2718                mSubjectTextEditor.requestFocus();
2719                break;
2720            case MENU_ADD_ATTACHMENT:
2721                // Launch the add-attachment list dialog
2722                showAddAttachmentDialog(false);
2723                break;
2724            case MENU_DISCARD:
2725                mWorkingMessage.discard();
2726                finish();
2727                break;
2728            case MENU_SEND:
2729                if (isPreparedForSending()) {
2730                    confirmSendMessageIfNeeded();
2731                }
2732                break;
2733            case MENU_SEARCH:
2734                onSearchRequested();
2735                break;
2736            case MENU_DELETE_THREAD:
2737                confirmDeleteThread(mConversation.getThreadId());
2738                break;
2739
2740            case android.R.id.home:
2741            case MENU_CONVERSATION_LIST:
2742                exitComposeMessageActivity(new Runnable() {
2743                    @Override
2744                    public void run() {
2745                        goToConversationList();
2746                    }
2747                });
2748                break;
2749            case MENU_CALL_RECIPIENT:
2750                dialRecipient();
2751                break;
2752            case MENU_INSERT_SMILEY:
2753                showSmileyDialog();
2754                break;
2755            case MENU_GROUP_PARTICIPANTS:
2756            {
2757                Intent intent = new Intent(this, RecipientListActivity.class);
2758                intent.putExtra(THREAD_ID, mConversation.getThreadId());
2759                startActivity(intent);
2760                break;
2761            }
2762            case MENU_VIEW_CONTACT: {
2763                // View the contact for the first (and only) recipient.
2764                ContactList list = getRecipients();
2765                if (list.size() == 1 && list.get(0).existsInDatabase()) {
2766                    Uri contactUri = list.get(0).getUri();
2767                    Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
2768                    intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
2769                    startActivity(intent);
2770                }
2771                break;
2772            }
2773            case MENU_ADD_ADDRESS_TO_CONTACTS:
2774                mAddContactIntent = item.getIntent();
2775                startActivityForResult(mAddContactIntent, REQUEST_CODE_ADD_CONTACT);
2776                break;
2777            case MENU_PREFERENCES: {
2778                Intent intent = new Intent(this, MessagingPreferenceActivity.class);
2779                startActivityIfNeeded(intent, -1);
2780                break;
2781            }
2782            case MENU_DEBUG_DUMP:
2783                mWorkingMessage.dump();
2784                Conversation.dump();
2785                LogTag.dumpInternalTables(this);
2786                break;
2787        }
2788
2789        return true;
2790    }
2791
2792    private void confirmDeleteThread(long threadId) {
2793        Conversation.startQueryHaveLockedMessages(mBackgroundQueryHandler,
2794                threadId, ConversationList.HAVE_LOCKED_MESSAGES_TOKEN);
2795    }
2796
2797//    static class SystemProperties { // TODO, temp class to get unbundling working
2798//        static int getInt(String s, int value) {
2799//            return value;       // just return the default value or now
2800//        }
2801//    }
2802
2803    private void addAttachment(int type, boolean replace) {
2804        // Calculate the size of the current slide if we're doing a replace so the
2805        // slide size can optionally be used in computing how much room is left for an attachment.
2806        int currentSlideSize = 0;
2807        SlideshowModel slideShow = mWorkingMessage.getSlideshow();
2808        if (replace && slideShow != null) {
2809            WorkingMessage.removeThumbnailsFromCache(slideShow);
2810            SlideModel slide = slideShow.get(0);
2811            currentSlideSize = slide.getSlideSize();
2812        }
2813        switch (type) {
2814            case AttachmentTypeSelectorAdapter.ADD_IMAGE:
2815                MessageUtils.selectImage(this, REQUEST_CODE_ATTACH_IMAGE);
2816                break;
2817
2818            case AttachmentTypeSelectorAdapter.TAKE_PICTURE: {
2819                MessageUtils.capturePicture(this, REQUEST_CODE_TAKE_PICTURE);
2820                break;
2821            }
2822
2823            case AttachmentTypeSelectorAdapter.ADD_VIDEO:
2824                MessageUtils.selectVideo(this, REQUEST_CODE_ATTACH_VIDEO);
2825                break;
2826
2827            case AttachmentTypeSelectorAdapter.RECORD_VIDEO: {
2828                long sizeLimit = computeAttachmentSizeLimit(slideShow, currentSlideSize);
2829                if (sizeLimit > 0) {
2830                    MessageUtils.recordVideo(this, REQUEST_CODE_TAKE_VIDEO, sizeLimit);
2831                } else {
2832                    Toast.makeText(this,
2833                            getString(R.string.message_too_big_for_video),
2834                            Toast.LENGTH_SHORT).show();
2835                }
2836            }
2837            break;
2838
2839            case AttachmentTypeSelectorAdapter.ADD_SOUND:
2840                MessageUtils.selectAudio(this, REQUEST_CODE_ATTACH_SOUND);
2841                break;
2842
2843            case AttachmentTypeSelectorAdapter.RECORD_SOUND:
2844                long sizeLimit = computeAttachmentSizeLimit(slideShow, currentSlideSize);
2845                MessageUtils.recordSound(this, REQUEST_CODE_RECORD_SOUND, sizeLimit);
2846                break;
2847
2848            case AttachmentTypeSelectorAdapter.ADD_SLIDESHOW:
2849                editSlideshow();
2850                break;
2851
2852            default:
2853                break;
2854        }
2855    }
2856
2857    public static long computeAttachmentSizeLimit(SlideshowModel slideShow, int currentSlideSize) {
2858        // Computer attachment size limit. Subtract 1K for some text.
2859        long sizeLimit = MmsConfig.getMaxMessageSize() - SlideshowModel.SLIDESHOW_SLOP;
2860        if (slideShow != null) {
2861            sizeLimit -= slideShow.getCurrentMessageSize();
2862
2863            // We're about to ask the camera to capture some video (or the sound recorder
2864            // to record some audio) which will eventually replace the content on the current
2865            // slide. Since the current slide already has some content (which was subtracted
2866            // out just above) and that content is going to get replaced, we can add the size of the
2867            // current slide into the available space used to capture a video (or audio).
2868            sizeLimit += currentSlideSize;
2869        }
2870        return sizeLimit;
2871    }
2872
2873    private void showAddAttachmentDialog(final boolean replace) {
2874        AlertDialog.Builder builder = new AlertDialog.Builder(this);
2875        builder.setIcon(R.drawable.ic_dialog_attach);
2876        builder.setTitle(R.string.add_attachment);
2877
2878        if (mAttachmentTypeSelectorAdapter == null) {
2879            mAttachmentTypeSelectorAdapter = new AttachmentTypeSelectorAdapter(
2880                    this, AttachmentTypeSelectorAdapter.MODE_WITH_SLIDESHOW);
2881        }
2882        builder.setAdapter(mAttachmentTypeSelectorAdapter, new DialogInterface.OnClickListener() {
2883            @Override
2884            public void onClick(DialogInterface dialog, int which) {
2885                addAttachment(mAttachmentTypeSelectorAdapter.buttonToCommand(which), replace);
2886                dialog.dismiss();
2887            }
2888        });
2889
2890        builder.show();
2891    }
2892
2893    @Override
2894    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
2895        if (LogTag.VERBOSE) {
2896            log("onActivityResult: requestCode=" + requestCode + ", resultCode=" + resultCode +
2897                    ", data=" + data);
2898        }
2899        mWaitingForSubActivity = false;          // We're back!
2900        mShouldLoadDraft = false;
2901        if (mWorkingMessage.isFakeMmsForDraft()) {
2902            // We no longer have to fake the fact we're an Mms. At this point we are or we aren't,
2903            // based on attachments and other Mms attrs.
2904            mWorkingMessage.removeFakeMmsForDraft();
2905        }
2906
2907        if (requestCode == REQUEST_CODE_PICK) {
2908            mWorkingMessage.asyncDeleteDraftSmsMessage(mConversation);
2909        }
2910
2911        if (requestCode == REQUEST_CODE_ADD_CONTACT) {
2912            // The user might have added a new contact. When we tell contacts to add a contact
2913            // and tap "Done", we're not returned to Messaging. If we back out to return to
2914            // messaging after adding a contact, the resultCode is RESULT_CANCELED. Therefore,
2915            // assume a contact was added and get the contact and force our cached contact to
2916            // get reloaded with the new info (such as contact name). After the
2917            // contact is reloaded, the function onUpdate() in this file will get called
2918            // and it will update the title bar, etc.
2919            if (mAddContactIntent != null) {
2920                String address =
2921                    mAddContactIntent.getStringExtra(ContactsContract.Intents.Insert.EMAIL);
2922                if (address == null) {
2923                    address =
2924                        mAddContactIntent.getStringExtra(ContactsContract.Intents.Insert.PHONE);
2925                }
2926                if (address != null) {
2927                    Contact contact = Contact.get(address, false);
2928                    if (contact != null) {
2929                        contact.reload();
2930                    }
2931                }
2932            }
2933        }
2934
2935        if (resultCode != RESULT_OK){
2936            if (LogTag.VERBOSE) log("bail due to resultCode=" + resultCode);
2937            return;
2938        }
2939
2940        switch (requestCode) {
2941            case REQUEST_CODE_CREATE_SLIDESHOW:
2942                if (data != null) {
2943                    WorkingMessage newMessage = WorkingMessage.load(this, data.getData());
2944                    if (newMessage != null) {
2945                        mWorkingMessage = newMessage;
2946                        mWorkingMessage.setConversation(mConversation);
2947                        updateThreadIdIfRunning();
2948                        drawTopPanel(false);
2949                        updateSendButtonState();
2950                    }
2951                }
2952                break;
2953
2954            case REQUEST_CODE_TAKE_PICTURE: {
2955                // create a file based uri and pass to addImage(). We want to read the JPEG
2956                // data directly from file (using UriImage) instead of decoding it into a Bitmap,
2957                // which takes up too much memory and could easily lead to OOM.
2958                File file = new File(TempFileProvider.getScrapPath(this));
2959                Uri uri = Uri.fromFile(file);
2960
2961                // Remove the old captured picture's thumbnail from the cache
2962                MmsApp.getApplication().getThumbnailManager().removeThumbnail(uri);
2963
2964                addImageAsync(uri, false);
2965                break;
2966            }
2967
2968            case REQUEST_CODE_ATTACH_IMAGE: {
2969                if (data != null) {
2970                    addImageAsync(data.getData(), false);
2971                }
2972                break;
2973            }
2974
2975            case REQUEST_CODE_TAKE_VIDEO:
2976                Uri videoUri = TempFileProvider.renameScrapFile(".3gp", null, this);
2977                // Remove the old captured video's thumbnail from the cache
2978                MmsApp.getApplication().getThumbnailManager().removeThumbnail(videoUri);
2979
2980                addVideoAsync(videoUri, false);      // can handle null videoUri
2981                break;
2982
2983            case REQUEST_CODE_ATTACH_VIDEO:
2984                if (data != null) {
2985                    addVideoAsync(data.getData(), false);
2986                }
2987                break;
2988
2989            case REQUEST_CODE_ATTACH_SOUND: {
2990                Uri uri = (Uri) data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
2991                if (Settings.System.DEFAULT_RINGTONE_URI.equals(uri)) {
2992                    break;
2993                }
2994                addAudio(uri);
2995                break;
2996            }
2997
2998            case REQUEST_CODE_RECORD_SOUND:
2999                if (data != null) {
3000                    addAudio(data.getData());
3001                }
3002                break;
3003
3004            case REQUEST_CODE_ECM_EXIT_DIALOG:
3005                boolean outOfEmergencyMode = data.getBooleanExtra(EXIT_ECM_RESULT, false);
3006                if (outOfEmergencyMode) {
3007                    sendMessage(false);
3008                }
3009                break;
3010
3011            case REQUEST_CODE_PICK:
3012                if (data != null) {
3013                    processPickResult(data);
3014                }
3015                break;
3016
3017            default:
3018                if (LogTag.VERBOSE) log("bail due to unknown requestCode=" + requestCode);
3019                break;
3020        }
3021    }
3022
3023    private void processPickResult(final Intent data) {
3024        // The EXTRA_PHONE_URIS stores the phone's urls that were selected by user in the
3025        // multiple phone picker.
3026        final Parcelable[] uris =
3027            data.getParcelableArrayExtra(Intents.EXTRA_PHONE_URIS);
3028
3029        final int recipientCount = uris != null ? uris.length : 0;
3030
3031        final int recipientLimit = MmsConfig.getRecipientLimit();
3032        if (recipientLimit != Integer.MAX_VALUE && recipientCount > recipientLimit) {
3033            new AlertDialog.Builder(this)
3034                    .setMessage(getString(R.string.too_many_recipients, recipientCount, recipientLimit))
3035                    .setPositiveButton(android.R.string.ok, null)
3036                    .create().show();
3037            return;
3038        }
3039
3040        final Handler handler = new Handler();
3041        final ProgressDialog progressDialog = new ProgressDialog(this);
3042        progressDialog.setTitle(getText(R.string.pick_too_many_recipients));
3043        progressDialog.setMessage(getText(R.string.adding_recipients));
3044        progressDialog.setIndeterminate(true);
3045        progressDialog.setCancelable(false);
3046
3047        final Runnable showProgress = new Runnable() {
3048            @Override
3049            public void run() {
3050                progressDialog.show();
3051            }
3052        };
3053        // Only show the progress dialog if we can not finish off parsing the return data in 1s,
3054        // otherwise the dialog could flicker.
3055        handler.postDelayed(showProgress, 1000);
3056
3057        new Thread(new Runnable() {
3058            @Override
3059            public void run() {
3060                final ContactList list;
3061                 try {
3062                    list = ContactList.blockingGetByUris(uris);
3063                } finally {
3064                    handler.removeCallbacks(showProgress);
3065                    progressDialog.dismiss();
3066                }
3067                // TODO: there is already code to update the contact header widget and recipients
3068                // editor if the contacts change. we can re-use that code.
3069                final Runnable populateWorker = new Runnable() {
3070                    @Override
3071                    public void run() {
3072                        mRecipientsEditor.populate(list);
3073                        updateTitle(list);
3074                    }
3075                };
3076                handler.post(populateWorker);
3077            }
3078        }, "ComoseMessageActivity.processPickResult").start();
3079    }
3080
3081    private final ResizeImageResultCallback mResizeImageCallback = new ResizeImageResultCallback() {
3082        // TODO: make this produce a Uri, that's what we want anyway
3083        @Override
3084        public void onResizeResult(PduPart part, boolean append) {
3085            if (part == null) {
3086                handleAddAttachmentError(WorkingMessage.UNKNOWN_ERROR, R.string.type_picture);
3087                return;
3088            }
3089
3090            Context context = ComposeMessageActivity.this;
3091            PduPersister persister = PduPersister.getPduPersister(context);
3092            int result;
3093
3094            Uri messageUri = mWorkingMessage.saveAsMms(true);
3095            if (messageUri == null) {
3096                result = WorkingMessage.UNKNOWN_ERROR;
3097            } else {
3098                try {
3099                    Uri dataUri = persister.persistPart(part,
3100                            ContentUris.parseId(messageUri), null);
3101                    result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, dataUri, append);
3102                    if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3103                        log("ResizeImageResultCallback: dataUri=" + dataUri);
3104                    }
3105                } catch (MmsException e) {
3106                    result = WorkingMessage.UNKNOWN_ERROR;
3107                }
3108            }
3109
3110            handleAddAttachmentError(result, R.string.type_picture);
3111        }
3112    };
3113
3114    private void handleAddAttachmentError(final int error, final int mediaTypeStringId) {
3115        if (error == WorkingMessage.OK) {
3116            return;
3117        }
3118        Log.d(TAG, "handleAddAttachmentError: " + error);
3119
3120        runOnUiThread(new Runnable() {
3121            @Override
3122            public void run() {
3123                Resources res = getResources();
3124                String mediaType = res.getString(mediaTypeStringId);
3125                String title, message;
3126
3127                switch(error) {
3128                case WorkingMessage.UNKNOWN_ERROR:
3129                    message = res.getString(R.string.failed_to_add_media, mediaType);
3130                    Toast.makeText(ComposeMessageActivity.this, message, Toast.LENGTH_SHORT).show();
3131                    return;
3132                case WorkingMessage.UNSUPPORTED_TYPE:
3133                    title = res.getString(R.string.unsupported_media_format, mediaType);
3134                    message = res.getString(R.string.select_different_media, mediaType);
3135                    break;
3136                case WorkingMessage.MESSAGE_SIZE_EXCEEDED:
3137                    title = res.getString(R.string.exceed_message_size_limitation, mediaType);
3138                    message = res.getString(R.string.failed_to_add_media, mediaType);
3139                    break;
3140                case WorkingMessage.IMAGE_TOO_LARGE:
3141                    title = res.getString(R.string.failed_to_resize_image);
3142                    message = res.getString(R.string.resize_image_error_information);
3143                    break;
3144                default:
3145                    throw new IllegalArgumentException("unknown error " + error);
3146                }
3147
3148                MessageUtils.showErrorDialog(ComposeMessageActivity.this, title, message);
3149            }
3150        });
3151    }
3152
3153    private void addImageAsync(final Uri uri, final boolean append) {
3154        getAsyncDialog().runAsync(new Runnable() {
3155            @Override
3156            public void run() {
3157                addImage(uri, append);
3158            }
3159        }, null, R.string.adding_attachments_title);
3160    }
3161
3162    private void addImage(Uri uri, boolean append) {
3163        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3164            log("addImage: append=" + append + ", uri=" + uri);
3165        }
3166
3167        int result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, uri, append);
3168
3169        if (result == WorkingMessage.IMAGE_TOO_LARGE ||
3170            result == WorkingMessage.MESSAGE_SIZE_EXCEEDED) {
3171            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3172                log("resize image " + uri);
3173            }
3174            MessageUtils.resizeImageAsync(ComposeMessageActivity.this,
3175                    uri, mAttachmentEditorHandler, mResizeImageCallback, append);
3176            return;
3177        }
3178        handleAddAttachmentError(result, R.string.type_picture);
3179    }
3180
3181    private void addVideoAsync(final Uri uri, final boolean append) {
3182        getAsyncDialog().runAsync(new Runnable() {
3183            @Override
3184            public void run() {
3185                addVideo(uri, append);
3186            }
3187        }, null, R.string.adding_attachments_title);
3188    }
3189
3190    private void addVideo(Uri uri, boolean append) {
3191        if (uri != null) {
3192            int result = mWorkingMessage.setAttachment(WorkingMessage.VIDEO, uri, append);
3193            handleAddAttachmentError(result, R.string.type_video);
3194        }
3195    }
3196
3197    private void addAudio(Uri uri) {
3198        int result = mWorkingMessage.setAttachment(WorkingMessage.AUDIO, uri, false);
3199        handleAddAttachmentError(result, R.string.type_audio);
3200    }
3201
3202    AsyncDialog getAsyncDialog() {
3203        if (mAsyncDialog == null) {
3204            mAsyncDialog = new AsyncDialog(this);
3205        }
3206        return mAsyncDialog;
3207    }
3208
3209    private boolean handleForwardedMessage() {
3210        Intent intent = getIntent();
3211
3212        // If this is a forwarded message, it will have an Intent extra
3213        // indicating so.  If not, bail out.
3214        if (!mForwardMessageMode) {
3215            return false;
3216        }
3217
3218        Uri uri = intent.getParcelableExtra("msg_uri");
3219
3220        if (Log.isLoggable(LogTag.APP, Log.DEBUG)) {
3221            log("" + uri);
3222        }
3223
3224        if (uri != null) {
3225            mWorkingMessage = WorkingMessage.load(this, uri);
3226            mWorkingMessage.setSubject(intent.getStringExtra("subject"), false);
3227        } else {
3228            mWorkingMessage.setText(intent.getStringExtra("sms_body"));
3229        }
3230
3231        // let's clear the message thread for forwarded messages
3232        mMsgListAdapter.changeCursor(null);
3233
3234        return true;
3235    }
3236
3237    // Handle send actions, where we're told to send a picture(s) or text.
3238    private boolean handleSendIntent() {
3239        Intent intent = getIntent();
3240        Bundle extras = intent.getExtras();
3241        if (extras == null) {
3242            return false;
3243        }
3244
3245        final String mimeType = intent.getType();
3246        String action = intent.getAction();
3247        if (Intent.ACTION_SEND.equals(action)) {
3248            if (extras.containsKey(Intent.EXTRA_STREAM)) {
3249                final Uri uri = (Uri)extras.getParcelable(Intent.EXTRA_STREAM);
3250                getAsyncDialog().runAsync(new Runnable() {
3251                    @Override
3252                    public void run() {
3253                        addAttachment(mimeType, uri, false);
3254                    }
3255                }, null, R.string.adding_attachments_title);
3256                return true;
3257            } else if (extras.containsKey(Intent.EXTRA_TEXT)) {
3258                mWorkingMessage.setText(extras.getString(Intent.EXTRA_TEXT));
3259                return true;
3260            }
3261        } else if (Intent.ACTION_SEND_MULTIPLE.equals(action) &&
3262                extras.containsKey(Intent.EXTRA_STREAM)) {
3263            SlideshowModel slideShow = mWorkingMessage.getSlideshow();
3264            final ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
3265            int currentSlideCount = slideShow != null ? slideShow.size() : 0;
3266            int importCount = uris.size();
3267            if (importCount + currentSlideCount > SlideshowEditor.MAX_SLIDE_NUM) {
3268                importCount = Math.min(SlideshowEditor.MAX_SLIDE_NUM - currentSlideCount,
3269                        importCount);
3270                Toast.makeText(ComposeMessageActivity.this,
3271                        getString(R.string.too_many_attachments,
3272                                SlideshowEditor.MAX_SLIDE_NUM, importCount),
3273                                Toast.LENGTH_LONG).show();
3274            }
3275
3276            // Attach all the pictures/videos asynchronously off of the UI thread.
3277            // Show a progress dialog if adding all the slides hasn't finished
3278            // within half a second.
3279            final int numberToImport = importCount;
3280            getAsyncDialog().runAsync(new Runnable() {
3281                @Override
3282                public void run() {
3283                    for (int i = 0; i < numberToImport; i++) {
3284                        Parcelable uri = uris.get(i);
3285                        addAttachment(mimeType, (Uri) uri, true);
3286                    }
3287                }
3288            }, null, R.string.adding_attachments_title);
3289            return true;
3290        }
3291        return false;
3292    }
3293
3294    // mVideoUri will look like this: content://media/external/video/media
3295    private static final String mVideoUri = Video.Media.getContentUri("external").toString();
3296    // mImageUri will look like this: content://media/external/images/media
3297    private static final String mImageUri = Images.Media.getContentUri("external").toString();
3298
3299    private void addAttachment(String type, Uri uri, boolean append) {
3300        if (uri != null) {
3301            // When we're handling Intent.ACTION_SEND_MULTIPLE, the passed in items can be
3302            // videos, and/or images, and/or some other unknown types we don't handle. When
3303            // a single attachment is "shared" the type will specify an image or video. When
3304            // there are multiple types, the type passed in is "*/*". In that case, we've got
3305            // to look at the uri to figure out if it is an image or video.
3306            boolean wildcard = "*/*".equals(type);
3307            if (type.startsWith("image/") || (wildcard && uri.toString().startsWith(mImageUri))) {
3308                addImage(uri, append);
3309            } else if (type.startsWith("video/") ||
3310                    (wildcard && uri.toString().startsWith(mVideoUri))) {
3311                addVideo(uri, append);
3312            }
3313        }
3314    }
3315
3316    private String getResourcesString(int id, String mediaName) {
3317        Resources r = getResources();
3318        return r.getString(id, mediaName);
3319    }
3320
3321    /**
3322     * draw the compose view at the bottom of the screen.
3323     */
3324    private void drawBottomPanel() {
3325        // Reset the counter for text editor.
3326        resetCounter();
3327
3328        if (mWorkingMessage.hasSlideshow()) {
3329            mBottomPanel.setVisibility(View.GONE);
3330            mAttachmentEditor.requestFocus();
3331            return;
3332        }
3333
3334        if (LOCAL_LOGV) {
3335            Log.v(TAG, "CMA.drawBottomPanel");
3336        }
3337        mBottomPanel.setVisibility(View.VISIBLE);
3338
3339        CharSequence text = mWorkingMessage.getText();
3340
3341        // TextView.setTextKeepState() doesn't like null input.
3342        if (text != null) {
3343            mTextEditor.setTextKeepState(text);
3344
3345            // Set the edit caret to the end of the text.
3346            mTextEditor.setSelection(mTextEditor.length());
3347        } else {
3348            mTextEditor.setText("");
3349        }
3350    }
3351
3352    private void hideBottomPanel() {
3353        if (LOCAL_LOGV) {
3354            Log.v(TAG, "CMA.hideBottomPanel");
3355        }
3356        mBottomPanel.setVisibility(View.INVISIBLE);
3357    }
3358
3359    private void drawTopPanel(boolean showSubjectEditor) {
3360        boolean showingAttachment = mAttachmentEditor.update(mWorkingMessage);
3361        mAttachmentEditorScrollView.setVisibility(showingAttachment ? View.VISIBLE : View.GONE);
3362        showSubjectEditor(showSubjectEditor || mWorkingMessage.hasSubject());
3363
3364        invalidateOptionsMenu();
3365    }
3366
3367    //==========================================================
3368    // Interface methods
3369    //==========================================================
3370
3371    @Override
3372    public void onClick(View v) {
3373        if ((v == mSendButtonSms || v == mSendButtonMms) && isPreparedForSending()) {
3374            confirmSendMessageIfNeeded();
3375        } else if ((v == mRecipientsPicker)) {
3376            launchMultiplePhonePicker();
3377        }
3378    }
3379
3380    private void launchMultiplePhonePicker() {
3381        Intent intent = new Intent(Intents.ACTION_GET_MULTIPLE_PHONES);
3382        intent.addCategory("android.intent.category.DEFAULT");
3383        intent.setType(Phone.CONTENT_TYPE);
3384        // We have to wait for the constructing complete.
3385        ContactList contacts = mRecipientsEditor.constructContactsFromInput(true);
3386        int urisCount = 0;
3387        Uri[] uris = new Uri[contacts.size()];
3388        urisCount = 0;
3389        for (Contact contact : contacts) {
3390            if (Contact.CONTACT_METHOD_TYPE_PHONE == contact.getContactMethodType()) {
3391                    uris[urisCount++] = contact.getPhoneUri();
3392            }
3393        }
3394        if (urisCount > 0) {
3395            intent.putExtra(Intents.EXTRA_PHONE_URIS, uris);
3396        }
3397        startActivityForResult(intent, REQUEST_CODE_PICK);
3398    }
3399
3400    @Override
3401    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
3402        if (event != null) {
3403            // if shift key is down, then we want to insert the '\n' char in the TextView;
3404            // otherwise, the default action is to send the message.
3405            if (!event.isShiftPressed() && event.getAction() == KeyEvent.ACTION_DOWN) {
3406                if (isPreparedForSending()) {
3407                    confirmSendMessageIfNeeded();
3408                }
3409                return true;
3410            }
3411            return false;
3412        }
3413
3414        if (isPreparedForSending()) {
3415            confirmSendMessageIfNeeded();
3416        }
3417        return true;
3418    }
3419
3420    private final TextWatcher mTextEditorWatcher = new TextWatcher() {
3421        @Override
3422        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3423        }
3424
3425        @Override
3426        public void onTextChanged(CharSequence s, int start, int before, int count) {
3427            // This is a workaround for bug 1609057.  Since onUserInteraction() is
3428            // not called when the user touches the soft keyboard, we pretend it was
3429            // called when textfields changes.  This should be removed when the bug
3430            // is fixed.
3431            onUserInteraction();
3432
3433            mWorkingMessage.setText(s);
3434
3435            updateSendButtonState();
3436
3437            updateCounter(s, start, before, count);
3438
3439            ensureCorrectButtonHeight();
3440        }
3441
3442        @Override
3443        public void afterTextChanged(Editable s) {
3444        }
3445    };
3446
3447    /**
3448     * Ensures that if the text edit box extends past two lines then the
3449     * button will be shifted up to allow enough space for the character
3450     * counter string to be placed beneath it.
3451     */
3452    private void ensureCorrectButtonHeight() {
3453        int currentTextLines = mTextEditor.getLineCount();
3454        if (currentTextLines <= 2) {
3455            mTextCounter.setVisibility(View.GONE);
3456        }
3457        else if (currentTextLines > 2 && mTextCounter.getVisibility() == View.GONE) {
3458            // Making the counter invisible ensures that it is used to correctly
3459            // calculate the position of the send button even if we choose not to
3460            // display the text.
3461            mTextCounter.setVisibility(View.INVISIBLE);
3462        }
3463    }
3464
3465    private final TextWatcher mSubjectEditorWatcher = new TextWatcher() {
3466        @Override
3467        public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
3468
3469        @Override
3470        public void onTextChanged(CharSequence s, int start, int before, int count) {
3471            mWorkingMessage.setSubject(s, true);
3472            updateSendButtonState();
3473        }
3474
3475        @Override
3476        public void afterTextChanged(Editable s) { }
3477    };
3478
3479    //==========================================================
3480    // Private methods
3481    //==========================================================
3482
3483    /**
3484     * Initialize all UI elements from resources.
3485     */
3486    private void initResourceRefs() {
3487        mMsgListView = (MessageListView) findViewById(R.id.history);
3488        mMsgListView.setDivider(null);      // no divider so we look like IM conversation.
3489
3490        // called to enable us to show some padding between the message list and the
3491        // input field but when the message list is scrolled that padding area is filled
3492        // in with message content
3493        mMsgListView.setClipToPadding(false);
3494
3495        mMsgListView.setOnSizeChangedListener(new OnSizeChangedListener() {
3496            public void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
3497                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3498                    Log.v(TAG, "onSizeChanged: w=" + width + " h=" + height +
3499                            " oldw=" + oldWidth + " oldh=" + oldHeight);
3500                }
3501
3502                if (!mMessagesAndDraftLoaded && (oldHeight-height > SMOOTH_SCROLL_THRESHOLD)) {
3503                    // perform the delayed loading now, after keyboard opens
3504                    loadMessagesAndDraft(3);
3505                }
3506
3507
3508                // The message list view changed size, most likely because the keyboard
3509                // appeared or disappeared or the user typed/deleted chars in the message
3510                // box causing it to change its height when expanding/collapsing to hold more
3511                // lines of text.
3512                smoothScrollToEnd(false, height - oldHeight);
3513            }
3514        });
3515
3516        mBottomPanel = findViewById(R.id.bottom_panel);
3517        mTextEditor = (EditText) findViewById(R.id.embedded_text_editor);
3518        mTextEditor.setOnEditorActionListener(this);
3519        mTextEditor.addTextChangedListener(mTextEditorWatcher);
3520        mTextEditor.setFilters(new InputFilter[] {
3521                new LengthFilter(MmsConfig.getMaxTextLimit())});
3522        mTextCounter = (TextView) findViewById(R.id.text_counter);
3523        mSendButtonMms = (TextView) findViewById(R.id.send_button_mms);
3524        mSendButtonSms = (ImageButton) findViewById(R.id.send_button_sms);
3525        mSendButtonMms.setOnClickListener(this);
3526        mSendButtonSms.setOnClickListener(this);
3527        mTopPanel = findViewById(R.id.recipients_subject_linear);
3528        mTopPanel.setFocusable(false);
3529        mAttachmentEditor = (AttachmentEditor) findViewById(R.id.attachment_editor);
3530        mAttachmentEditor.setHandler(mAttachmentEditorHandler);
3531        mAttachmentEditorScrollView = findViewById(R.id.attachment_editor_scroll_view);
3532    }
3533
3534    private void confirmDeleteDialog(OnClickListener listener, boolean locked) {
3535        AlertDialog.Builder builder = new AlertDialog.Builder(this);
3536        builder.setCancelable(true);
3537        builder.setMessage(locked ? R.string.confirm_delete_locked_message :
3538                    R.string.confirm_delete_message);
3539        builder.setPositiveButton(R.string.delete, listener);
3540        builder.setNegativeButton(R.string.no, null);
3541        builder.show();
3542    }
3543
3544    void undeliveredMessageDialog(long date) {
3545        String body;
3546
3547        if (date >= 0) {
3548            body = getString(R.string.undelivered_msg_dialog_body,
3549                    MessageUtils.formatTimeStampString(this, date));
3550        } else {
3551            // FIXME: we can not get sms retry time.
3552            body = getString(R.string.undelivered_sms_dialog_body);
3553        }
3554
3555        Toast.makeText(this, body, Toast.LENGTH_LONG).show();
3556    }
3557
3558    private void startMsgListQuery() {
3559        startMsgListQuery(MESSAGE_LIST_QUERY_TOKEN);
3560    }
3561
3562    private void startMsgListQuery(int token) {
3563        if (mSendDiscreetMode) {
3564            return;
3565        }
3566        Uri conversationUri = mConversation.getUri();
3567
3568        if (conversationUri == null) {
3569            log("##### startMsgListQuery: conversationUri is null, bail!");
3570            return;
3571        }
3572
3573        long threadId = mConversation.getThreadId();
3574        if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3575            log("startMsgListQuery for " + conversationUri + ", threadId=" + threadId +
3576                    " token: " + token + " mConversation: " + mConversation);
3577        }
3578
3579        // Cancel any pending queries
3580        mBackgroundQueryHandler.cancelOperation(token);
3581        try {
3582            // Kick off the new query
3583            mBackgroundQueryHandler.startQuery(
3584                    token,
3585                    threadId /* cookie */,
3586                    conversationUri,
3587                    PROJECTION,
3588                    null, null, null);
3589        } catch (SQLiteException e) {
3590            SqliteWrapper.checkSQLiteException(this, e);
3591        }
3592    }
3593
3594    private void initMessageList() {
3595        if (mMsgListAdapter != null) {
3596            return;
3597        }
3598
3599        String highlightString = getIntent().getStringExtra("highlight");
3600        Pattern highlight = highlightString == null
3601            ? null
3602            : Pattern.compile("\\b" + Pattern.quote(highlightString), Pattern.CASE_INSENSITIVE);
3603
3604        // Initialize the list adapter with a null cursor.
3605        mMsgListAdapter = new MessageListAdapter(this, null, mMsgListView, true, highlight);
3606        mMsgListAdapter.setOnDataSetChangedListener(mDataSetChangedListener);
3607        mMsgListAdapter.setMsgListItemHandler(mMessageListItemHandler);
3608        mMsgListView.setAdapter(mMsgListAdapter);
3609        mMsgListView.setItemsCanFocus(false);
3610        mMsgListView.setVisibility(mSendDiscreetMode ? View.INVISIBLE : View.VISIBLE);
3611        mMsgListView.setOnCreateContextMenuListener(mMsgListMenuCreateListener);
3612        mMsgListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
3613            @Override
3614            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
3615                if (view != null) {
3616                    ((MessageListItem) view).onMessageListItemClick();
3617                }
3618            }
3619        });
3620    }
3621
3622    /**
3623     * Load the draft
3624     *
3625     * If mWorkingMessage has content in memory that's worth saving, return false.
3626     * Otherwise, call the async operation to load draft and return true.
3627     */
3628    private boolean loadDraft() {
3629        if (mWorkingMessage.isWorthSaving()) {
3630            Log.w(TAG, "CMA.loadDraft: called with non-empty working message, bail");
3631            return false;
3632        }
3633
3634        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3635            log("CMA.loadDraft");
3636        }
3637
3638        mWorkingMessage = WorkingMessage.loadDraft(this, mConversation,
3639                new Runnable() {
3640                    @Override
3641                    public void run() {
3642                        drawTopPanel(false);
3643                        drawBottomPanel();
3644                        updateSendButtonState();
3645                    }
3646                });
3647
3648        // WorkingMessage.loadDraft() can return a new WorkingMessage object that doesn't
3649        // have its conversation set. Make sure it is set.
3650        mWorkingMessage.setConversation(mConversation);
3651
3652        return true;
3653    }
3654
3655    private void saveDraft(boolean isStopping) {
3656        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3657            LogTag.debug("saveDraft");
3658        }
3659        // TODO: Do something better here.  Maybe make discard() legal
3660        // to call twice and make isEmpty() return true if discarded
3661        // so it is caught in the clause above this one?
3662        if (mWorkingMessage.isDiscarded()) {
3663            return;
3664        }
3665
3666        if (!mWaitingForSubActivity &&
3667                !mWorkingMessage.isWorthSaving() &&
3668                (!isRecipientsEditorVisible() || recipientCount() == 0)) {
3669            if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3670                log("not worth saving, discard WorkingMessage and bail");
3671            }
3672            mWorkingMessage.discard();
3673            return;
3674        }
3675
3676        mWorkingMessage.saveDraft(isStopping);
3677
3678        if (mToastForDraftSave) {
3679            Toast.makeText(this, R.string.message_saved_as_draft,
3680                    Toast.LENGTH_SHORT).show();
3681        }
3682    }
3683
3684    private boolean isPreparedForSending() {
3685        int recipientCount = recipientCount();
3686
3687        return recipientCount > 0 && recipientCount <= MmsConfig.getRecipientLimit() &&
3688            (mWorkingMessage.hasAttachment() ||
3689                    mWorkingMessage.hasText() ||
3690                    mWorkingMessage.hasSubject());
3691    }
3692
3693    private int recipientCount() {
3694        int recipientCount;
3695
3696        // To avoid creating a bunch of invalid Contacts when the recipients
3697        // editor is in flux, we keep the recipients list empty.  So if the
3698        // recipients editor is showing, see if there is anything in it rather
3699        // than consulting the empty recipient list.
3700        if (isRecipientsEditorVisible()) {
3701            recipientCount = mRecipientsEditor.getRecipientCount();
3702        } else {
3703            recipientCount = getRecipients().size();
3704        }
3705        return recipientCount;
3706    }
3707
3708    private void sendMessage(boolean bCheckEcmMode) {
3709        if (bCheckEcmMode) {
3710            // TODO: expose this in telephony layer for SDK build
3711            String inEcm = SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE);
3712            if (Boolean.parseBoolean(inEcm)) {
3713                try {
3714                    startActivityForResult(
3715                            new Intent(TelephonyIntents.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS, null),
3716                            REQUEST_CODE_ECM_EXIT_DIALOG);
3717                    return;
3718                } catch (ActivityNotFoundException e) {
3719                    // continue to send message
3720                    Log.e(TAG, "Cannot find EmergencyCallbackModeExitDialog", e);
3721                }
3722            }
3723        }
3724
3725        if (!mSendingMessage) {
3726            if (LogTag.SEVERE_WARNING) {
3727                String sendingRecipients = mConversation.getRecipients().serialize();
3728                if (!sendingRecipients.equals(mDebugRecipients)) {
3729                    String workingRecipients = mWorkingMessage.getWorkingRecipients();
3730                    if (!mDebugRecipients.equals(workingRecipients)) {
3731                        LogTag.warnPossibleRecipientMismatch("ComposeMessageActivity.sendMessage" +
3732                                " recipients in window: \"" +
3733                                mDebugRecipients + "\" differ from recipients from conv: \"" +
3734                                sendingRecipients + "\" and working recipients: " +
3735                                workingRecipients, this);
3736                    }
3737                }
3738                sanityCheckConversation();
3739            }
3740
3741            // send can change the recipients. Make sure we remove the listeners first and then add
3742            // them back once the recipient list has settled.
3743            removeRecipientsListeners();
3744
3745            mWorkingMessage.send(mDebugRecipients);
3746
3747            mSentMessage = true;
3748            mSendingMessage = true;
3749            addRecipientsListeners();
3750
3751            mScrollOnSend = true;   // in the next onQueryComplete, scroll the list to the end.
3752        }
3753        // But bail out if we are supposed to exit after the message is sent.
3754        if (mSendDiscreetMode) {
3755            finish();
3756        }
3757    }
3758
3759    private void resetMessage() {
3760        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3761            log("resetMessage");
3762        }
3763
3764        // Make the attachment editor hide its view.
3765        mAttachmentEditor.hideView();
3766        mAttachmentEditorScrollView.setVisibility(View.GONE);
3767
3768        // Hide the subject editor.
3769        showSubjectEditor(false);
3770
3771        // Focus to the text editor.
3772        mTextEditor.requestFocus();
3773
3774        // We have to remove the text change listener while the text editor gets cleared and
3775        // we subsequently turn the message back into SMS. When the listener is listening while
3776        // doing the clearing, it's fighting to update its counts and itself try and turn
3777        // the message one way or the other.
3778        mTextEditor.removeTextChangedListener(mTextEditorWatcher);
3779
3780        // Clear the text box.
3781        TextKeyListener.clear(mTextEditor.getText());
3782
3783        mWorkingMessage.clearConversation(mConversation, false);
3784        mWorkingMessage = WorkingMessage.createEmpty(this);
3785        mWorkingMessage.setConversation(mConversation);
3786
3787        hideRecipientEditor();
3788        drawBottomPanel();
3789
3790        // "Or not", in this case.
3791        updateSendButtonState();
3792
3793        // Our changes are done. Let the listener respond to text changes once again.
3794        mTextEditor.addTextChangedListener(mTextEditorWatcher);
3795
3796        // Close the soft on-screen keyboard if we're in landscape mode so the user can see the
3797        // conversation.
3798        if (mIsLandscape) {
3799            hideKeyboard();
3800        }
3801
3802        mLastRecipientCount = 0;
3803        mSendingMessage = false;
3804        invalidateOptionsMenu();
3805   }
3806
3807    private void hideKeyboard() {
3808        InputMethodManager inputMethodManager =
3809            (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
3810        inputMethodManager.hideSoftInputFromWindow(mTextEditor.getWindowToken(), 0);
3811    }
3812
3813    private void updateSendButtonState() {
3814        boolean enable = false;
3815        if (isPreparedForSending()) {
3816            // When the type of attachment is slideshow, we should
3817            // also hide the 'Send' button since the slideshow view
3818            // already has a 'Send' button embedded.
3819            if (!mWorkingMessage.hasSlideshow()) {
3820                enable = true;
3821            } else {
3822                mAttachmentEditor.setCanSend(true);
3823            }
3824        } else if (null != mAttachmentEditor){
3825            mAttachmentEditor.setCanSend(false);
3826        }
3827
3828        boolean requiresMms = mWorkingMessage.requiresMms();
3829        View sendButton = showSmsOrMmsSendButton(requiresMms);
3830        sendButton.setEnabled(enable);
3831        sendButton.setFocusable(enable);
3832    }
3833
3834    private long getMessageDate(Uri uri) {
3835        if (uri != null) {
3836            Cursor cursor = SqliteWrapper.query(this, mContentResolver,
3837                    uri, new String[] { Mms.DATE }, null, null, null);
3838            if (cursor != null) {
3839                try {
3840                    if ((cursor.getCount() == 1) && cursor.moveToFirst()) {
3841                        return cursor.getLong(0) * 1000L;
3842                    }
3843                } finally {
3844                    cursor.close();
3845                }
3846            }
3847        }
3848        return NO_DATE_FOR_DIALOG;
3849    }
3850
3851    private void initActivityState(Bundle bundle) {
3852        Intent intent = getIntent();
3853        if (bundle != null) {
3854            setIntent(getIntent().setAction(Intent.ACTION_VIEW));
3855            String recipients = bundle.getString(RECIPIENTS);
3856            if (LogTag.VERBOSE) log("get mConversation by recipients " + recipients);
3857            mConversation = Conversation.get(this,
3858                    ContactList.getByNumbers(recipients,
3859                            false /* don't block */, true /* replace number */), false);
3860            addRecipientsListeners();
3861            mSendDiscreetMode = bundle.getBoolean(KEY_EXIT_ON_SENT, false);
3862            mForwardMessageMode = bundle.getBoolean(KEY_FORWARDED_MESSAGE, false);
3863
3864            if (mSendDiscreetMode) {
3865                mMsgListView.setVisibility(View.INVISIBLE);
3866            }
3867            mWorkingMessage.readStateFromBundle(bundle);
3868
3869            return;
3870        }
3871
3872        // If we have been passed a thread_id, use that to find our conversation.
3873        long threadId = intent.getLongExtra(THREAD_ID, 0);
3874        if (threadId > 0) {
3875            if (LogTag.VERBOSE) log("get mConversation by threadId " + threadId);
3876            mConversation = Conversation.get(this, threadId, false);
3877        } else {
3878            Uri intentData = intent.getData();
3879            if (intentData != null) {
3880                // try to get a conversation based on the data URI passed to our intent.
3881                if (LogTag.VERBOSE) log("get mConversation by intentData " + intentData);
3882                mConversation = Conversation.get(this, intentData, false);
3883                mWorkingMessage.setText(getBody(intentData));
3884            } else {
3885                // special intent extra parameter to specify the address
3886                String address = intent.getStringExtra("address");
3887                if (!TextUtils.isEmpty(address)) {
3888                    if (LogTag.VERBOSE) log("get mConversation by address " + address);
3889                    mConversation = Conversation.get(this, ContactList.getByNumbers(address,
3890                            false /* don't block */, true /* replace number */), false);
3891                } else {
3892                    if (LogTag.VERBOSE) log("create new conversation");
3893                    mConversation = Conversation.createNew(this);
3894                }
3895            }
3896        }
3897        addRecipientsListeners();
3898        updateThreadIdIfRunning();
3899
3900        mSendDiscreetMode = intent.getBooleanExtra(KEY_EXIT_ON_SENT, false);
3901        mForwardMessageMode = intent.getBooleanExtra(KEY_FORWARDED_MESSAGE, false);
3902        if (mSendDiscreetMode) {
3903            mMsgListView.setVisibility(View.INVISIBLE);
3904        }
3905        if (intent.hasExtra("sms_body")) {
3906            mWorkingMessage.setText(intent.getStringExtra("sms_body"));
3907        }
3908        mWorkingMessage.setSubject(intent.getStringExtra("subject"), false);
3909    }
3910
3911    private void initFocus() {
3912        if (!mIsKeyboardOpen) {
3913            return;
3914        }
3915
3916        // If the recipients editor is visible, there is nothing in it,
3917        // and the text editor is not already focused, focus the
3918        // recipients editor.
3919        if (isRecipientsEditorVisible()
3920                && TextUtils.isEmpty(mRecipientsEditor.getText())
3921                && !mTextEditor.isFocused()) {
3922            mRecipientsEditor.requestFocus();
3923            return;
3924        }
3925
3926        // If we decided not to focus the recipients editor, focus the text editor.
3927        mTextEditor.requestFocus();
3928    }
3929
3930    private final MessageListAdapter.OnDataSetChangedListener
3931                    mDataSetChangedListener = new MessageListAdapter.OnDataSetChangedListener() {
3932        @Override
3933        public void onDataSetChanged(MessageListAdapter adapter) {
3934        }
3935
3936        @Override
3937        public void onContentChanged(MessageListAdapter adapter) {
3938            startMsgListQuery();
3939        }
3940    };
3941
3942    /**
3943     * smoothScrollToEnd will scroll the message list to the bottom if the list is already near
3944     * the bottom. Typically this is called to smooth scroll a newly received message into view.
3945     * It's also called when sending to scroll the list to the bottom, regardless of where it is,
3946     * so the user can see the just sent message. This function is also called when the message
3947     * list view changes size because the keyboard state changed or the compose message field grew.
3948     *
3949     * @param force always scroll to the bottom regardless of current list position
3950     * @param listSizeChange the amount the message list view size has vertically changed
3951     */
3952    private void smoothScrollToEnd(boolean force, int listSizeChange) {
3953        int lastItemVisible = mMsgListView.getLastVisiblePosition();
3954        int lastItemInList = mMsgListAdapter.getCount() - 1;
3955        if (lastItemVisible < 0 || lastItemInList < 0) {
3956            if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3957                Log.v(TAG, "smoothScrollToEnd: lastItemVisible=" + lastItemVisible +
3958                        ", lastItemInList=" + lastItemInList +
3959                        ", mMsgListView not ready");
3960            }
3961            return;
3962        }
3963
3964        View lastChildVisible =
3965                mMsgListView.getChildAt(lastItemVisible - mMsgListView.getFirstVisiblePosition());
3966        int lastVisibleItemBottom = 0;
3967        int lastVisibleItemHeight = 0;
3968        if (lastChildVisible != null) {
3969            lastVisibleItemBottom = lastChildVisible.getBottom();
3970            lastVisibleItemHeight = lastChildVisible.getHeight();
3971        }
3972
3973        if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3974            Log.v(TAG, "smoothScrollToEnd newPosition: " + lastItemInList +
3975                    " mLastSmoothScrollPosition: " + mLastSmoothScrollPosition +
3976                    " first: " + mMsgListView.getFirstVisiblePosition() +
3977                    " lastItemVisible: " + lastItemVisible +
3978                    " lastVisibleItemBottom: " + lastVisibleItemBottom +
3979                    " lastVisibleItemBottom + listSizeChange: " +
3980                    (lastVisibleItemBottom + listSizeChange) +
3981                    " mMsgListView.getHeight() - mMsgListView.getPaddingBottom(): " +
3982                    (mMsgListView.getHeight() - mMsgListView.getPaddingBottom()) +
3983                    " listSizeChange: " + listSizeChange);
3984        }
3985        // Only scroll if the list if we're responding to a newly sent message (force == true) or
3986        // the list is already scrolled to the end. This code also has to handle the case where
3987        // the listview has changed size (from the keyboard coming up or down or the message entry
3988        // field growing/shrinking) and it uses that grow/shrink factor in listSizeChange to
3989        // compute whether the list was at the end before the resize took place.
3990        // For example, when the keyboard comes up, listSizeChange will be negative, something
3991        // like -524. The lastChild listitem's bottom value will be the old value before the
3992        // keyboard became visible but the size of the list will have changed. The test below
3993        // add listSizeChange to bottom to figure out if the old position was already scrolled
3994        // to the bottom. We also scroll the list if the last item is taller than the size of the
3995        // list. This happens when the keyboard is up and the last item is an mms with an
3996        // attachment thumbnail, such as picture. In this situation, we want to scroll the list so
3997        // the bottom of the thumbnail is visible and the top of the item is scroll off the screen.
3998        int listHeight = mMsgListView.getHeight();
3999        boolean lastItemTooTall = lastVisibleItemHeight > listHeight;
4000        boolean willScroll = force ||
4001                ((listSizeChange != 0 || lastItemInList != mLastSmoothScrollPosition) &&
4002                lastVisibleItemBottom + listSizeChange <=
4003                    listHeight - mMsgListView.getPaddingBottom());
4004        if (willScroll || (lastItemTooTall && lastItemInList == lastItemVisible)) {
4005            if (Math.abs(listSizeChange) > SMOOTH_SCROLL_THRESHOLD) {
4006                // When the keyboard comes up, the window manager initiates a cross fade
4007                // animation that conflicts with smooth scroll. Handle that case by jumping the
4008                // list directly to the end.
4009                if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
4010                    Log.v(TAG, "keyboard state changed. setSelection=" + lastItemInList);
4011                }
4012                if (lastItemTooTall) {
4013                    // If the height of the last item is taller than the whole height of the list,
4014                    // we need to scroll that item so that its top is negative or above the top of
4015                    // the list. That way, the bottom of the last item will be exposed above the
4016                    // keyboard.
4017                    mMsgListView.setSelectionFromTop(lastItemInList,
4018                            listHeight - lastVisibleItemHeight);
4019                } else {
4020                    mMsgListView.setSelection(lastItemInList);
4021                }
4022            } else if (lastItemInList - lastItemVisible > MAX_ITEMS_TO_INVOKE_SCROLL_SHORTCUT) {
4023                if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
4024                    Log.v(TAG, "too many to scroll, setSelection=" + lastItemInList);
4025                }
4026                mMsgListView.setSelection(lastItemInList);
4027            } else {
4028                if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
4029                    Log.v(TAG, "smooth scroll to " + lastItemInList);
4030                }
4031                if (lastItemTooTall) {
4032                    // If the height of the last item is taller than the whole height of the list,
4033                    // we need to scroll that item so that its top is negative or above the top of
4034                    // the list. That way, the bottom of the last item will be exposed above the
4035                    // keyboard. We should use smoothScrollToPositionFromTop here, but it doesn't
4036                    // seem to work -- the list ends up scrolling to a random position.
4037                    mMsgListView.setSelectionFromTop(lastItemInList,
4038                            listHeight - lastVisibleItemHeight);
4039                } else {
4040                    mMsgListView.smoothScrollToPosition(lastItemInList);
4041                }
4042                mLastSmoothScrollPosition = lastItemInList;
4043            }
4044        }
4045    }
4046
4047    private final class BackgroundQueryHandler extends ConversationQueryHandler {
4048        public BackgroundQueryHandler(ContentResolver contentResolver) {
4049            super(contentResolver);
4050        }
4051
4052        @Override
4053        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
4054            switch(token) {
4055                case MESSAGE_LIST_QUERY_TOKEN:
4056                    mConversation.blockMarkAsRead(false);
4057
4058                    // check consistency between the query result and 'mConversation'
4059                    long tid = (Long) cookie;
4060
4061                    if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
4062                        log("##### onQueryComplete: msg history result for threadId " + tid);
4063                    }
4064                    if (tid != mConversation.getThreadId()) {
4065                        log("onQueryComplete: msg history query result is for threadId " +
4066                                tid + ", but mConversation has threadId " +
4067                                mConversation.getThreadId() + " starting a new query");
4068                        if (cursor != null) {
4069                            cursor.close();
4070                        }
4071                        startMsgListQuery();
4072                        return;
4073                    }
4074
4075                    // check consistency b/t mConversation & mWorkingMessage.mConversation
4076                    ComposeMessageActivity.this.sanityCheckConversation();
4077
4078                    int newSelectionPos = -1;
4079                    long targetMsgId = getIntent().getLongExtra("select_id", -1);
4080                    if (targetMsgId != -1) {
4081                        cursor.moveToPosition(-1);
4082                        while (cursor.moveToNext()) {
4083                            long msgId = cursor.getLong(COLUMN_ID);
4084                            if (msgId == targetMsgId) {
4085                                newSelectionPos = cursor.getPosition();
4086                                break;
4087                            }
4088                        }
4089                    } else if (mSavedScrollPosition != -1) {
4090                        // mSavedScrollPosition is set when this activity pauses. If equals maxint,
4091                        // it means the message list was scrolled to the end. Meanwhile, messages
4092                        // could have been received. When the activity resumes and we were
4093                        // previously scrolled to the end, jump the list so any new messages are
4094                        // visible.
4095                        if (mSavedScrollPosition == Integer.MAX_VALUE) {
4096                            int cnt = mMsgListAdapter.getCount();
4097                            if (cnt > 0) {
4098                                // Have to wait until the adapter is loaded before jumping to
4099                                // the end.
4100                                newSelectionPos = cnt - 1;
4101                                mSavedScrollPosition = -1;
4102                            }
4103                        } else {
4104                            // remember the saved scroll position before the activity is paused.
4105                            // reset it after the message list query is done
4106                            newSelectionPos = mSavedScrollPosition;
4107                            mSavedScrollPosition = -1;
4108                        }
4109                    }
4110
4111                    mMsgListAdapter.changeCursor(cursor);
4112
4113                    if (newSelectionPos != -1) {
4114                        mMsgListView.setSelection(newSelectionPos);     // jump the list to the pos
4115                    } else {
4116                        int count = mMsgListAdapter.getCount();
4117                        long lastMsgId = 0;
4118                        if (count > 0) {
4119                            cursor.moveToLast();
4120                            lastMsgId = cursor.getLong(COLUMN_ID);
4121                        }
4122                        // mScrollOnSend is set when we send a message. We always want to scroll
4123                        // the message list to the end when we send a message, but have to wait
4124                        // until the DB has changed. We also want to scroll the list when a
4125                        // new message has arrived.
4126                        smoothScrollToEnd(mScrollOnSend || lastMsgId != mLastMessageId, 0);
4127                        mLastMessageId = lastMsgId;
4128                        mScrollOnSend = false;
4129                    }
4130                    // Adjust the conversation's message count to match reality. The
4131                    // conversation's message count is eventually used in
4132                    // WorkingMessage.clearConversation to determine whether to delete
4133                    // the conversation or not.
4134                    mConversation.setMessageCount(mMsgListAdapter.getCount());
4135
4136                    // Once we have completed the query for the message history, if
4137                    // there is nothing in the cursor and we are not composing a new
4138                    // message, we must be editing a draft in a new conversation (unless
4139                    // mSentMessage is true).
4140                    // Show the recipients editor to give the user a chance to add
4141                    // more people before the conversation begins.
4142                    if (cursor.getCount() == 0 && !isRecipientsEditorVisible() && !mSentMessage) {
4143                        initRecipientsEditor();
4144                    }
4145
4146                    // FIXME: freshing layout changes the focused view to an unexpected
4147                    // one, set it back to TextEditor forcely.
4148                    mTextEditor.requestFocus();
4149
4150                    invalidateOptionsMenu();    // some menu items depend on the adapter's count
4151                    return;
4152
4153                case ConversationList.HAVE_LOCKED_MESSAGES_TOKEN:
4154                    @SuppressWarnings("unchecked")
4155                    ArrayList<Long> threadIds = (ArrayList<Long>)cookie;
4156                    ConversationList.confirmDeleteThreadDialog(
4157                            new ConversationList.DeleteThreadListener(threadIds,
4158                                mBackgroundQueryHandler, ComposeMessageActivity.this),
4159                            threadIds,
4160                            cursor != null && cursor.getCount() > 0,
4161                            ComposeMessageActivity.this);
4162                    if (cursor != null) {
4163                        cursor.close();
4164                    }
4165                    break;
4166
4167                case MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN:
4168                    // check consistency between the query result and 'mConversation'
4169                    tid = (Long) cookie;
4170
4171                    if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
4172                        log("##### onQueryComplete (after delete): msg history result for threadId "
4173                                + tid);
4174                    }
4175                    if (cursor == null) {
4176                        return;
4177                    }
4178                    if (tid > 0 && cursor.getCount() == 0) {
4179                        // We just deleted the last message and the thread will get deleted
4180                        // by a trigger in the database. Clear the threadId so next time we
4181                        // need the threadId a new thread will get created.
4182                        log("##### MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN clearing thread id: "
4183                                + tid);
4184                        Conversation conv = Conversation.get(ComposeMessageActivity.this, tid,
4185                                false);
4186                        if (conv != null) {
4187                            conv.clearThreadId();
4188                            conv.setDraftState(false);
4189                        }
4190                        // The last message in this converation was just deleted. Send the user
4191                        // to the conversation list.
4192                        exitComposeMessageActivity(new Runnable() {
4193                            @Override
4194                            public void run() {
4195                                goToConversationList();
4196                            }
4197                        });
4198                    }
4199                    cursor.close();
4200            }
4201        }
4202
4203        @Override
4204        protected void onDeleteComplete(int token, Object cookie, int result) {
4205            super.onDeleteComplete(token, cookie, result);
4206            switch(token) {
4207                case ConversationList.DELETE_CONVERSATION_TOKEN:
4208                    mConversation.setMessageCount(0);
4209                    // fall through
4210                case DELETE_MESSAGE_TOKEN:
4211                    if (cookie instanceof Boolean && ((Boolean)cookie).booleanValue()) {
4212                        // If we just deleted the last message, reset the saved id.
4213                        mLastMessageId = 0;
4214                    }
4215                    // Update the notification for new messages since they
4216                    // may be deleted.
4217                    MessagingNotification.nonBlockingUpdateNewMessageIndicator(
4218                            ComposeMessageActivity.this, MessagingNotification.THREAD_NONE, false);
4219                    // Update the notification for failed messages since they
4220                    // may be deleted.
4221                    updateSendFailedNotification();
4222                    break;
4223            }
4224            // If we're deleting the whole conversation, throw away
4225            // our current working message and bail.
4226            if (token == ConversationList.DELETE_CONVERSATION_TOKEN) {
4227                ContactList recipients = mConversation.getRecipients();
4228                mWorkingMessage.discard();
4229
4230                // Remove any recipients referenced by this single thread from the
4231                // contacts cache. It's possible for two or more threads to reference
4232                // the same contact. That's ok if we remove it. We'll recreate that contact
4233                // when we init all Conversations below.
4234                if (recipients != null) {
4235                    for (Contact contact : recipients) {
4236                        contact.removeFromCache();
4237                    }
4238                }
4239
4240                // Make sure the conversation cache reflects the threads in the DB.
4241                Conversation.init(ComposeMessageActivity.this);
4242                finish();
4243            } else if (token == DELETE_MESSAGE_TOKEN) {
4244                // Check to see if we just deleted the last message
4245                startMsgListQuery(MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN);
4246            }
4247
4248            MmsWidgetProvider.notifyDatasetChanged(getApplicationContext());
4249        }
4250    }
4251
4252    private void showSmileyDialog() {
4253        if (mSmileyDialog == null) {
4254            int[] icons = SmileyParser.DEFAULT_SMILEY_RES_IDS;
4255            String[] names = getResources().getStringArray(
4256                    SmileyParser.DEFAULT_SMILEY_NAMES);
4257            final String[] texts = getResources().getStringArray(
4258                    SmileyParser.DEFAULT_SMILEY_TEXTS);
4259
4260            final int N = names.length;
4261
4262            List<Map<String, ?>> entries = new ArrayList<Map<String, ?>>();
4263            for (int i = 0; i < N; i++) {
4264                // We might have different ASCII for the same icon, skip it if
4265                // the icon is already added.
4266                boolean added = false;
4267                for (int j = 0; j < i; j++) {
4268                    if (icons[i] == icons[j]) {
4269                        added = true;
4270                        break;
4271                    }
4272                }
4273                if (!added) {
4274                    HashMap<String, Object> entry = new HashMap<String, Object>();
4275
4276                    entry. put("icon", icons[i]);
4277                    entry. put("name", names[i]);
4278                    entry.put("text", texts[i]);
4279
4280                    entries.add(entry);
4281                }
4282            }
4283
4284            final SimpleAdapter a = new SimpleAdapter(
4285                    this,
4286                    entries,
4287                    R.layout.smiley_menu_item,
4288                    new String[] {"icon", "name", "text"},
4289                    new int[] {R.id.smiley_icon, R.id.smiley_name, R.id.smiley_text});
4290            SimpleAdapter.ViewBinder viewBinder = new SimpleAdapter.ViewBinder() {
4291                @Override
4292                public boolean setViewValue(View view, Object data, String textRepresentation) {
4293                    if (view instanceof ImageView) {
4294                        Drawable img = getResources().getDrawable((Integer)data);
4295                        ((ImageView)view).setImageDrawable(img);
4296                        return true;
4297                    }
4298                    return false;
4299                }
4300            };
4301            a.setViewBinder(viewBinder);
4302
4303            AlertDialog.Builder b = new AlertDialog.Builder(this);
4304
4305            b.setTitle(getString(R.string.menu_insert_smiley));
4306
4307            b.setCancelable(true);
4308            b.setAdapter(a, new DialogInterface.OnClickListener() {
4309                @Override
4310                @SuppressWarnings("unchecked")
4311                public final void onClick(DialogInterface dialog, int which) {
4312                    HashMap<String, Object> item = (HashMap<String, Object>) a.getItem(which);
4313
4314                    String smiley = (String)item.get("text");
4315                    if (mSubjectTextEditor != null && mSubjectTextEditor.hasFocus()) {
4316                        mSubjectTextEditor.append(smiley);
4317                    } else {
4318                        mTextEditor.append(smiley);
4319                    }
4320
4321                    dialog.dismiss();
4322                }
4323            });
4324
4325            mSmileyDialog = b.create();
4326        }
4327
4328        mSmileyDialog.show();
4329    }
4330
4331    @Override
4332    public void onUpdate(final Contact updated) {
4333        // Using an existing handler for the post, rather than conjuring up a new one.
4334        mMessageListItemHandler.post(new Runnable() {
4335            @Override
4336            public void run() {
4337                ContactList recipients = isRecipientsEditorVisible() ?
4338                        mRecipientsEditor.constructContactsFromInput(false) : getRecipients();
4339                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
4340                    log("[CMA] onUpdate contact updated: " + updated);
4341                    log("[CMA] onUpdate recipients: " + recipients);
4342                }
4343                updateTitle(recipients);
4344
4345                // The contact information for one (or more) of the recipients has changed.
4346                // Rebuild the message list so each MessageItem will get the last contact info.
4347                ComposeMessageActivity.this.mMsgListAdapter.notifyDataSetChanged();
4348
4349                // Don't do this anymore. When we're showing chips, we don't want to switch from
4350                // chips to text.
4351//                if (mRecipientsEditor != null) {
4352//                    mRecipientsEditor.populate(recipients);
4353//                }
4354            }
4355        });
4356    }
4357
4358    private void addRecipientsListeners() {
4359        Contact.addListener(this);
4360    }
4361
4362    private void removeRecipientsListeners() {
4363        Contact.removeListener(this);
4364    }
4365
4366    public static Intent createIntent(Context context, long threadId) {
4367        Intent intent = new Intent(context, ComposeMessageActivity.class);
4368
4369        if (threadId > 0) {
4370            intent.setData(Conversation.getUri(threadId));
4371        }
4372
4373        return intent;
4374    }
4375
4376    private String getBody(Uri uri) {
4377        if (uri == null) {
4378            return null;
4379        }
4380        String urlStr = uri.getSchemeSpecificPart();
4381        if (!urlStr.contains("?")) {
4382            return null;
4383        }
4384        urlStr = urlStr.substring(urlStr.indexOf('?') + 1);
4385        String[] params = urlStr.split("&");
4386        for (String p : params) {
4387            if (p.startsWith("body=")) {
4388                try {
4389                    return URLDecoder.decode(p.substring(5), "UTF-8");
4390                } catch (UnsupportedEncodingException e) { }
4391            }
4392        }
4393        return null;
4394    }
4395
4396    private void updateThreadIdIfRunning() {
4397        if (mIsRunning && mConversation != null) {
4398            if (DEBUG) {
4399                Log.v(TAG, "updateThreadIdIfRunning: threadId: " +
4400                        mConversation.getThreadId());
4401            }
4402            MessagingNotification.setCurrentlyDisplayedThreadId(mConversation.getThreadId());
4403        } else {
4404            if (DEBUG) {
4405                Log.v(TAG, "updateThreadIdIfRunning: mIsRunning: " + mIsRunning +
4406                        " mConversation: " + mConversation);
4407            }
4408        }
4409        // If we're not running, but resume later, the current thread ID will be set in onResume()
4410    }
4411}
4412