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