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