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