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