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