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