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