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