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