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