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