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