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