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