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