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