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