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