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