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