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