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