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