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