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