ComposeMessageActivity.java revision 24f3447b5fc91b26864859caa33502b937768fdf
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_MMS_LOCKED;
27import static com.android.mms.ui.MessageListAdapter.COLUMN_MSG_TYPE;
28import static com.android.mms.ui.MessageListAdapter.PROJECTION;
29
30import java.io.File;
31import java.io.FileInputStream;
32import java.io.FileOutputStream;
33import java.io.IOException;
34import java.io.InputStream;
35import java.io.UnsupportedEncodingException;
36import java.net.URLDecoder;
37import java.util.ArrayList;
38import java.util.HashMap;
39import java.util.HashSet;
40import java.util.List;
41import java.util.Map;
42import java.util.regex.Pattern;
43
44import android.app.ActionBar;
45import android.app.Activity;
46import android.app.AlertDialog;
47import android.app.ProgressDialog;
48import android.content.ActivityNotFoundException;
49import android.content.AsyncQueryHandler;
50import android.content.BroadcastReceiver;
51import android.content.ContentResolver;
52import android.content.ContentUris;
53import android.content.ContentValues;
54import android.content.Context;
55import android.content.DialogInterface;
56import android.content.Intent;
57import android.content.IntentFilter;
58import android.content.DialogInterface.OnClickListener;
59import android.content.res.Configuration;
60import android.content.res.Resources;
61import android.database.Cursor;
62import android.database.sqlite.SQLiteException;
63import android.database.sqlite.SqliteWrapper;
64import android.drm.mobile1.DrmException;
65import android.drm.mobile1.DrmRawContent;
66import android.graphics.drawable.Drawable;
67import android.media.RingtoneManager;
68import android.net.Uri;
69import android.os.Bundle;
70import android.os.Environment;
71import android.os.Handler;
72import android.os.Message;
73import android.os.Parcelable;
74import android.os.SystemProperties;
75import android.provider.ContactsContract;
76import android.provider.ContactsContract.CommonDataKinds.Email;
77import android.provider.ContactsContract.Contacts;
78import android.provider.DrmStore;
79import android.provider.MediaStore;
80import android.provider.Settings;
81import android.provider.ContactsContract.Intents;
82import android.provider.MediaStore.Images;
83import android.provider.MediaStore.Video;
84import android.provider.Telephony.Mms;
85import android.provider.Telephony.Sms;
86import android.provider.ContactsContract.CommonDataKinds.Phone;
87import android.telephony.PhoneNumberUtils;
88import android.telephony.SmsMessage;
89import android.text.ClipboardManager;
90import android.text.Editable;
91import android.text.InputFilter;
92import android.text.SpannableString;
93import android.text.Spanned;
94import android.text.TextUtils;
95import android.text.TextWatcher;
96import android.text.method.TextKeyListener;
97import android.text.style.URLSpan;
98import android.text.util.Linkify;
99import android.util.Log;
100import android.view.ContextMenu;
101import android.view.KeyEvent;
102import android.view.LayoutInflater;
103import android.view.Menu;
104import android.view.MenuItem;
105import android.view.View;
106import android.view.ViewStub;
107import android.view.WindowManager;
108import android.view.ContextMenu.ContextMenuInfo;
109import android.view.View.OnCreateContextMenuListener;
110import android.view.View.OnKeyListener;
111import android.view.inputmethod.InputMethodManager;
112import android.webkit.MimeTypeMap;
113import android.widget.AdapterView;
114import android.widget.EditText;
115import android.widget.ImageButton;
116import android.widget.ImageView;
117import android.widget.LinearLayout;
118import android.widget.ListView;
119import android.widget.SimpleAdapter;
120import android.widget.CursorAdapter;
121import android.widget.TextView;
122import android.widget.Toast;
123
124import com.android.internal.telephony.TelephonyIntents;
125import com.android.internal.telephony.TelephonyProperties;
126import com.android.mms.LogTag;
127import com.android.mms.MmsApp;
128import com.android.mms.MmsConfig;
129import com.android.mms.R;
130import com.android.mms.TempFileProvider;
131import com.android.mms.data.Contact;
132import com.android.mms.data.ContactList;
133import com.android.mms.data.Conversation;
134import com.android.mms.data.WorkingMessage;
135import com.android.mms.data.WorkingMessage.MessageStatusListener;
136import com.google.android.mms.ContentType;
137import com.google.android.mms.pdu.EncodedStringValue;
138import com.google.android.mms.MmsException;
139import com.google.android.mms.pdu.PduBody;
140import com.google.android.mms.pdu.PduPart;
141import com.google.android.mms.pdu.PduPersister;
142import com.google.android.mms.pdu.SendReq;
143import com.android.mms.model.SlideModel;
144import com.android.mms.model.SlideshowModel;
145import com.android.mms.transaction.MessagingNotification;
146import com.android.mms.ui.MessageUtils.ResizeImageResultCallback;
147import com.android.mms.ui.RecipientsEditor.RecipientContextMenuInfo;
148import com.android.mms.util.SendingProgressTokenManager;
149import com.android.mms.util.SmileyParser;
150
151import android.text.InputFilter.LengthFilter;
152
153/**
154 * This is the main UI for:
155 * 1. Composing a new message;
156 * 2. Viewing/managing message history of a conversation.
157 *
158 * This activity can handle following parameters from the intent
159 * by which it's launched.
160 * thread_id long Identify the conversation to be viewed. When creating a
161 *         new message, this parameter shouldn't be present.
162 * msg_uri Uri The message which should be opened for editing in the editor.
163 * address String The addresses of the recipients in current conversation.
164 * exit_on_sent boolean Exit this activity after the message is sent.
165 */
166public class ComposeMessageActivity extends Activity
167        implements View.OnClickListener, TextView.OnEditorActionListener,
168        MessageStatusListener, Contact.UpdateListener {
169    public static final int REQUEST_CODE_ATTACH_IMAGE     = 10;
170    public static final int REQUEST_CODE_TAKE_PICTURE     = 11;
171    public static final int REQUEST_CODE_ATTACH_VIDEO     = 12;
172    public static final int REQUEST_CODE_TAKE_VIDEO       = 13;
173    public static final int REQUEST_CODE_ATTACH_SOUND     = 14;
174    public static final int REQUEST_CODE_RECORD_SOUND     = 15;
175    public static final int REQUEST_CODE_CREATE_SLIDESHOW = 16;
176    public static final int REQUEST_CODE_ECM_EXIT_DIALOG  = 17;
177    public static final int REQUEST_CODE_ADD_CONTACT      = 18;
178    public static final int REQUEST_CODE_PICK             = 19;
179
180    private static final String TAG = "Mms/compose";
181
182    private static final boolean DEBUG = false;
183    private static final boolean TRACE = false;
184    private static final boolean LOCAL_LOGV = false;
185
186    // Menu ID
187    private static final int MENU_ADD_SUBJECT           = 0;
188    private static final int MENU_DELETE_THREAD         = 1;
189    private static final int MENU_ADD_ATTACHMENT        = 2;
190    private static final int MENU_DISCARD               = 3;
191    private static final int MENU_SEND                  = 4;
192    private static final int MENU_CALL_RECIPIENT        = 5;
193    private static final int MENU_CONVERSATION_LIST     = 6;
194    private static final int MENU_DEBUG_DUMP            = 7;
195
196    // Context menu ID
197    private static final int MENU_VIEW_CONTACT          = 12;
198    private static final int MENU_ADD_TO_CONTACTS       = 13;
199
200    private static final int MENU_EDIT_MESSAGE          = 14;
201    private static final int MENU_VIEW_SLIDESHOW        = 16;
202    private static final int MENU_VIEW_MESSAGE_DETAILS  = 17;
203    private static final int MENU_DELETE_MESSAGE        = 18;
204    private static final int MENU_SEARCH                = 19;
205    private static final int MENU_DELIVERY_REPORT       = 20;
206    private static final int MENU_FORWARD_MESSAGE       = 21;
207    private static final int MENU_CALL_BACK             = 22;
208    private static final int MENU_SEND_EMAIL            = 23;
209    private static final int MENU_COPY_MESSAGE_TEXT     = 24;
210    private static final int MENU_COPY_TO_SDCARD        = 25;
211    private static final int MENU_INSERT_SMILEY         = 26;
212    private static final int MENU_ADD_ADDRESS_TO_CONTACTS = 27;
213    private static final int MENU_LOCK_MESSAGE          = 28;
214    private static final int MENU_UNLOCK_MESSAGE        = 29;
215    private static final int MENU_COPY_TO_DRM_PROVIDER  = 30;
216    private static final int MENU_PREFERENCES           = 31;
217
218    private static final int RECIPIENTS_MAX_LENGTH = 312;
219
220    private static final int MESSAGE_LIST_QUERY_TOKEN = 9527;
221
222    private static final int DELETE_MESSAGE_TOKEN  = 9700;
223
224    private static final int CHARS_REMAINING_BEFORE_COUNTER_SHOWN = 10;
225
226    private static final long NO_DATE_FOR_DIALOG = -1L;
227
228    private static final String EXIT_ECM_RESULT = "exit_ecm_result";
229
230    private ContentResolver mContentResolver;
231
232    private BackgroundQueryHandler mBackgroundQueryHandler;
233
234    private Conversation mConversation;     // Conversation we are working in
235
236    private boolean mExitOnSent;            // Should we finish() after sending a message?
237                                            // TODO: mExitOnSent is obsolete -- remove
238
239    private View mTopPanel;                 // View containing the recipient and subject editors
240    private View mBottomPanel;              // View containing the text editor, send button, ec.
241    private EditText mTextEditor;           // Text editor to type your message into
242    private TextView mTextCounter;          // Shows the number of characters used in text editor
243    private TextView mSendButtonMms;        // Press to send mms
244    private ImageButton mSendButtonSms;     // Press to send sms
245    private EditText mSubjectTextEditor;    // Text editor for MMS subject
246
247    private AttachmentEditor mAttachmentEditor;
248    private View mAttachmentEditorScrollView;
249
250    private MessageListView mMsgListView;        // ListView for messages in this conversation
251    public MessageListAdapter mMsgListAdapter;  // and its corresponding ListAdapter
252
253    private RecipientsEditor mRecipientsEditor;  // UI control for editing recipients
254    private ImageButton mRecipientsPicker;       // UI control for recipients picker
255
256    private boolean mIsKeyboardOpen;             // Whether the hardware keyboard is visible
257    private boolean mIsLandscape;                // Whether we're in landscape mode
258
259    private boolean mPossiblePendingNotification;   // If the message list has changed, we may have
260                                                    // a pending notification to deal with.
261
262    private boolean mToastForDraftSave;   // Whether to notify the user that a draft is being saved
263
264    private boolean mSentMessage;       // true if the user has sent a message while in this
265                                        // activity. On a new compose message case, when the first
266                                        // message is sent is a MMS w/ attachment, the list blanks
267                                        // for a second before showing the sent message. But we'd
268                                        // think the message list is empty, thus show the recipients
269                                        // editor thinking it's a draft message. This flag should
270                                        // help clarify the situation.
271
272    private WorkingMessage mWorkingMessage;         // The message currently being composed.
273
274    private AlertDialog mSmileyDialog;
275
276    private boolean mWaitingForSubActivity;
277    private int mLastRecipientCount;            // Used for warning the user on too many recipients.
278    private AttachmentTypeSelectorAdapter mAttachmentTypeSelectorAdapter;
279
280    private boolean mSendingMessage;    // Indicates the current message is sending, and shouldn't send again.
281
282    private Intent mAddContactIntent;   // Intent used to add a new contact
283
284    private String mDebugRecipients;
285
286    @SuppressWarnings("unused")
287    public static void log(String logMsg) {
288        Thread current = Thread.currentThread();
289        long tid = current.getId();
290        StackTraceElement[] stack = current.getStackTrace();
291        String methodName = stack[3].getMethodName();
292        // Prepend current thread ID and name of calling method to the message.
293        logMsg = "[" + tid + "] [" + methodName + "] " + logMsg;
294        Log.d(TAG, logMsg);
295    }
296
297    //==========================================================
298    // Inner classes
299    //==========================================================
300
301    private void editSlideshow() {
302        Uri dataUri = mWorkingMessage.saveAsMms(false);
303        if (dataUri == null) {
304            return;
305        }
306        Intent intent = new Intent(this, SlideshowEditActivity.class);
307        intent.setData(dataUri);
308        startActivityForResult(intent, REQUEST_CODE_CREATE_SLIDESHOW);
309    }
310
311    private final Handler mAttachmentEditorHandler = new Handler() {
312        @Override
313        public void handleMessage(Message msg) {
314            switch (msg.what) {
315                case AttachmentEditor.MSG_EDIT_SLIDESHOW: {
316                    editSlideshow();
317                    break;
318                }
319                case AttachmentEditor.MSG_SEND_SLIDESHOW: {
320                    if (isPreparedForSending()) {
321                        ComposeMessageActivity.this.confirmSendMessageIfNeeded();
322                    }
323                    break;
324                }
325                case AttachmentEditor.MSG_VIEW_IMAGE:
326                case AttachmentEditor.MSG_PLAY_VIDEO:
327                case AttachmentEditor.MSG_PLAY_AUDIO:
328                case AttachmentEditor.MSG_PLAY_SLIDESHOW:
329                    MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this,
330                            mWorkingMessage);
331                    break;
332
333                case AttachmentEditor.MSG_REPLACE_IMAGE:
334                case AttachmentEditor.MSG_REPLACE_VIDEO:
335                case AttachmentEditor.MSG_REPLACE_AUDIO:
336                    showAddAttachmentDialog(true);
337                    break;
338
339                case AttachmentEditor.MSG_REMOVE_ATTACHMENT:
340                    mWorkingMessage.removeAttachment(true);
341                    break;
342
343                default:
344                    break;
345            }
346        }
347    };
348
349    private final Handler mMessageListItemHandler = new Handler() {
350        @Override
351        public void handleMessage(Message msg) {
352            String type;
353            switch (msg.what) {
354                case MessageListItem.MSG_LIST_EDIT_MMS:
355                    type = "mms";
356                    break;
357                case MessageListItem.MSG_LIST_EDIT_SMS:
358                    type = "sms";
359                    break;
360                default:
361                    Log.w(TAG, "Unknown message: " + msg.what);
362                    return;
363            }
364
365            MessageItem msgItem = getMessageItem(type, (Long) msg.obj, false);
366            if (msgItem != null) {
367                editMessageItem(msgItem);
368                drawBottomPanel();
369            }
370        }
371    };
372
373    private final OnKeyListener mSubjectKeyListener = new OnKeyListener() {
374        public boolean onKey(View v, int keyCode, KeyEvent event) {
375            if (event.getAction() != KeyEvent.ACTION_DOWN) {
376                return false;
377            }
378
379            // When the subject editor is empty, press "DEL" to hide the input field.
380            if ((keyCode == KeyEvent.KEYCODE_DEL) && (mSubjectTextEditor.length() == 0)) {
381                showSubjectEditor(false);
382                mWorkingMessage.setSubject(null, true);
383                return true;
384            }
385
386            return false;
387        }
388    };
389
390    /**
391     * Return the messageItem associated with the type ("mms" or "sms") and message id.
392     * @param type Type of the message: "mms" or "sms"
393     * @param msgId Message id of the message. This is the _id of the sms or pdu row and is
394     * stored in the MessageItem
395     * @param createFromCursorIfNotInCache true if the item is not found in the MessageListAdapter's
396     * cache and the code can create a new MessageItem based on the position of the current cursor.
397     * If false, the function returns null if the MessageItem isn't in the cache.
398     * @return MessageItem or null if not found and createFromCursorIfNotInCache is false
399     */
400    private MessageItem getMessageItem(String type, long msgId,
401            boolean createFromCursorIfNotInCache) {
402        return mMsgListAdapter.getCachedMessageItem(type, msgId,
403                createFromCursorIfNotInCache ? mMsgListAdapter.getCursor() : null);
404    }
405
406    private boolean isCursorValid() {
407        // Check whether the cursor is valid or not.
408        Cursor cursor = mMsgListAdapter.getCursor();
409        if (cursor.isClosed() || cursor.isBeforeFirst() || cursor.isAfterLast()) {
410            Log.e(TAG, "Bad cursor.", new RuntimeException());
411            return false;
412        }
413        return true;
414    }
415
416    private void resetCounter() {
417        mTextCounter.setText("");
418        mTextCounter.setVisibility(View.GONE);
419    }
420
421    private void updateCounter(CharSequence text, int start, int before, int count) {
422        WorkingMessage workingMessage = mWorkingMessage;
423        if (workingMessage.requiresMms()) {
424            // If we're not removing text (i.e. no chance of converting back to SMS
425            // because of this change) and we're in MMS mode, just bail out since we
426            // then won't have to calculate the length unnecessarily.
427            final boolean textRemoved = (before > count);
428            if (!textRemoved) {
429                showSmsOrMmsSendButton(workingMessage.requiresMms());
430                return;
431            }
432        }
433
434        int[] params = SmsMessage.calculateLength(text, false);
435            /* SmsMessage.calculateLength returns an int[4] with:
436             *   int[0] being the number of SMS's required,
437             *   int[1] the number of code units used,
438             *   int[2] is the number of code units remaining until the next message.
439             *   int[3] is the encoding type that should be used for the message.
440             */
441        int msgCount = params[0];
442        int remainingInCurrentMessage = params[2];
443
444        if (!MmsConfig.getMultipartSmsEnabled()) {
445            mWorkingMessage.setLengthRequiresMms(
446                    msgCount >= MmsConfig.getSmsToMmsTextThreshold(), true);
447        }
448
449        // Show the counter only if:
450        // - We are not in MMS mode
451        // - We are going to send more than one message OR we are getting close
452        boolean showCounter = false;
453        if (!workingMessage.requiresMms() &&
454                (msgCount > 1 ||
455                 remainingInCurrentMessage <= CHARS_REMAINING_BEFORE_COUNTER_SHOWN)) {
456            showCounter = true;
457        }
458
459        showSmsOrMmsSendButton(workingMessage.requiresMms());
460
461        if (showCounter) {
462            // Update the remaining characters and number of messages required.
463            String counterText = msgCount > 1 ? remainingInCurrentMessage + " / " + msgCount
464                    : String.valueOf(remainingInCurrentMessage);
465            mTextCounter.setText(counterText);
466            mTextCounter.setVisibility(View.VISIBLE);
467        } else {
468            mTextCounter.setVisibility(View.GONE);
469        }
470    }
471
472    @Override
473    public void startActivityForResult(Intent intent, int requestCode)
474    {
475        // requestCode >= 0 means the activity in question is a sub-activity.
476        if (requestCode >= 0) {
477            mWaitingForSubActivity = true;
478        }
479
480        super.startActivityForResult(intent, requestCode);
481    }
482
483    private void toastConvertInfo(boolean toMms) {
484        final int resId = toMms ? R.string.converting_to_picture_message
485                : R.string.converting_to_text_message;
486        Toast.makeText(this, resId, Toast.LENGTH_SHORT).show();
487    }
488
489    private class DeleteMessageListener implements OnClickListener {
490        private final Uri mDeleteUri;
491        private final boolean mDeleteLocked;
492
493        public DeleteMessageListener(Uri uri, boolean deleteLocked) {
494            mDeleteUri = uri;
495            mDeleteLocked = deleteLocked;
496        }
497
498        public DeleteMessageListener(long msgId, String type, boolean deleteLocked) {
499            if ("mms".equals(type)) {
500                mDeleteUri = ContentUris.withAppendedId(Mms.CONTENT_URI, msgId);
501            } else {
502                mDeleteUri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgId);
503            }
504            mDeleteLocked = deleteLocked;
505        }
506
507        public void onClick(DialogInterface dialog, int whichButton) {
508            mBackgroundQueryHandler.startDelete(DELETE_MESSAGE_TOKEN,
509                    null, mDeleteUri, mDeleteLocked ? null : "locked=0", null);
510            dialog.dismiss();
511        }
512    }
513
514    private class DiscardDraftListener implements OnClickListener {
515        public void onClick(DialogInterface dialog, int whichButton) {
516            mWorkingMessage.discard();
517            dialog.dismiss();
518            finish();
519        }
520    }
521
522    private class SendIgnoreInvalidRecipientListener implements OnClickListener {
523        public void onClick(DialogInterface dialog, int whichButton) {
524            sendMessage(true);
525            dialog.dismiss();
526        }
527    }
528
529    private class CancelSendingListener implements OnClickListener {
530        public void onClick(DialogInterface dialog, int whichButton) {
531            if (isRecipientsEditorVisible()) {
532                mRecipientsEditor.requestFocus();
533            }
534            dialog.dismiss();
535        }
536    }
537
538    private void confirmSendMessageIfNeeded() {
539        if (!isRecipientsEditorVisible()) {
540            sendMessage(true);
541            return;
542        }
543
544        boolean isMms = mWorkingMessage.requiresMms();
545        if (mRecipientsEditor.hasInvalidRecipient(isMms)) {
546            if (mRecipientsEditor.hasValidRecipient(isMms)) {
547                String title = getResourcesString(R.string.has_invalid_recipient,
548                        mRecipientsEditor.formatInvalidNumbers(isMms));
549                new AlertDialog.Builder(this)
550                    .setIcon(android.R.drawable.ic_dialog_alert)
551                    .setTitle(title)
552                    .setMessage(R.string.invalid_recipient_message)
553                    .setPositiveButton(R.string.try_to_send,
554                            new SendIgnoreInvalidRecipientListener())
555                    .setNegativeButton(R.string.no, new CancelSendingListener())
556                    .show();
557            } else {
558                new AlertDialog.Builder(this)
559                    .setIcon(android.R.drawable.ic_dialog_alert)
560                    .setTitle(R.string.cannot_send_message)
561                    .setMessage(R.string.cannot_send_message_reason)
562                    .setPositiveButton(R.string.yes, new CancelSendingListener())
563                    .show();
564            }
565        } else {
566            sendMessage(true);
567        }
568    }
569
570    private final TextWatcher mRecipientsWatcher = new TextWatcher() {
571        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
572        }
573
574        public void onTextChanged(CharSequence s, int start, int before, int count) {
575            // This is a workaround for bug 1609057.  Since onUserInteraction() is
576            // not called when the user touches the soft keyboard, we pretend it was
577            // called when textfields changes.  This should be removed when the bug
578            // is fixed.
579            onUserInteraction();
580        }
581
582        public void afterTextChanged(Editable s) {
583            // Bug 1474782 describes a situation in which we send to
584            // the wrong recipient.  We have been unable to reproduce this,
585            // but the best theory we have so far is that the contents of
586            // mRecipientList somehow become stale when entering
587            // ComposeMessageActivity via onNewIntent().  This assertion is
588            // meant to catch one possible path to that, of a non-visible
589            // mRecipientsEditor having its TextWatcher fire and refreshing
590            // mRecipientList with its stale contents.
591            if (!isRecipientsEditorVisible()) {
592                IllegalStateException e = new IllegalStateException(
593                        "afterTextChanged called with invisible mRecipientsEditor");
594                // Make sure the crash is uploaded to the service so we
595                // can see if this is happening in the field.
596                Log.w(TAG,
597                     "RecipientsWatcher: afterTextChanged called with invisible mRecipientsEditor");
598                return;
599            }
600
601            mWorkingMessage.setWorkingRecipients(mRecipientsEditor.getNumbers());
602            mWorkingMessage.setHasEmail(mRecipientsEditor.containsEmail(), true);
603
604            checkForTooManyRecipients();
605
606            // Walk backwards in the text box, skipping spaces.  If the last
607            // character is a comma, update the title bar.
608            for (int pos = s.length() - 1; pos >= 0; pos--) {
609                char c = s.charAt(pos);
610                if (c == ' ')
611                    continue;
612
613                if (c == ',') {
614                    updateTitle(mConversation.getRecipients());
615                }
616
617                break;
618            }
619
620            // If we have gone to zero recipients, disable send button.
621            updateSendButtonState();
622        }
623    };
624
625    private void checkForTooManyRecipients() {
626        final int recipientLimit = MmsConfig.getRecipientLimit();
627        if (recipientLimit != Integer.MAX_VALUE) {
628            final int recipientCount = recipientCount();
629            boolean tooMany = recipientCount > recipientLimit;
630
631            if (recipientCount != mLastRecipientCount) {
632                // Don't warn the user on every character they type when they're over the limit,
633                // only when the actual # of recipients changes.
634                mLastRecipientCount = recipientCount;
635                if (tooMany) {
636                    String tooManyMsg = getString(R.string.too_many_recipients, recipientCount,
637                            recipientLimit);
638                    Toast.makeText(ComposeMessageActivity.this,
639                            tooManyMsg, Toast.LENGTH_LONG).show();
640                }
641            }
642        }
643    }
644
645    private final OnCreateContextMenuListener mRecipientsMenuCreateListener =
646        new OnCreateContextMenuListener() {
647        public void onCreateContextMenu(ContextMenu menu, View v,
648                ContextMenuInfo menuInfo) {
649            if (menuInfo != null) {
650                Contact c = ((RecipientContextMenuInfo) menuInfo).recipient;
651                RecipientsMenuClickListener l = new RecipientsMenuClickListener(c);
652
653                menu.setHeaderTitle(c.getName());
654
655                if (c.existsInDatabase()) {
656                    menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact)
657                            .setOnMenuItemClickListener(l);
658                } else if (canAddToContacts(c)){
659                    menu.add(0, MENU_ADD_TO_CONTACTS, 0, R.string.menu_add_to_contacts)
660                            .setOnMenuItemClickListener(l);
661                }
662            }
663        }
664    };
665
666    private final class RecipientsMenuClickListener implements MenuItem.OnMenuItemClickListener {
667        private final Contact mRecipient;
668
669        RecipientsMenuClickListener(Contact recipient) {
670            mRecipient = recipient;
671        }
672
673        public boolean onMenuItemClick(MenuItem item) {
674            switch (item.getItemId()) {
675                // Context menu handlers for the recipients editor.
676                case MENU_VIEW_CONTACT: {
677                    Uri contactUri = mRecipient.getUri();
678                    Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
679                    intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
680                    startActivity(intent);
681                    return true;
682                }
683                case MENU_ADD_TO_CONTACTS: {
684                    mAddContactIntent = ConversationList.createAddContactIntent(
685                            mRecipient.getNumber());
686                    ComposeMessageActivity.this.startActivityForResult(mAddContactIntent,
687                            REQUEST_CODE_ADD_CONTACT);
688                    return true;
689                }
690            }
691            return false;
692        }
693    }
694
695    private boolean canAddToContacts(Contact contact) {
696        // There are some kind of automated messages, like STK messages, that we don't want
697        // to add to contacts. These names begin with special characters, like, "*Info".
698        final String name = contact.getName();
699        if (!TextUtils.isEmpty(contact.getNumber())) {
700            char c = contact.getNumber().charAt(0);
701            if (isSpecialChar(c)) {
702                return false;
703            }
704        }
705        if (!TextUtils.isEmpty(name)) {
706            char c = name.charAt(0);
707            if (isSpecialChar(c)) {
708                return false;
709            }
710        }
711        if (!(Mms.isEmailAddress(name) || Mms.isPhoneNumber(name) || contact.isMe())) {
712            return false;
713        }
714        return true;
715    }
716
717    private boolean isSpecialChar(char c) {
718        return c == '*' || c == '%' || c == '$';
719    }
720
721    private void addPositionBasedMenuItems(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
722        AdapterView.AdapterContextMenuInfo info;
723
724        try {
725            info = (AdapterView.AdapterContextMenuInfo) menuInfo;
726        } catch (ClassCastException e) {
727            Log.e(TAG, "bad menuInfo");
728            return;
729        }
730        final int position = info.position;
731
732        addUriSpecificMenuItems(menu, v, position);
733    }
734
735    private Uri getSelectedUriFromMessageList(ListView listView, int position) {
736        // If the context menu was opened over a uri, get that uri.
737        MessageListItem msglistItem = (MessageListItem) listView.getChildAt(position);
738        if (msglistItem == null) {
739            // FIXME: Should get the correct view. No such interface in ListView currently
740            // to get the view by position. The ListView.getChildAt(position) cannot
741            // get correct view since the list doesn't create one child for each item.
742            // And if setSelection(position) then getSelectedView(),
743            // cannot get corrent view when in touch mode.
744            return null;
745        }
746
747        TextView textView;
748        CharSequence text = null;
749        int selStart = -1;
750        int selEnd = -1;
751
752        //check if message sender is selected
753        textView = (TextView) msglistItem.findViewById(R.id.text_view);
754        if (textView != null) {
755            text = textView.getText();
756            selStart = textView.getSelectionStart();
757            selEnd = textView.getSelectionEnd();
758        }
759
760        if (selStart == -1) {
761            //sender is not being selected, it may be within the message body
762            textView = (TextView) msglistItem.findViewById(R.id.body_text_view);
763            if (textView != null) {
764                text = textView.getText();
765                selStart = textView.getSelectionStart();
766                selEnd = textView.getSelectionEnd();
767            }
768        }
769
770        // Check that some text is actually selected, rather than the cursor
771        // just being placed within the TextView.
772        if (selStart != selEnd) {
773            int min = Math.min(selStart, selEnd);
774            int max = Math.max(selStart, selEnd);
775
776            URLSpan[] urls = ((Spanned) text).getSpans(min, max,
777                                                        URLSpan.class);
778
779            if (urls.length == 1) {
780                return Uri.parse(urls[0].getURL());
781            }
782        }
783
784        //no uri was selected
785        return null;
786    }
787
788    private void addUriSpecificMenuItems(ContextMenu menu, View v, int position) {
789        Uri uri = getSelectedUriFromMessageList((ListView) v, position);
790
791        if (uri != null) {
792            Intent intent = new Intent(null, uri);
793            intent.addCategory(Intent.CATEGORY_SELECTED_ALTERNATIVE);
794            menu.addIntentOptions(0, 0, 0,
795                    new android.content.ComponentName(this, ComposeMessageActivity.class),
796                    null, intent, 0, null);
797        }
798    }
799
800    private final void addCallAndContactMenuItems(
801            ContextMenu menu, MsgListMenuClickListener l, MessageItem msgItem) {
802        SpannableString msg = new SpannableString(msgItem.mBody);
803        Linkify.addLinks(msg, Linkify.ALL);
804        ArrayList<String> uris =
805            MessageUtils.extractUris(msg.getSpans(0, msg.length(), URLSpan.class));
806
807        // Remove any dupes so they don't get added to the menu multiple times
808        HashSet<String> collapsedUris = new HashSet<String>();
809        for (String uri : uris) {
810            collapsedUris.add(uri.toLowerCase());
811        }
812        for (String uriString : collapsedUris) {
813            String prefix = null;
814            int sep = uriString.indexOf(":");
815            if (sep >= 0) {
816                prefix = uriString.substring(0, sep);
817                uriString = uriString.substring(sep + 1);
818            }
819            Uri contactUri = null;
820            boolean knownPrefix = true;
821            if ("mailto".equalsIgnoreCase(prefix))  {
822                contactUri = getContactUriForEmail(uriString);
823            } else if ("tel".equalsIgnoreCase(prefix)) {
824                contactUri = getContactUriForPhoneNumber(uriString);
825            } else {
826                knownPrefix = false;
827            }
828            if (knownPrefix && contactUri == null) {
829                Intent intent = ConversationList.createAddContactIntent(uriString);
830
831                String addContactString = getString(R.string.menu_add_address_to_contacts,
832                        uriString);
833                menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, addContactString)
834                    .setOnMenuItemClickListener(l)
835                    .setIntent(intent);
836            }
837        }
838    }
839
840    private Uri getContactUriForEmail(String emailAddress) {
841        Cursor cursor = SqliteWrapper.query(this, getContentResolver(),
842                Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(emailAddress)),
843                new String[] { Email.CONTACT_ID, Contacts.DISPLAY_NAME }, null, null, null);
844
845        if (cursor != null) {
846            try {
847                while (cursor.moveToNext()) {
848                    String name = cursor.getString(1);
849                    if (!TextUtils.isEmpty(name)) {
850                        return ContentUris.withAppendedId(Contacts.CONTENT_URI, cursor.getLong(0));
851                    }
852                }
853            } finally {
854                cursor.close();
855            }
856        }
857        return null;
858    }
859
860    private Uri getContactUriForPhoneNumber(String phoneNumber) {
861        Contact contact = Contact.get(phoneNumber, false);
862        if (contact.existsInDatabase()) {
863            return contact.getUri();
864        }
865        return null;
866    }
867
868    private final OnCreateContextMenuListener mMsgListMenuCreateListener =
869        new OnCreateContextMenuListener() {
870        public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
871            if (!isCursorValid()) {
872                return;
873            }
874            Cursor cursor = mMsgListAdapter.getCursor();
875            String type = cursor.getString(COLUMN_MSG_TYPE);
876            long msgId = cursor.getLong(COLUMN_ID);
877
878            addPositionBasedMenuItems(menu, v, menuInfo);
879
880            MessageItem msgItem = mMsgListAdapter.getCachedMessageItem(type, msgId, cursor);
881            if (msgItem == null) {
882                Log.e(TAG, "Cannot load message item for type = " + type
883                        + ", msgId = " + msgId);
884                return;
885            }
886
887            menu.setHeaderTitle(R.string.message_options);
888
889            MsgListMenuClickListener l = new MsgListMenuClickListener();
890
891            // It is unclear what would make most sense for copying an MMS message
892            // to the clipboard, so we currently do SMS only.
893            if (msgItem.isSms()) {
894                // Message type is sms. Only allow "edit" if the message has a single recipient
895                if (getRecipients().size() == 1 &&
896                        (msgItem.mBoxId == Sms.MESSAGE_TYPE_OUTBOX ||
897                                msgItem.mBoxId == Sms.MESSAGE_TYPE_FAILED)) {
898                    menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit)
899                    .setOnMenuItemClickListener(l);
900                }
901
902                menu.add(0, MENU_COPY_MESSAGE_TEXT, 0, R.string.copy_message_text)
903                .setOnMenuItemClickListener(l);
904            }
905
906            addCallAndContactMenuItems(menu, l, msgItem);
907
908            // Forward is not available for undownloaded messages.
909            if (msgItem.isDownloaded()) {
910                menu.add(0, MENU_FORWARD_MESSAGE, 0, R.string.menu_forward)
911                        .setOnMenuItemClickListener(l);
912            }
913
914            if (msgItem.isMms()) {
915                switch (msgItem.mBoxId) {
916                    case Mms.MESSAGE_BOX_INBOX:
917                        break;
918                    case Mms.MESSAGE_BOX_OUTBOX:
919                        // Since we currently break outgoing messages to multiple
920                        // recipients into one message per recipient, only allow
921                        // editing a message for single-recipient conversations.
922                        if (getRecipients().size() == 1) {
923                            menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit)
924                                    .setOnMenuItemClickListener(l);
925                        }
926                        break;
927                }
928                switch (msgItem.mAttachmentType) {
929                    case WorkingMessage.TEXT:
930                        break;
931                    case WorkingMessage.VIDEO:
932                    case WorkingMessage.IMAGE:
933                        if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) {
934                            menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard)
935                            .setOnMenuItemClickListener(l);
936                        }
937                        break;
938                    case WorkingMessage.SLIDESHOW:
939                    default:
940                        menu.add(0, MENU_VIEW_SLIDESHOW, 0, R.string.view_slideshow)
941                        .setOnMenuItemClickListener(l);
942                        if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) {
943                            menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard)
944                            .setOnMenuItemClickListener(l);
945                        }
946                        if (haveSomethingToCopyToDrmProvider(msgItem.mMsgId)) {
947                            menu.add(0, MENU_COPY_TO_DRM_PROVIDER, 0,
948                                    getDrmMimeMenuStringRsrc(msgItem.mMsgId))
949                            .setOnMenuItemClickListener(l);
950                        }
951                        break;
952                }
953            }
954
955            if (msgItem.mLocked) {
956                menu.add(0, MENU_UNLOCK_MESSAGE, 0, R.string.menu_unlock)
957                    .setOnMenuItemClickListener(l);
958            } else {
959                menu.add(0, MENU_LOCK_MESSAGE, 0, R.string.menu_lock)
960                    .setOnMenuItemClickListener(l);
961            }
962
963            menu.add(0, MENU_VIEW_MESSAGE_DETAILS, 0, R.string.view_message_details)
964                .setOnMenuItemClickListener(l);
965
966            if (msgItem.mDeliveryStatus != MessageItem.DeliveryStatus.NONE || msgItem.mReadReport) {
967                menu.add(0, MENU_DELIVERY_REPORT, 0, R.string.view_delivery_report)
968                        .setOnMenuItemClickListener(l);
969            }
970
971            menu.add(0, MENU_DELETE_MESSAGE, 0, R.string.delete_message)
972                .setOnMenuItemClickListener(l);
973        }
974    };
975
976    private void editMessageItem(MessageItem msgItem) {
977        if ("sms".equals(msgItem.mType)) {
978            editSmsMessageItem(msgItem);
979        } else {
980            editMmsMessageItem(msgItem);
981        }
982        if (msgItem.isFailedMessage() && mMsgListAdapter.getCount() <= 1) {
983            // For messages with bad addresses, let the user re-edit the recipients.
984            initRecipientsEditor();
985        }
986    }
987
988    private void editSmsMessageItem(MessageItem msgItem) {
989        // When the message being edited is the only message in the conversation, the delete
990        // below does something subtle. The trigger "delete_obsolete_threads_pdu" sees that a
991        // thread contains no messages and silently deletes the thread. Meanwhile, the mConversation
992        // object still holds onto the old thread_id and code thinks there's a backing thread in
993        // the DB when it really has been deleted. Here we try and notice that situation and
994        // clear out the thread_id. Later on, when Conversation.ensureThreadId() is called, we'll
995        // create a new thread if necessary.
996        synchronized(mConversation) {
997            if (mConversation.getMessageCount() <= 1) {
998                mConversation.clearThreadId();
999            }
1000        }
1001        // Delete the old undelivered SMS and load its content.
1002        Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgItem.mMsgId);
1003        SqliteWrapper.delete(ComposeMessageActivity.this,
1004                mContentResolver, uri, null, null);
1005
1006        mWorkingMessage.setText(msgItem.mBody);
1007    }
1008
1009    private void editMmsMessageItem(MessageItem msgItem) {
1010        // Discard the current message in progress.
1011        mWorkingMessage.discard();
1012
1013        // Load the selected message in as the working message.
1014        mWorkingMessage = WorkingMessage.load(this, msgItem.mMessageUri);
1015        mWorkingMessage.setConversation(mConversation);
1016
1017        drawTopPanel(false);
1018
1019        // WorkingMessage.load() above only loads the slideshow. Set the
1020        // subject here because we already know what it is and avoid doing
1021        // another DB lookup in load() just to get it.
1022        mWorkingMessage.setSubject(msgItem.mSubject, false);
1023
1024        if (mWorkingMessage.hasSubject()) {
1025            showSubjectEditor(true);
1026        }
1027    }
1028
1029    private void copyToClipboard(String str) {
1030        ClipboardManager clip =
1031            (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
1032        clip.setText(str);
1033    }
1034
1035    private void forwardMessage(MessageItem msgItem) {
1036        Intent intent = createIntent(this, 0);
1037
1038        intent.putExtra("exit_on_sent", true);
1039        intent.putExtra("forwarded_message", true);
1040
1041        if (msgItem.mType.equals("sms")) {
1042            intent.putExtra("sms_body", msgItem.mBody);
1043        } else {
1044            SendReq sendReq = new SendReq();
1045            String subject = getString(R.string.forward_prefix);
1046            if (msgItem.mSubject != null) {
1047                subject += msgItem.mSubject;
1048            }
1049            sendReq.setSubject(new EncodedStringValue(subject));
1050            sendReq.setBody(msgItem.mSlideshow.makeCopy(
1051                    ComposeMessageActivity.this));
1052
1053            Uri uri = null;
1054            try {
1055                PduPersister persister = PduPersister.getPduPersister(this);
1056                // Copy the parts of the message here.
1057                uri = persister.persist(sendReq, Mms.Draft.CONTENT_URI);
1058            } catch (MmsException e) {
1059                Log.e(TAG, "Failed to copy message: " + msgItem.mMessageUri, e);
1060                Toast.makeText(ComposeMessageActivity.this,
1061                        R.string.cannot_save_message, Toast.LENGTH_SHORT).show();
1062                return;
1063            }
1064
1065            intent.putExtra("msg_uri", uri);
1066            intent.putExtra("subject", subject);
1067        }
1068        // ForwardMessageActivity is simply an alias in the manifest for ComposeMessageActivity.
1069        // We have to make an alias because ComposeMessageActivity launch flags specify
1070        // singleTop. When we forward a message, we want to start a separate ComposeMessageActivity.
1071        // The only way to do that is to override the singleTop flag, which is impossible to do
1072        // in code. By creating an alias to the activity, without the singleTop flag, we can
1073        // launch a separate ComposeMessageActivity to edit the forward message.
1074        intent.setClassName(this, "com.android.mms.ui.ForwardMessageActivity");
1075        startActivity(intent);
1076    }
1077
1078    /**
1079     * Context menu handlers for the message list view.
1080     */
1081    private final class MsgListMenuClickListener implements MenuItem.OnMenuItemClickListener {
1082        public boolean onMenuItemClick(MenuItem item) {
1083            if (!isCursorValid()) {
1084                return false;
1085            }
1086            Cursor cursor = mMsgListAdapter.getCursor();
1087            String type = cursor.getString(COLUMN_MSG_TYPE);
1088            long msgId = cursor.getLong(COLUMN_ID);
1089            MessageItem msgItem = getMessageItem(type, msgId, true);
1090
1091            if (msgItem == null) {
1092                return false;
1093            }
1094
1095            switch (item.getItemId()) {
1096                case MENU_EDIT_MESSAGE:
1097                    editMessageItem(msgItem);
1098                    drawBottomPanel();
1099                    return true;
1100
1101                case MENU_COPY_MESSAGE_TEXT:
1102                    copyToClipboard(msgItem.mBody);
1103                    return true;
1104
1105                case MENU_FORWARD_MESSAGE:
1106                    forwardMessage(msgItem);
1107                    return true;
1108
1109                case MENU_VIEW_SLIDESHOW:
1110                    MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this,
1111                            ContentUris.withAppendedId(Mms.CONTENT_URI, msgId), null);
1112                    return true;
1113
1114                case MENU_VIEW_MESSAGE_DETAILS: {
1115                    String messageDetails = MessageUtils.getMessageDetails(
1116                            ComposeMessageActivity.this, cursor, msgItem.mMessageSize);
1117                    new AlertDialog.Builder(ComposeMessageActivity.this)
1118                            .setTitle(R.string.message_details_title)
1119                            .setMessage(messageDetails)
1120                            .setCancelable(true)
1121                            .show();
1122                    return true;
1123                }
1124                case MENU_DELETE_MESSAGE: {
1125                    DeleteMessageListener l = new DeleteMessageListener(
1126                            msgItem.mMessageUri, msgItem.mLocked);
1127                    confirmDeleteDialog(l, msgItem.mLocked);
1128                    return true;
1129                }
1130                case MENU_DELIVERY_REPORT:
1131                    showDeliveryReport(msgId, type);
1132                    return true;
1133
1134                case MENU_COPY_TO_SDCARD: {
1135                    int resId = copyMedia(msgId) ? R.string.copy_to_sdcard_success :
1136                        R.string.copy_to_sdcard_fail;
1137                    Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show();
1138                    return true;
1139                }
1140
1141                case MENU_COPY_TO_DRM_PROVIDER: {
1142                    int resId = getDrmMimeSavedStringRsrc(msgId, copyToDrmProvider(msgId));
1143                    Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show();
1144                    return true;
1145                }
1146
1147                case MENU_LOCK_MESSAGE: {
1148                    lockMessage(msgItem, true);
1149                    return true;
1150                }
1151
1152                case MENU_UNLOCK_MESSAGE: {
1153                    lockMessage(msgItem, false);
1154                    return true;
1155                }
1156
1157                default:
1158                    return false;
1159            }
1160        }
1161    }
1162
1163    private void lockMessage(MessageItem msgItem, boolean locked) {
1164        Uri uri;
1165        if ("sms".equals(msgItem.mType)) {
1166            uri = Sms.CONTENT_URI;
1167        } else {
1168            uri = Mms.CONTENT_URI;
1169        }
1170        final Uri lockUri = ContentUris.withAppendedId(uri, msgItem.mMsgId);
1171
1172        final ContentValues values = new ContentValues(1);
1173        values.put("locked", locked ? 1 : 0);
1174
1175        new Thread(new Runnable() {
1176            public void run() {
1177                getContentResolver().update(lockUri,
1178                        values, null, null);
1179            }
1180        }, "lockMessage").start();
1181    }
1182
1183    /**
1184     * Looks to see if there are any valid parts of the attachment that can be copied to a SD card.
1185     * @param msgId
1186     */
1187    private boolean haveSomethingToCopyToSDCard(long msgId) {
1188        PduBody body = PduBodyCache.getPduBody(this,
1189                ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1190        if (body == null) {
1191            return false;
1192        }
1193
1194        boolean result = false;
1195        int partNum = body.getPartsNum();
1196        for(int i = 0; i < partNum; i++) {
1197            PduPart part = body.getPart(i);
1198            String type = new String(part.getContentType());
1199
1200            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1201                log("[CMA] haveSomethingToCopyToSDCard: part[" + i + "] contentType=" + type);
1202            }
1203
1204            if (ContentType.isImageType(type) || ContentType.isVideoType(type) ||
1205                    ContentType.isAudioType(type)) {
1206                result = true;
1207                break;
1208            }
1209        }
1210        return result;
1211    }
1212
1213    /**
1214     * Looks to see if there are any drm'd parts of the attachment that can be copied to the
1215     * DrmProvider. Right now we only support saving audio (e.g. ringtones).
1216     * @param msgId
1217     */
1218    private boolean haveSomethingToCopyToDrmProvider(long msgId) {
1219        String mimeType = getDrmMimeType(msgId);
1220        return isAudioMimeType(mimeType);
1221    }
1222
1223    /**
1224     * Simple cache to prevent having to load the same PduBody again and again for the same uri.
1225     */
1226    private static class PduBodyCache {
1227        private static PduBody mLastPduBody;
1228        private static Uri mLastUri;
1229
1230        static public PduBody getPduBody(Context context, Uri contentUri) {
1231            if (contentUri.equals(mLastUri)) {
1232                return mLastPduBody;
1233            }
1234            try {
1235                mLastPduBody = SlideshowModel.getPduBody(context, contentUri);
1236                mLastUri = contentUri;
1237             } catch (MmsException e) {
1238                 Log.e(TAG, e.getMessage(), e);
1239                 return null;
1240             }
1241             return mLastPduBody;
1242        }
1243    };
1244
1245    /**
1246     * Copies media from an Mms to the DrmProvider
1247     * @param msgId
1248     */
1249    private boolean copyToDrmProvider(long msgId) {
1250        boolean result = true;
1251        PduBody body = PduBodyCache.getPduBody(this,
1252                ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1253        if (body == null) {
1254            return false;
1255        }
1256
1257        int partNum = body.getPartsNum();
1258        for(int i = 0; i < partNum; i++) {
1259            PduPart part = body.getPart(i);
1260            String type = new String(part.getContentType());
1261
1262            if (ContentType.isDrmType(type)) {
1263                // All parts (but there's probably only a single one) have to be successful
1264                // for a valid result.
1265                result &= copyPartToDrmProvider(part);
1266            }
1267        }
1268        return result;
1269    }
1270
1271    private String mimeTypeOfDrmPart(PduPart part) {
1272        Uri uri = part.getDataUri();
1273        InputStream input = null;
1274        try {
1275            input = mContentResolver.openInputStream(uri);
1276            if (input instanceof FileInputStream) {
1277                FileInputStream fin = (FileInputStream) input;
1278
1279                DrmRawContent content = new DrmRawContent(fin, fin.available(),
1280                        DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING);
1281                String mimeType = content.getContentType();
1282                return mimeType;
1283            }
1284        } catch (IOException e) {
1285            // Ignore
1286            Log.e(TAG, "IOException caught while opening or reading stream", e);
1287        } catch (DrmException e) {
1288            Log.e(TAG, "DrmException caught ", e);
1289        } finally {
1290            if (null != input) {
1291                try {
1292                    input.close();
1293                } catch (IOException e) {
1294                    // Ignore
1295                    Log.e(TAG, "IOException caught while closing stream", e);
1296                }
1297            }
1298        }
1299        return null;
1300    }
1301
1302    /**
1303     * Returns the type of the first drm'd pdu part.
1304     * @param msgId
1305     */
1306    private String getDrmMimeType(long msgId) {
1307        PduBody body = PduBodyCache.getPduBody(this,
1308                ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1309        if (body == null) {
1310            return null;
1311        }
1312
1313        int partNum = body.getPartsNum();
1314        for(int i = 0; i < partNum; i++) {
1315            PduPart part = body.getPart(i);
1316            String type = new String(part.getContentType());
1317
1318            if (ContentType.isDrmType(type)) {
1319                return mimeTypeOfDrmPart(part);
1320            }
1321        }
1322        return null;
1323    }
1324
1325    private int getDrmMimeMenuStringRsrc(long msgId) {
1326        String mimeType = getDrmMimeType(msgId);
1327        if (isAudioMimeType(mimeType)) {
1328            return R.string.save_ringtone;
1329        }
1330        return 0;
1331    }
1332
1333    private int getDrmMimeSavedStringRsrc(long msgId, boolean success) {
1334        String mimeType = getDrmMimeType(msgId);
1335        if (isAudioMimeType(mimeType)) {
1336            return success ? R.string.saved_ringtone : R.string.saved_ringtone_fail;
1337        }
1338        return 0;
1339    }
1340
1341    private boolean isAudioMimeType(String mimeType) {
1342        return mimeType != null && mimeType.startsWith("audio/");
1343    }
1344
1345    private boolean isImageMimeType(String mimeType) {
1346        return mimeType != null && mimeType.startsWith("image/");
1347    }
1348
1349    private boolean copyPartToDrmProvider(PduPart part) {
1350        Uri uri = part.getDataUri();
1351
1352        InputStream input = null;
1353        try {
1354            input = mContentResolver.openInputStream(uri);
1355            if (input instanceof FileInputStream) {
1356                FileInputStream fin = (FileInputStream) input;
1357
1358                // Build a nice title
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 title = new String(location);
1370                int index;
1371                if ((index = title.indexOf(".")) == -1) {
1372                    String type = new String(part.getContentType());
1373                } else {
1374                    title = title.substring(0, index);
1375                }
1376
1377                // transfer the file to the DRM content provider
1378                Intent item = DrmStore.addDrmFile(mContentResolver, fin, title);
1379                if (item == null) {
1380                    Log.w(TAG, "unable to add file " + uri + " to DrmProvider");
1381                    return false;
1382                }
1383            }
1384        } catch (IOException e) {
1385            // Ignore
1386            Log.e(TAG, "IOException caught while opening or reading stream", e);
1387            return false;
1388        } finally {
1389            if (null != input) {
1390                try {
1391                    input.close();
1392                } catch (IOException e) {
1393                    // Ignore
1394                    Log.e(TAG, "IOException caught while closing stream", e);
1395                    return false;
1396                }
1397            }
1398        }
1399        return true;
1400    }
1401
1402    /**
1403     * Copies media from an Mms to the "download" directory on the SD card
1404     * @param msgId
1405     */
1406    private boolean copyMedia(long msgId) {
1407        boolean result = true;
1408        PduBody body = PduBodyCache.getPduBody(this,
1409                ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
1410        if (body == null) {
1411            return false;
1412        }
1413
1414        int partNum = body.getPartsNum();
1415        for(int i = 0; i < partNum; i++) {
1416            PduPart part = body.getPart(i);
1417            String type = new String(part.getContentType());
1418
1419            if (ContentType.isImageType(type) || ContentType.isVideoType(type) ||
1420                    ContentType.isAudioType(type)) {
1421                result &= copyPart(part, Long.toHexString(msgId));   // all parts have to be successful for a valid result.
1422            }
1423        }
1424        return result;
1425    }
1426
1427    private boolean copyPart(PduPart part, String fallback) {
1428        Uri uri = part.getDataUri();
1429
1430        InputStream input = null;
1431        FileOutputStream fout = null;
1432        try {
1433            input = mContentResolver.openInputStream(uri);
1434            if (input instanceof FileInputStream) {
1435                FileInputStream fin = (FileInputStream) input;
1436
1437                byte[] location = part.getName();
1438                if (location == null) {
1439                    location = part.getFilename();
1440                }
1441                if (location == null) {
1442                    location = part.getContentLocation();
1443                }
1444
1445                String fileName;
1446                if (location == null) {
1447                    // Use fallback name.
1448                    fileName = fallback;
1449                } else {
1450                    fileName = new String(location);
1451                }
1452                // Depending on the location, there may be an
1453                // extension already on the name or not
1454                String dir = Environment.getExternalStorageDirectory() + "/"
1455                                + Environment.DIRECTORY_DOWNLOADS  + "/";
1456                String extension;
1457                int index;
1458                if ((index = fileName.indexOf(".")) == -1) {
1459                    String type = new String(part.getContentType());
1460                    extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
1461                } else {
1462                    extension = fileName.substring(index + 1, fileName.length());
1463                    fileName = fileName.substring(0, index);
1464                }
1465
1466                File file = getUniqueDestination(dir + fileName, extension);
1467
1468                // make sure the path is valid and directories created for this file.
1469                File parentFile = file.getParentFile();
1470                if (!parentFile.exists() && !parentFile.mkdirs()) {
1471                    Log.e(TAG, "[MMS] copyPart: mkdirs for " + parentFile.getPath() + " failed!");
1472                    return false;
1473                }
1474
1475                fout = new FileOutputStream(file);
1476
1477                byte[] buffer = new byte[8000];
1478                int size = 0;
1479                while ((size=fin.read(buffer)) != -1) {
1480                    fout.write(buffer, 0, size);
1481                }
1482
1483                // Notify other applications listening to scanner events
1484                // that a media file has been added to the sd card
1485                sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
1486                        Uri.fromFile(file)));
1487            }
1488        } catch (IOException e) {
1489            // Ignore
1490            Log.e(TAG, "IOException caught while opening or reading stream", e);
1491            return false;
1492        } finally {
1493            if (null != input) {
1494                try {
1495                    input.close();
1496                } catch (IOException e) {
1497                    // Ignore
1498                    Log.e(TAG, "IOException caught while closing stream", e);
1499                    return false;
1500                }
1501            }
1502            if (null != fout) {
1503                try {
1504                    fout.close();
1505                } catch (IOException e) {
1506                    // Ignore
1507                    Log.e(TAG, "IOException caught while closing stream", e);
1508                    return false;
1509                }
1510            }
1511        }
1512        return true;
1513    }
1514
1515    private File getUniqueDestination(String base, String extension) {
1516        File file = new File(base + "." + extension);
1517
1518        for (int i = 2; file.exists(); i++) {
1519            file = new File(base + "_" + i + "." + extension);
1520        }
1521        return file;
1522    }
1523
1524    private void showDeliveryReport(long messageId, String type) {
1525        Intent intent = new Intent(this, DeliveryReportActivity.class);
1526        intent.putExtra("message_id", messageId);
1527        intent.putExtra("message_type", type);
1528
1529        startActivity(intent);
1530    }
1531
1532    private final IntentFilter mHttpProgressFilter = new IntentFilter(PROGRESS_STATUS_ACTION);
1533
1534    private final BroadcastReceiver mHttpProgressReceiver = new BroadcastReceiver() {
1535        @Override
1536        public void onReceive(Context context, Intent intent) {
1537            if (PROGRESS_STATUS_ACTION.equals(intent.getAction())) {
1538                long token = intent.getLongExtra("token",
1539                                    SendingProgressTokenManager.NO_TOKEN);
1540                if (token != mConversation.getThreadId()) {
1541                    return;
1542                }
1543
1544                int progress = intent.getIntExtra("progress", 0);
1545                switch (progress) {
1546                    case PROGRESS_START:
1547                        setProgressBarVisibility(true);
1548                        break;
1549                    case PROGRESS_ABORT:
1550                    case PROGRESS_COMPLETE:
1551                        setProgressBarVisibility(false);
1552                        break;
1553                    default:
1554                        setProgress(100 * progress);
1555                }
1556            }
1557        }
1558    };
1559
1560    private static ContactList sEmptyContactList;
1561
1562    private ContactList getRecipients() {
1563        // If the recipients editor is visible, the conversation has
1564        // not really officially 'started' yet.  Recipients will be set
1565        // on the conversation once it has been saved or sent.  In the
1566        // meantime, let anyone who needs the recipient list think it
1567        // is empty rather than giving them a stale one.
1568        if (isRecipientsEditorVisible()) {
1569            if (sEmptyContactList == null) {
1570                sEmptyContactList = new ContactList();
1571            }
1572            return sEmptyContactList;
1573        }
1574        return mConversation.getRecipients();
1575    }
1576
1577    private void updateTitle(ContactList list) {
1578        String title = null;;
1579        String subTitle = null;
1580        int cnt = list.size();
1581        switch (cnt) {
1582            case 0: {
1583                String recipient = null;
1584                if (mRecipientsEditor != null) {
1585                    recipient = mRecipientsEditor.getText().toString();
1586                }
1587                title = TextUtils.isEmpty(recipient) ? getString(R.string.new_message) : recipient;
1588                break;
1589            }
1590            case 1: {
1591                title = list.get(0).getName();      // get name returns the number if there's no
1592                                                    // name available.
1593                String number = list.get(0).getNumber();
1594                if (!title.equals(number)) {
1595                    subTitle = PhoneNumberUtils.formatNumber(number, number,
1596                            MmsApp.getApplication().getCurrentCountryIso());
1597                }
1598                break;
1599            }
1600            default: {
1601                // Handle multiple recipients
1602                title = list.formatNames(", ");
1603                subTitle = getResources().getQuantityString(R.plurals.recipient_count, cnt, cnt);
1604                break;
1605            }
1606        }
1607        mDebugRecipients = list.serialize();
1608
1609        ActionBar actionBar = getActionBar();
1610        actionBar.setTitle(title);
1611        actionBar.setSubtitle(subTitle);
1612    }
1613
1614    // Get the recipients editor ready to be displayed onscreen.
1615    private void initRecipientsEditor() {
1616        if (isRecipientsEditorVisible()) {
1617            return;
1618        }
1619        // Must grab the recipients before the view is made visible because getRecipients()
1620        // returns empty recipients when the editor is visible.
1621        ContactList recipients = getRecipients();
1622
1623        ViewStub stub = (ViewStub)findViewById(R.id.recipients_editor_stub);
1624        if (stub != null) {
1625            View stubView = stub.inflate();
1626            mRecipientsEditor = (RecipientsEditor) stubView.findViewById(R.id.recipients_editor);
1627            mRecipientsPicker = (ImageButton) stubView.findViewById(R.id.recipients_picker);
1628        } else {
1629            mRecipientsEditor = (RecipientsEditor)findViewById(R.id.recipients_editor);
1630            mRecipientsEditor.setVisibility(View.VISIBLE);
1631            mRecipientsPicker = (ImageButton)findViewById(R.id.recipients_picker);
1632        }
1633        mRecipientsPicker.setOnClickListener(this);
1634
1635        mRecipientsEditor.setAdapter(new RecipientsAdapter(this));
1636        mRecipientsEditor.populate(recipients);
1637        mRecipientsEditor.setOnCreateContextMenuListener(mRecipientsMenuCreateListener);
1638        mRecipientsEditor.addTextChangedListener(mRecipientsWatcher);
1639        // TODO : Remove the max length limitation due to the multiple phone picker is added and the
1640        // user is able to select a large number of recipients from the Contacts. The coming
1641        // potential issue is that it is hard for user to edit a recipient from hundred of
1642        // recipients in the editor box. We may redesign the editor box UI for this use case.
1643        // mRecipientsEditor.setFilters(new InputFilter[] {
1644        //         new InputFilter.LengthFilter(RECIPIENTS_MAX_LENGTH) });
1645        mRecipientsEditor.setOnItemClickListener(new AdapterView.OnItemClickListener() {
1646            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
1647                // After the user selects an item in the pop-up contacts list, move the
1648                // focus to the text editor if there is only one recipient.  This helps
1649                // the common case of selecting one recipient and then typing a message,
1650                // but avoids annoying a user who is trying to add five recipients and
1651                // keeps having focus stolen away.
1652                if (mRecipientsEditor.getRecipientCount() == 1) {
1653                    // if we're in extract mode then don't request focus
1654                    final InputMethodManager inputManager = (InputMethodManager)
1655                        getSystemService(Context.INPUT_METHOD_SERVICE);
1656                    if (inputManager == null || !inputManager.isFullscreenMode()) {
1657                        mTextEditor.requestFocus();
1658                    }
1659                }
1660            }
1661        });
1662
1663        mRecipientsEditor.setOnFocusChangeListener(new View.OnFocusChangeListener() {
1664            public void onFocusChange(View v, boolean hasFocus) {
1665                if (!hasFocus) {
1666                    RecipientsEditor editor = (RecipientsEditor) v;
1667                    ContactList contacts = editor.constructContactsFromInput(false);
1668                    updateTitle(contacts);
1669                }
1670            }
1671        });
1672
1673        mTopPanel.setVisibility(View.VISIBLE);
1674    }
1675
1676    //==========================================================
1677    // Activity methods
1678    //==========================================================
1679
1680    public static boolean cancelFailedToDeliverNotification(Intent intent, Context context) {
1681        if (MessagingNotification.isFailedToDeliver(intent)) {
1682            // Cancel any failed message notifications
1683            MessagingNotification.cancelNotification(context,
1684                        MessagingNotification.MESSAGE_FAILED_NOTIFICATION_ID);
1685            return true;
1686        }
1687        return false;
1688    }
1689
1690    public static boolean cancelFailedDownloadNotification(Intent intent, Context context) {
1691        if (MessagingNotification.isFailedToDownload(intent)) {
1692            // Cancel any failed download notifications
1693            MessagingNotification.cancelNotification(context,
1694                        MessagingNotification.DOWNLOAD_FAILED_NOTIFICATION_ID);
1695            return true;
1696        }
1697        return false;
1698    }
1699
1700    @Override
1701    protected void onCreate(Bundle savedInstanceState) {
1702        super.onCreate(savedInstanceState);
1703
1704        resetConfiguration(getResources().getConfiguration());
1705
1706        setContentView(R.layout.compose_message_activity);
1707        setProgressBarVisibility(false);
1708
1709        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
1710                WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
1711
1712        // Initialize members for UI elements.
1713        initResourceRefs();
1714
1715        mContentResolver = getContentResolver();
1716        mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver);
1717
1718        initialize(0);
1719
1720        if (TRACE) {
1721            android.os.Debug.startMethodTracing("compose");
1722        }
1723    }
1724
1725    private void showSubjectEditor(boolean show) {
1726        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1727            log("" + show);
1728        }
1729
1730        if (mSubjectTextEditor == null) {
1731            // Don't bother to initialize the subject editor if
1732            // we're just going to hide it.
1733            if (show == false) {
1734                return;
1735            }
1736            mSubjectTextEditor = (EditText)findViewById(R.id.subject);
1737        }
1738
1739        mSubjectTextEditor.setOnKeyListener(show ? mSubjectKeyListener : null);
1740
1741        if (show) {
1742            mSubjectTextEditor.addTextChangedListener(mSubjectEditorWatcher);
1743        } else {
1744            mSubjectTextEditor.removeTextChangedListener(mSubjectEditorWatcher);
1745        }
1746
1747        mSubjectTextEditor.setText(mWorkingMessage.getSubject());
1748        mSubjectTextEditor.setVisibility(show ? View.VISIBLE : View.GONE);
1749        hideOrShowTopPanel();
1750    }
1751
1752    private void hideOrShowTopPanel() {
1753        boolean anySubViewsVisible = (isSubjectEditorVisible() || isRecipientsEditorVisible());
1754        mTopPanel.setVisibility(anySubViewsVisible ? View.VISIBLE : View.GONE);
1755    }
1756
1757    public void initialize(long originalThreadId) {
1758        Intent intent = getIntent();
1759
1760        // Create a new empty working message.
1761        mWorkingMessage = WorkingMessage.createEmpty(this);
1762
1763        // Read parameters or previously saved state of this activity. This will load a new
1764        // mConversation
1765        initActivityState(intent);
1766
1767        if (LogTag.SEVERE_WARNING && originalThreadId != 0 &&
1768                originalThreadId == mConversation.getThreadId()) {
1769            LogTag.warnPossibleRecipientMismatch("ComposeMessageActivity.initialize: " +
1770                    " threadId didn't change from: " + originalThreadId, this);
1771        }
1772
1773        log(" intent = " + intent +
1774            "originalThreadId = " + originalThreadId +
1775            " mConversation = " + mConversation);
1776
1777        if (cancelFailedToDeliverNotification(getIntent(), this)) {
1778            // Show a pop-up dialog to inform user the message was
1779            // failed to deliver.
1780            undeliveredMessageDialog(getMessageDate(null));
1781        }
1782        cancelFailedDownloadNotification(getIntent(), this);
1783
1784        // Set up the message history ListAdapter
1785        initMessageList();
1786
1787        // Load the draft for this thread, if we aren't already handling
1788        // existing data, such as a shared picture or forwarded message.
1789        boolean isForwardedMessage = false;
1790        if (!handleSendIntent(intent)) {
1791            isForwardedMessage = handleForwardedMessage();
1792            if (!isForwardedMessage) {
1793                loadDraft();
1794            }
1795        }
1796
1797        // Let the working message know what conversation it belongs to
1798        mWorkingMessage.setConversation(mConversation);
1799
1800        // Show the recipients editor if we don't have a valid thread. Hide it otherwise.
1801        if (mConversation.getThreadId() <= 0) {
1802            // Hide the recipients editor so the call to initRecipientsEditor won't get
1803            // short-circuited.
1804            hideRecipientEditor();
1805            initRecipientsEditor();
1806
1807            // Bring up the softkeyboard so the user can immediately enter recipients. This
1808            // call won't do anything on devices with a hard keyboard.
1809            getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
1810                    WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
1811        } else {
1812            hideRecipientEditor();
1813        }
1814
1815        updateSendButtonState();
1816
1817        drawTopPanel(false);
1818        drawBottomPanel();
1819
1820        onKeyboardStateChanged(mIsKeyboardOpen);
1821
1822        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1823            log("update title, mConversation=" + mConversation.toString());
1824        }
1825
1826        updateTitle(mConversation.getRecipients());
1827
1828        if (isForwardedMessage && isRecipientsEditorVisible()) {
1829            // The user is forwarding the message to someone. Put the focus on the
1830            // recipient editor rather than in the message editor.
1831            mRecipientsEditor.requestFocus();
1832        }
1833    }
1834
1835    @Override
1836    protected void onNewIntent(Intent intent) {
1837        super.onNewIntent(intent);
1838
1839        setIntent(intent);
1840
1841        Conversation conversation = null;
1842        mSentMessage = false;
1843
1844        // If we have been passed a thread_id, use that to find our
1845        // conversation.
1846
1847        // Note that originalThreadId might be zero but if this is a draft and we save the
1848        // draft, ensureThreadId gets called async from WorkingMessage.asyncUpdateDraftSmsMessage
1849        // the thread will get a threadId behind the UI thread's back.
1850        long originalThreadId = mConversation.getThreadId();
1851        long threadId = intent.getLongExtra("thread_id", 0);
1852        Uri intentUri = intent.getData();
1853
1854        boolean sameThread = false;
1855        if (threadId > 0) {
1856            conversation = Conversation.get(this, threadId, false);
1857        } else {
1858            if (mConversation.getThreadId() == 0) {
1859                // We've got a draft. Make sure the working recipients are synched
1860                // to the conversation so when we compare conversations later in this function,
1861                // the compare will work.
1862                mWorkingMessage.syncWorkingRecipients();
1863            }
1864            // Get the "real" conversation based on the intentUri. The intentUri might specify
1865            // the conversation by a phone number or by a thread id. We'll typically get a threadId
1866            // based uri when the user pulls down a notification while in ComposeMessageActivity and
1867            // we end up here in onNewIntent. mConversation can have a threadId of zero when we're
1868            // working on a draft. When a new message comes in for that same recipient, a
1869            // conversation will get created behind CMA's back when the message is inserted into
1870            // the database and the corresponding entry made in the threads table. The code should
1871            // use the real conversation as soon as it can rather than finding out the threadId
1872            // when sending with "ensureThreadId".
1873            conversation = Conversation.get(this, intentUri, false);
1874        }
1875
1876        if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1877            log("onNewIntent: data=" + intentUri + ", thread_id extra is " + threadId +
1878                    ", new conversation=" + conversation + ", mConversation=" + mConversation);
1879        }
1880
1881        // this is probably paranoid to compare both thread_ids and recipient lists,
1882        // but we want to make double sure because this is a last minute fix for Froyo
1883        // and the previous code checked thread ids only.
1884        // (we cannot just compare thread ids because there is a case where mConversation
1885        // has a stale/obsolete thread id (=1) that could collide against the new thread_id(=1),
1886        // even though the recipient lists are different)
1887        sameThread = ((conversation.getThreadId() == mConversation.getThreadId() ||
1888                mConversation.getThreadId() == 0) &&
1889                conversation.equals(mConversation));
1890
1891        // Don't let any markAsRead DB updates occur before we've loaded the messages for
1892        // the thread. Unblocking occurs when we're done querying for the conversation
1893        // items.
1894        conversation.blockMarkAsRead(true);
1895
1896        if (sameThread) {
1897            log("onNewIntent: same conversation");
1898            if (mConversation.getThreadId() == 0) {
1899                mConversation = conversation;
1900                mWorkingMessage.setConversation(mConversation);
1901            }
1902            mConversation.markAsRead();         // dismiss any notifications for this convo
1903        } else {
1904            if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1905                log("onNewIntent: different conversation");
1906            }
1907            saveDraft(false);    // if we've got a draft, save it first
1908
1909            initialize(originalThreadId);
1910        }
1911        loadMessageContent();
1912    }
1913
1914    private void sanityCheckConversation() {
1915        if (mWorkingMessage.getConversation() != mConversation) {
1916            LogTag.warnPossibleRecipientMismatch(
1917                    "ComposeMessageActivity: mWorkingMessage.mConversation=" +
1918                    mWorkingMessage.getConversation() + ", mConversation=" +
1919                    mConversation + ", MISMATCH!", this);
1920        }
1921    }
1922
1923    @Override
1924    protected void onRestart() {
1925        super.onRestart();
1926
1927        if (mWorkingMessage.isDiscarded()) {
1928            // If the message isn't worth saving, don't resurrect it. Doing so can lead to
1929            // a situation where a new incoming message gets the old thread id of the discarded
1930            // draft. This activity can end up displaying the recipients of the old message with
1931            // the contents of the new message. Recognize that dangerous situation and bail out
1932            // to the ConversationList where the user can enter this in a clean manner.
1933            if (mWorkingMessage.isWorthSaving()) {
1934                if (LogTag.VERBOSE) {
1935                    log("onRestart: mWorkingMessage.unDiscard()");
1936                }
1937                mWorkingMessage.unDiscard();    // it was discarded in onStop().
1938
1939                sanityCheckConversation();
1940            } else if (isRecipientsEditorVisible()) {
1941                if (LogTag.VERBOSE) {
1942                    log("onRestart: goToConversationList");
1943                }
1944                goToConversationList();
1945            } else {
1946                if (LogTag.VERBOSE) {
1947                    log("onRestart: loadDraft");
1948                }
1949                loadDraft();
1950                mWorkingMessage.setConversation(mConversation);
1951                mAttachmentEditor.update(mWorkingMessage);
1952            }
1953        }
1954    }
1955
1956    @Override
1957    protected void onStart() {
1958        super.onStart();
1959        mConversation.blockMarkAsRead(true);
1960
1961        initFocus();
1962
1963        // Register a BroadcastReceiver to listen on HTTP I/O process.
1964        registerReceiver(mHttpProgressReceiver, mHttpProgressFilter);
1965
1966        loadMessageContent();
1967
1968        // Update the fasttrack info in case any of the recipients' contact info changed
1969        // while we were paused. This can happen, for example, if a user changes or adds
1970        // an avatar associated with a contact.
1971        mWorkingMessage.syncWorkingRecipients();
1972
1973        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
1974            log("update title, mConversation=" + mConversation.toString());
1975        }
1976
1977        updateTitle(mConversation.getRecipients());
1978
1979        ActionBar actionBar = getActionBar();
1980        actionBar.setDisplayHomeAsUpEnabled(true);
1981    }
1982
1983    public void loadMessageContent() {
1984        startMsgListQuery();
1985        updateSendFailedNotification();
1986        drawBottomPanel();
1987    }
1988
1989    private void updateSendFailedNotification() {
1990        final long threadId = mConversation.getThreadId();
1991        if (threadId <= 0)
1992            return;
1993
1994        // updateSendFailedNotificationForThread makes a database call, so do the work off
1995        // of the ui thread.
1996        new Thread(new Runnable() {
1997            public void run() {
1998                MessagingNotification.updateSendFailedNotificationForThread(
1999                        ComposeMessageActivity.this, threadId);
2000            }
2001        }, "updateSendFailedNotification").start();
2002    }
2003
2004    @Override
2005    protected void onResume() {
2006        super.onResume();
2007
2008        // OLD: get notified of presence updates to update the titlebar.
2009        // NEW: we are using ContactHeaderWidget which displays presence, but updating presence
2010        //      there is out of our control.
2011        //Contact.startPresenceObserver();
2012
2013        addRecipientsListeners();
2014
2015        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2016            log("update title, mConversation=" + mConversation.toString());
2017        }
2018
2019        // There seems to be a bug in the framework such that setting the title
2020        // here gets overwritten to the original title.  Do this delayed as a
2021        // workaround.
2022        mMessageListItemHandler.postDelayed(new Runnable() {
2023            public void run() {
2024                ContactList recipients = isRecipientsEditorVisible() ?
2025                        mRecipientsEditor.constructContactsFromInput(false) : getRecipients();
2026                updateTitle(recipients);
2027            }
2028        }, 100);
2029    }
2030
2031    @Override
2032    protected void onPause() {
2033        super.onPause();
2034
2035        // OLD: stop getting notified of presence updates to update the titlebar.
2036        // NEW: we are using ContactHeaderWidget which displays presence, but updating presence
2037        //      there is out of our control.
2038        //Contact.stopPresenceObserver();
2039
2040        removeRecipientsListeners();
2041    }
2042
2043    @Override
2044    protected void onStop() {
2045        super.onStop();
2046
2047        // Allow any blocked calls to update the thread's read status.
2048        mConversation.blockMarkAsRead(false);
2049
2050        if (mMsgListAdapter != null) {
2051            mMsgListAdapter.changeCursor(null);
2052        }
2053
2054        if (mRecipientsEditor != null) {
2055            CursorAdapter recipientsAdapter = (CursorAdapter)mRecipientsEditor.getAdapter();
2056            if (recipientsAdapter != null) {
2057                recipientsAdapter.changeCursor(null);
2058            }
2059        }
2060
2061        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2062            log("save draft");
2063        }
2064        saveDraft(true);
2065
2066        // Cleanup the BroadcastReceiver.
2067        unregisterReceiver(mHttpProgressReceiver);
2068    }
2069
2070    @Override
2071    protected void onDestroy() {
2072        if (TRACE) {
2073            android.os.Debug.stopMethodTracing();
2074        }
2075
2076        super.onDestroy();
2077    }
2078
2079    @Override
2080    public void onConfigurationChanged(Configuration newConfig) {
2081        super.onConfigurationChanged(newConfig);
2082        if (LOCAL_LOGV) {
2083            Log.v(TAG, "onConfigurationChanged: " + newConfig);
2084        }
2085
2086        if (resetConfiguration(newConfig)) {
2087            // Have to re-layout the attachment editor because we have different layouts
2088            // depending on whether we're portrait or landscape.
2089            drawTopPanel(isSubjectEditorVisible());
2090        }
2091        onKeyboardStateChanged(mIsKeyboardOpen);
2092    }
2093
2094    // returns true if landscape/portrait configuration has changed
2095    private boolean resetConfiguration(Configuration config) {
2096        mIsKeyboardOpen = config.keyboardHidden == KEYBOARDHIDDEN_NO;
2097        boolean isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE;
2098        if (mIsLandscape != isLandscape) {
2099            mIsLandscape = isLandscape;
2100            return true;
2101        }
2102        return false;
2103    }
2104
2105    private void onKeyboardStateChanged(boolean isKeyboardOpen) {
2106        // If the keyboard is hidden, don't show focus highlights for
2107        // things that cannot receive input.
2108        if (isKeyboardOpen) {
2109            if (mRecipientsEditor != null) {
2110                mRecipientsEditor.setFocusableInTouchMode(true);
2111            }
2112            if (mSubjectTextEditor != null) {
2113                mSubjectTextEditor.setFocusableInTouchMode(true);
2114            }
2115            mTextEditor.setFocusableInTouchMode(true);
2116            mTextEditor.setHint(R.string.type_to_compose_text_enter_to_send);
2117        } else {
2118            if (mRecipientsEditor != null) {
2119                mRecipientsEditor.setFocusable(false);
2120            }
2121            if (mSubjectTextEditor != null) {
2122                mSubjectTextEditor.setFocusable(false);
2123            }
2124            mTextEditor.setFocusable(false);
2125            mTextEditor.setHint(R.string.open_keyboard_to_compose_message);
2126        }
2127    }
2128
2129    @Override
2130    public void onUserInteraction() {
2131        checkPendingNotification();
2132    }
2133
2134    @Override
2135    public void onWindowFocusChanged(boolean hasFocus) {
2136        if (hasFocus) {
2137            checkPendingNotification();
2138        }
2139    }
2140
2141    @Override
2142    public boolean onKeyDown(int keyCode, KeyEvent event) {
2143        switch (keyCode) {
2144            case KeyEvent.KEYCODE_DEL:
2145                if ((mMsgListAdapter != null) && mMsgListView.isFocused()) {
2146                    Cursor cursor;
2147                    try {
2148                        cursor = (Cursor) mMsgListView.getSelectedItem();
2149                    } catch (ClassCastException e) {
2150                        Log.e(TAG, "Unexpected ClassCastException.", e);
2151                        return super.onKeyDown(keyCode, event);
2152                    }
2153
2154                    if (cursor != null) {
2155                        boolean locked = cursor.getInt(COLUMN_MMS_LOCKED) != 0;
2156                        DeleteMessageListener l = new DeleteMessageListener(
2157                                cursor.getLong(COLUMN_ID),
2158                                cursor.getString(COLUMN_MSG_TYPE),
2159                                locked);
2160                        confirmDeleteDialog(l, locked);
2161                        return true;
2162                    }
2163                }
2164                break;
2165            case KeyEvent.KEYCODE_DPAD_CENTER:
2166            case KeyEvent.KEYCODE_ENTER:
2167                if (isPreparedForSending()) {
2168                    confirmSendMessageIfNeeded();
2169                    return true;
2170                }
2171                break;
2172            case KeyEvent.KEYCODE_BACK:
2173                exitComposeMessageActivity(new Runnable() {
2174                    public void run() {
2175                        finish();
2176                    }
2177                });
2178                return true;
2179        }
2180
2181        return super.onKeyDown(keyCode, event);
2182    }
2183
2184    private void exitComposeMessageActivity(final Runnable exit) {
2185        // If the message is empty, just quit -- finishing the
2186        // activity will cause an empty draft to be deleted.
2187        if (!mWorkingMessage.isWorthSaving()) {
2188            exit.run();
2189            return;
2190        }
2191
2192        if (isRecipientsEditorVisible() &&
2193                !mRecipientsEditor.hasValidRecipient(mWorkingMessage.requiresMms())) {
2194            MessageUtils.showDiscardDraftConfirmDialog(this, new DiscardDraftListener());
2195            return;
2196        }
2197
2198        mToastForDraftSave = true;
2199        exit.run();
2200    }
2201
2202    private void goToConversationList() {
2203        finish();
2204        startActivity(new Intent(this, ConversationList.class));
2205    }
2206
2207    private void hideRecipientEditor() {
2208        if (mRecipientsEditor != null) {
2209            mRecipientsEditor.removeTextChangedListener(mRecipientsWatcher);
2210            mRecipientsEditor.setVisibility(View.GONE);
2211            hideOrShowTopPanel();
2212        }
2213    }
2214
2215    private boolean isRecipientsEditorVisible() {
2216        return (null != mRecipientsEditor)
2217                    && (View.VISIBLE == mRecipientsEditor.getVisibility());
2218    }
2219
2220    private boolean isSubjectEditorVisible() {
2221        return (null != mSubjectTextEditor)
2222                    && (View.VISIBLE == mSubjectTextEditor.getVisibility());
2223    }
2224
2225    public void onAttachmentChanged() {
2226        // Have to make sure we're on the UI thread. This function can be called off of the UI
2227        // thread when we're adding multi-attachments
2228        runOnUiThread(new Runnable() {
2229            public void run() {
2230                drawBottomPanel();
2231                updateSendButtonState();
2232                drawTopPanel(isSubjectEditorVisible());
2233            }
2234        });
2235    }
2236
2237    public void onProtocolChanged(final boolean mms) {
2238        // Have to make sure we're on the UI thread. This function can be called off of the UI
2239        // thread when we're adding multi-attachments
2240        runOnUiThread(new Runnable() {
2241            public void run() {
2242                toastConvertInfo(mms);
2243                showSmsOrMmsSendButton(mms);
2244
2245                if (mms) {
2246                    // In the case we went from a long sms with a counter to an mms because
2247                    // the user added an attachment or a subject, hide the counter --
2248                    // it doesn't apply to mms.
2249                    mTextCounter.setVisibility(View.GONE);
2250                }
2251            }
2252        });
2253    }
2254
2255    // Show or hide the Sms or Mms button as appropriate. Return the view so that the caller
2256    // can adjust the enableness and focusability.
2257    private View showSmsOrMmsSendButton(boolean isMms) {
2258        View showButton;
2259        View hideButton;
2260        if (isMms) {
2261            showButton = mSendButtonMms;
2262            hideButton = mSendButtonSms;
2263        } else {
2264            showButton = mSendButtonSms;
2265            hideButton = mSendButtonMms;
2266        }
2267        showButton.setVisibility(View.VISIBLE);
2268        hideButton.setVisibility(View.GONE);
2269
2270        return showButton;
2271    }
2272
2273    Runnable mResetMessageRunnable = new Runnable() {
2274        public void run() {
2275            resetMessage();
2276        }
2277    };
2278
2279    public void onPreMessageSent() {
2280        runOnUiThread(mResetMessageRunnable);
2281    }
2282
2283    public void onMessageSent() {
2284        // If we already have messages in the list adapter, it
2285        // will be auto-requerying; don't thrash another query in.
2286        if (mMsgListAdapter.getCount() == 0) {
2287            if (LogTag.VERBOSE) {
2288                log("onMessageSent");
2289            }
2290            startMsgListQuery();
2291        }
2292    }
2293
2294    public void onMaxPendingMessagesReached() {
2295        saveDraft(false);
2296
2297        runOnUiThread(new Runnable() {
2298            public void run() {
2299                Toast.makeText(ComposeMessageActivity.this, R.string.too_many_unsent_mms,
2300                        Toast.LENGTH_LONG).show();
2301            }
2302        });
2303    }
2304
2305    public void onAttachmentError(final int error) {
2306        runOnUiThread(new Runnable() {
2307            public void run() {
2308                handleAddAttachmentError(error, R.string.type_picture);
2309                onMessageSent();        // now requery the list of messages
2310            }
2311        });
2312    }
2313
2314    // We don't want to show the "call" option unless there is only one
2315    // recipient and it's a phone number.
2316    private boolean isRecipientCallable() {
2317        ContactList recipients = getRecipients();
2318        return (recipients.size() == 1 && !recipients.containsEmail());
2319    }
2320
2321    private void dialRecipient() {
2322        if (isRecipientCallable()) {
2323            String number = getRecipients().get(0).getNumber();
2324            Intent dialIntent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + number));
2325            startActivity(dialIntent);
2326        }
2327    }
2328
2329    @Override
2330    public boolean onPrepareOptionsMenu(Menu menu) {
2331        menu.clear();
2332
2333        if (isRecipientCallable()) {
2334            MenuItem item = menu.add(0, MENU_CALL_RECIPIENT, 0, R.string.menu_call)
2335                .setIcon(R.drawable.ic_menu_call)
2336                .setTitle(R.string.menu_call);
2337            if (!isRecipientsEditorVisible()) {
2338                // If we're not composing a new message, show the call icon in the actionbar
2339                item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
2340            }
2341        }
2342
2343        if (MmsConfig.getMmsEnabled()) {
2344            if (!isSubjectEditorVisible()) {
2345                menu.add(0, MENU_ADD_SUBJECT, 0, R.string.add_subject).setIcon(
2346                        R.drawable.ic_menu_edit);
2347            }
2348            menu.add(0, MENU_ADD_ATTACHMENT, 0, R.string.add_attachment)
2349                .setIcon(R.drawable.ic_menu_attachment)
2350                .setTitle(R.string.add_attachment)
2351                .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);    // add to actionbar
2352        }
2353
2354        if (isPreparedForSending()) {
2355            menu.add(0, MENU_SEND, 0, R.string.send).setIcon(android.R.drawable.ic_menu_send);
2356        }
2357
2358        menu.add(0, MENU_INSERT_SMILEY, 0, R.string.menu_insert_smiley).setIcon(
2359                R.drawable.ic_menu_emoticons);
2360
2361        if (mMsgListAdapter.getCount() > 0) {
2362            // Removed search as part of b/1205708
2363            //menu.add(0, MENU_SEARCH, 0, R.string.menu_search).setIcon(
2364            //        R.drawable.ic_menu_search);
2365            Cursor cursor = mMsgListAdapter.getCursor();
2366            if ((null != cursor) && (cursor.getCount() > 0)) {
2367                menu.add(0, MENU_DELETE_THREAD, 0, R.string.delete_thread).setIcon(
2368                    android.R.drawable.ic_menu_delete);
2369            }
2370        } else {
2371            menu.add(0, MENU_DISCARD, 0, R.string.discard).setIcon(android.R.drawable.ic_menu_delete);
2372        }
2373
2374        buildAddAddressToContactMenuItem(menu);
2375
2376        menu.add(0, MENU_PREFERENCES, 0, R.string.menu_preferences).setIcon(
2377                android.R.drawable.ic_menu_preferences);
2378
2379        if (LogTag.DEBUG_DUMP) {
2380            menu.add(0, MENU_DEBUG_DUMP, 0, R.string.menu_debug_dump);
2381        }
2382
2383        return true;
2384    }
2385
2386    private void buildAddAddressToContactMenuItem(Menu menu) {
2387        // Look for the first recipient we don't have a contact for and create a menu item to
2388        // add the number to contacts.
2389        for (Contact c : getRecipients()) {
2390            if (!c.existsInDatabase() && canAddToContacts(c)) {
2391                Intent intent = ConversationList.createAddContactIntent(c.getNumber());
2392                menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, R.string.menu_add_to_contacts)
2393                    .setIcon(android.R.drawable.ic_menu_add)
2394                    .setIntent(intent);
2395                break;
2396            }
2397        }
2398    }
2399
2400    @Override
2401    public boolean onOptionsItemSelected(MenuItem item) {
2402        switch (item.getItemId()) {
2403            case MENU_ADD_SUBJECT:
2404                showSubjectEditor(true);
2405                mWorkingMessage.setSubject("", true);
2406                mSubjectTextEditor.requestFocus();
2407                break;
2408            case MENU_ADD_ATTACHMENT:
2409                // Launch the add-attachment list dialog
2410                showAddAttachmentDialog(false);
2411                break;
2412            case MENU_DISCARD:
2413                mWorkingMessage.discard();
2414                finish();
2415                break;
2416            case MENU_SEND:
2417                if (isPreparedForSending()) {
2418                    confirmSendMessageIfNeeded();
2419                }
2420                break;
2421            case MENU_SEARCH:
2422                onSearchRequested();
2423                break;
2424            case MENU_DELETE_THREAD:
2425                confirmDeleteThread(mConversation.getThreadId());
2426                break;
2427
2428            case android.R.id.home:
2429                onBackPressed();
2430                break;
2431
2432            case MENU_CONVERSATION_LIST:
2433                exitComposeMessageActivity(new Runnable() {
2434                    public void run() {
2435                        goToConversationList();
2436                    }
2437                });
2438                break;
2439            case MENU_CALL_RECIPIENT:
2440                dialRecipient();
2441                break;
2442            case MENU_INSERT_SMILEY:
2443                showSmileyDialog();
2444                break;
2445            case MENU_VIEW_CONTACT: {
2446                // View the contact for the first (and only) recipient.
2447                ContactList list = getRecipients();
2448                if (list.size() == 1 && list.get(0).existsInDatabase()) {
2449                    Uri contactUri = list.get(0).getUri();
2450                    Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
2451                    intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
2452                    startActivity(intent);
2453                }
2454                break;
2455            }
2456            case MENU_ADD_ADDRESS_TO_CONTACTS:
2457                mAddContactIntent = item.getIntent();
2458                startActivityForResult(mAddContactIntent, REQUEST_CODE_ADD_CONTACT);
2459                break;
2460            case MENU_PREFERENCES: {
2461                Intent intent = new Intent(this, MessagingPreferenceActivity.class);
2462                startActivityIfNeeded(intent, -1);
2463                break;
2464            }
2465            case MENU_DEBUG_DUMP:
2466                mWorkingMessage.dump();
2467                Conversation.dump();
2468                LogTag.dumpInternalTables(this);
2469                break;
2470        }
2471
2472        return true;
2473    }
2474
2475    private void confirmDeleteThread(long threadId) {
2476        Conversation.startQueryHaveLockedMessages(mBackgroundQueryHandler,
2477                threadId, ConversationList.HAVE_LOCKED_MESSAGES_TOKEN);
2478    }
2479
2480//    static class SystemProperties { // TODO, temp class to get unbundling working
2481//        static int getInt(String s, int value) {
2482//            return value;       // just return the default value or now
2483//        }
2484//    }
2485
2486    private void addAttachment(int type, boolean replace) {
2487        // Calculate the size of the current slide if we're doing a replace so the
2488        // slide size can optionally be used in computing how much room is left for an attachment.
2489        int currentSlideSize = 0;
2490        SlideshowModel slideShow = mWorkingMessage.getSlideshow();
2491        if (replace && slideShow != null) {
2492            SlideModel slide = slideShow.get(0);
2493            currentSlideSize = slide.getSlideSize();
2494        }
2495        switch (type) {
2496            case AttachmentTypeSelectorAdapter.ADD_IMAGE:
2497                MessageUtils.selectImage(this, REQUEST_CODE_ATTACH_IMAGE);
2498                break;
2499
2500            case AttachmentTypeSelectorAdapter.TAKE_PICTURE: {
2501                Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
2502
2503                intent.putExtra(MediaStore.EXTRA_OUTPUT, TempFileProvider.SCRAP_CONTENT_URI);
2504                startActivityForResult(intent, REQUEST_CODE_TAKE_PICTURE);
2505                break;
2506            }
2507
2508            case AttachmentTypeSelectorAdapter.ADD_VIDEO:
2509                MessageUtils.selectVideo(this, REQUEST_CODE_ATTACH_VIDEO);
2510                break;
2511
2512            case AttachmentTypeSelectorAdapter.RECORD_VIDEO: {
2513                long sizeLimit = computeAttachmentSizeLimit(slideShow, currentSlideSize);
2514                if (sizeLimit > 0) {
2515                    MessageUtils.recordVideo(this, REQUEST_CODE_TAKE_VIDEO, sizeLimit);
2516                } else {
2517                    Toast.makeText(this,
2518                            getString(R.string.message_too_big_for_video),
2519                            Toast.LENGTH_SHORT).show();
2520                }
2521            }
2522            break;
2523
2524            case AttachmentTypeSelectorAdapter.ADD_SOUND:
2525                MessageUtils.selectAudio(this, REQUEST_CODE_ATTACH_SOUND);
2526                break;
2527
2528            case AttachmentTypeSelectorAdapter.RECORD_SOUND:
2529                long sizeLimit = computeAttachmentSizeLimit(slideShow, currentSlideSize);
2530                MessageUtils.recordSound(this, REQUEST_CODE_RECORD_SOUND, sizeLimit);
2531                break;
2532
2533            case AttachmentTypeSelectorAdapter.ADD_SLIDESHOW:
2534                editSlideshow();
2535                break;
2536
2537            default:
2538                break;
2539        }
2540    }
2541
2542    public static long computeAttachmentSizeLimit(SlideshowModel slideShow, int currentSlideSize) {
2543        // Computer attachment size limit. Subtract 1K for some text.
2544        long sizeLimit = MmsConfig.getMaxMessageSize() - SlideshowModel.SLIDESHOW_SLOP;
2545        if (slideShow != null) {
2546            sizeLimit -= slideShow.getCurrentMessageSize();
2547
2548            // We're about to ask the camera to capture some video (or the sound recorder
2549            // to record some audio) which will eventually replace the content on the current
2550            // slide. Since the current slide already has some content (which was subtracted
2551            // out just above) and that content is going to get replaced, we can add the size of the
2552            // current slide into the available space used to capture a video (or audio).
2553            sizeLimit += currentSlideSize;
2554        }
2555        return sizeLimit;
2556    }
2557
2558    private void showAddAttachmentDialog(final boolean replace) {
2559        AlertDialog.Builder builder = new AlertDialog.Builder(this);
2560        builder.setIcon(R.drawable.ic_dialog_attach);
2561        builder.setTitle(R.string.add_attachment);
2562
2563        if (mAttachmentTypeSelectorAdapter == null) {
2564            mAttachmentTypeSelectorAdapter = new AttachmentTypeSelectorAdapter(
2565                    this, AttachmentTypeSelectorAdapter.MODE_WITH_SLIDESHOW);
2566        }
2567        builder.setAdapter(mAttachmentTypeSelectorAdapter, new DialogInterface.OnClickListener() {
2568            public void onClick(DialogInterface dialog, int which) {
2569                addAttachment(mAttachmentTypeSelectorAdapter.buttonToCommand(which), replace);
2570                dialog.dismiss();
2571            }
2572        });
2573
2574        builder.show();
2575    }
2576
2577    @Override
2578    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
2579        if (DEBUG) {
2580            log("requestCode=" + requestCode + ", resultCode=" + resultCode + ", data=" + data);
2581        }
2582        mWaitingForSubActivity = false;          // We're back!
2583        if (mWorkingMessage.isFakeMmsForDraft()) {
2584            // We no longer have to fake the fact we're an Mms. At this point we are or we aren't,
2585            // based on attachments and other Mms attrs.
2586            mWorkingMessage.removeFakeMmsForDraft();
2587        }
2588
2589        if (requestCode == REQUEST_CODE_PICK) {
2590            mWorkingMessage.asyncDeleteDraftSmsMessage(mConversation);
2591        }
2592
2593        // If there's no data (because the user didn't select a picture and
2594        // just hit BACK, for example), there's nothing to do.
2595        if (requestCode != REQUEST_CODE_TAKE_PICTURE) {
2596            if (data == null) {
2597                return;
2598            }
2599        } else if (resultCode != RESULT_OK){
2600            if (DEBUG) log("bail due to resultCode=" + resultCode);
2601            return;
2602        }
2603
2604        switch(requestCode) {
2605            case REQUEST_CODE_CREATE_SLIDESHOW:
2606                if (data != null) {
2607                    WorkingMessage newMessage = WorkingMessage.load(this, data.getData());
2608                    if (newMessage != null) {
2609                        mWorkingMessage = newMessage;
2610                        mWorkingMessage.setConversation(mConversation);
2611                        drawTopPanel(false);
2612                        updateSendButtonState();
2613                    }
2614                }
2615                break;
2616
2617            case REQUEST_CODE_TAKE_PICTURE: {
2618                // create a file based uri and pass to addImage(). We want to read the JPEG
2619                // data directly from file (using UriImage) instead of decoding it into a Bitmap,
2620                // which takes up too much memory and could easily lead to OOM.
2621                File file = new File(TempFileProvider.getScrapPath());
2622                Uri uri = Uri.fromFile(file);
2623                addImage(uri, false);
2624                break;
2625            }
2626
2627            case REQUEST_CODE_ATTACH_IMAGE: {
2628                addImage(data.getData(), false);
2629                break;
2630            }
2631
2632            case REQUEST_CODE_TAKE_VIDEO:
2633                Uri videoUri = TempFileProvider.renameScrapFile(".3gp", null);
2634                addVideo(videoUri, false);      // can handle null videoUri
2635                break;
2636
2637            case REQUEST_CODE_ATTACH_VIDEO:
2638                addVideo(data.getData(), false);
2639                break;
2640
2641            case REQUEST_CODE_ATTACH_SOUND: {
2642                Uri uri = (Uri) data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
2643                if (Settings.System.DEFAULT_RINGTONE_URI.equals(uri)) {
2644                    break;
2645                }
2646                addAudio(uri);
2647                break;
2648            }
2649
2650            case REQUEST_CODE_RECORD_SOUND:
2651                addAudio(data.getData());
2652                break;
2653
2654            case REQUEST_CODE_ECM_EXIT_DIALOG:
2655                boolean outOfEmergencyMode = data.getBooleanExtra(EXIT_ECM_RESULT, false);
2656                if (outOfEmergencyMode) {
2657                    sendMessage(false);
2658                }
2659                break;
2660
2661            case REQUEST_CODE_ADD_CONTACT:
2662                // The user just added a new contact. We saved the contact info in
2663                // mAddContactIntent. Get the contact and force our cached contact to
2664                // get reloaded with the new info (such as contact name). After the
2665                // contact is reloaded, the function onUpdate() in this file will get called
2666                // and it will update the title bar, etc.
2667                if (mAddContactIntent != null) {
2668                    String address =
2669                        mAddContactIntent.getStringExtra(ContactsContract.Intents.Insert.EMAIL);
2670                    if (address == null) {
2671                        address =
2672                            mAddContactIntent.getStringExtra(ContactsContract.Intents.Insert.PHONE);
2673                    }
2674                    if (address != null) {
2675                        Contact contact = Contact.get(address, false);
2676                        if (contact != null) {
2677                            contact.reload();
2678                        }
2679                    }
2680                }
2681                break;
2682
2683            case REQUEST_CODE_PICK:
2684                processPickResult(data);
2685                break;
2686            default:
2687                // TODO
2688                break;
2689        }
2690    }
2691
2692    private void processPickResult(final Intent data) {
2693        // The EXTRA_PHONE_URIS stores the phone's urls that were selected by user in the
2694        // multiple phone picker.
2695        final Parcelable[] uris =
2696            data.getParcelableArrayExtra(Intents.EXTRA_PHONE_URIS);
2697
2698        final int recipientCount = uris != null ? uris.length : 0;
2699
2700        final int recipientLimit = MmsConfig.getRecipientLimit();
2701        if (recipientLimit != Integer.MAX_VALUE && recipientCount > recipientLimit) {
2702            new AlertDialog.Builder(this)
2703                    .setTitle(R.string.pick_too_many_recipients)
2704                    .setIcon(android.R.drawable.ic_dialog_alert)
2705                    .setMessage(getString(R.string.too_many_recipients, recipientCount, recipientLimit))
2706                    .setPositiveButton(android.R.string.ok, null)
2707                    .create().show();
2708            return;
2709        }
2710
2711        final Handler handler = new Handler();
2712        final ProgressDialog progressDialog = new ProgressDialog(this);
2713        progressDialog.setTitle(getText(R.string.pick_too_many_recipients));
2714        progressDialog.setMessage(getText(R.string.adding_recipients));
2715        progressDialog.setIndeterminate(true);
2716        progressDialog.setCancelable(false);
2717
2718        final Runnable showProgress = new Runnable() {
2719            public void run() {
2720                progressDialog.show();
2721            }
2722        };
2723        // Only show the progress dialog if we can not finish off parsing the return data in 1s,
2724        // otherwise the dialog could flicker.
2725        handler.postDelayed(showProgress, 1000);
2726
2727        new Thread(new Runnable() {
2728            public void run() {
2729                final ContactList list;
2730                 try {
2731                    list = ContactList.blockingGetByUris(uris);
2732                } finally {
2733                    handler.removeCallbacks(showProgress);
2734                    progressDialog.dismiss();
2735                }
2736                // TODO: there is already code to update the contact header widget and recipients
2737                // editor if the contacts change. we can re-use that code.
2738                final Runnable populateWorker = new Runnable() {
2739                    public void run() {
2740                        mRecipientsEditor.populate(list);
2741                        updateTitle(list);
2742                    }
2743                };
2744                handler.post(populateWorker);
2745            }
2746        }).start();
2747    }
2748
2749    private final ResizeImageResultCallback mResizeImageCallback = new ResizeImageResultCallback() {
2750        // TODO: make this produce a Uri, that's what we want anyway
2751        public void onResizeResult(PduPart part, boolean append) {
2752            if (part == null) {
2753                handleAddAttachmentError(WorkingMessage.UNKNOWN_ERROR, R.string.type_picture);
2754                return;
2755            }
2756
2757            Context context = ComposeMessageActivity.this;
2758            PduPersister persister = PduPersister.getPduPersister(context);
2759            int result;
2760
2761            Uri messageUri = mWorkingMessage.saveAsMms(true);
2762            if (messageUri == null) {
2763                result = WorkingMessage.UNKNOWN_ERROR;
2764            } else {
2765                try {
2766                    Uri dataUri = persister.persistPart(part, ContentUris.parseId(messageUri));
2767                    result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, dataUri, append);
2768                    if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2769                        log("ResizeImageResultCallback: dataUri=" + dataUri);
2770                    }
2771                } catch (MmsException e) {
2772                    result = WorkingMessage.UNKNOWN_ERROR;
2773                }
2774            }
2775
2776            handleAddAttachmentError(result, R.string.type_picture);
2777        }
2778    };
2779
2780    private void handleAddAttachmentError(final int error, final int mediaTypeStringId) {
2781        if (error == WorkingMessage.OK) {
2782            return;
2783        }
2784
2785        runOnUiThread(new Runnable() {
2786            public void run() {
2787                Resources res = getResources();
2788                String mediaType = res.getString(mediaTypeStringId);
2789                String title, message;
2790
2791                switch(error) {
2792                case WorkingMessage.UNKNOWN_ERROR:
2793                    message = res.getString(R.string.failed_to_add_media, mediaType);
2794                    Toast.makeText(ComposeMessageActivity.this, message, Toast.LENGTH_SHORT).show();
2795                    return;
2796                case WorkingMessage.UNSUPPORTED_TYPE:
2797                    title = res.getString(R.string.unsupported_media_format, mediaType);
2798                    message = res.getString(R.string.select_different_media, mediaType);
2799                    break;
2800                case WorkingMessage.MESSAGE_SIZE_EXCEEDED:
2801                    title = res.getString(R.string.exceed_message_size_limitation, mediaType);
2802                    message = res.getString(R.string.failed_to_add_media, mediaType);
2803                    break;
2804                case WorkingMessage.IMAGE_TOO_LARGE:
2805                    title = res.getString(R.string.failed_to_resize_image);
2806                    message = res.getString(R.string.resize_image_error_information);
2807                    break;
2808                default:
2809                    throw new IllegalArgumentException("unknown error " + error);
2810                }
2811
2812                MessageUtils.showErrorDialog(ComposeMessageActivity.this, title, message);
2813            }
2814        });
2815    }
2816
2817    private void addImage(Uri uri, boolean append) {
2818        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2819            log("append=" + append + ", uri=" + uri);
2820        }
2821
2822        int result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, uri, append);
2823
2824        if (result == WorkingMessage.IMAGE_TOO_LARGE ||
2825            result == WorkingMessage.MESSAGE_SIZE_EXCEEDED) {
2826            if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
2827                log("resize image " + uri);
2828            }
2829            MessageUtils.resizeImageAsync(this,
2830                    uri, mAttachmentEditorHandler, mResizeImageCallback, append);
2831            return;
2832        }
2833        handleAddAttachmentError(result, R.string.type_picture);
2834    }
2835
2836    private void addVideo(Uri uri, boolean append) {
2837        if (uri != null) {
2838            int result = mWorkingMessage.setAttachment(WorkingMessage.VIDEO, uri, append);
2839            handleAddAttachmentError(result, R.string.type_video);
2840        }
2841    }
2842
2843    private void addAudio(Uri uri) {
2844        int result = mWorkingMessage.setAttachment(WorkingMessage.AUDIO, uri, false);
2845        handleAddAttachmentError(result, R.string.type_audio);
2846    }
2847
2848    private boolean handleForwardedMessage() {
2849        Intent intent = getIntent();
2850
2851        // If this is a forwarded message, it will have an Intent extra
2852        // indicating so.  If not, bail out.
2853        if (intent.getBooleanExtra("forwarded_message", false) == false) {
2854            return false;
2855        }
2856
2857        Uri uri = intent.getParcelableExtra("msg_uri");
2858
2859        if (Log.isLoggable(LogTag.APP, Log.DEBUG)) {
2860            log("" + uri);
2861        }
2862
2863        if (uri != null) {
2864            mWorkingMessage = WorkingMessage.load(this, uri);
2865            mWorkingMessage.setSubject(intent.getStringExtra("subject"), false);
2866        } else {
2867            mWorkingMessage.setText(intent.getStringExtra("sms_body"));
2868        }
2869
2870        // let's clear the message thread for forwarded messages
2871        mMsgListAdapter.changeCursor(null);
2872
2873        return true;
2874    }
2875
2876    private boolean handleSendIntent(Intent intent) {
2877        Bundle extras = intent.getExtras();
2878        if (extras == null) {
2879            return false;
2880        }
2881
2882        final String mimeType = intent.getType();
2883        String action = intent.getAction();
2884        if (Intent.ACTION_SEND.equals(action)) {
2885            if (extras.containsKey(Intent.EXTRA_STREAM)) {
2886                Uri uri = (Uri)extras.getParcelable(Intent.EXTRA_STREAM);
2887                addAttachment(mimeType, uri, false);
2888                return true;
2889            } else if (extras.containsKey(Intent.EXTRA_TEXT)) {
2890                mWorkingMessage.setText(extras.getString(Intent.EXTRA_TEXT));
2891                return true;
2892            }
2893        } else if (Intent.ACTION_SEND_MULTIPLE.equals(action) &&
2894                extras.containsKey(Intent.EXTRA_STREAM)) {
2895            SlideshowModel slideShow = mWorkingMessage.getSlideshow();
2896            final ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
2897            int currentSlideCount = slideShow != null ? slideShow.size() : 0;
2898            int importCount = uris.size();
2899            if (importCount + currentSlideCount > SlideshowEditor.MAX_SLIDE_NUM) {
2900                importCount = Math.min(SlideshowEditor.MAX_SLIDE_NUM - currentSlideCount,
2901                        importCount);
2902                Toast.makeText(ComposeMessageActivity.this,
2903                        getString(R.string.too_many_attachments,
2904                                SlideshowEditor.MAX_SLIDE_NUM, importCount),
2905                                Toast.LENGTH_LONG).show();
2906            }
2907
2908            // Attach all the pictures/videos off of the UI thread.
2909            // Show a progress alert if adding all the slides hasn't finished
2910            // within one second.
2911            // Stash the runnable for showing it away so we can cancel
2912            // it later if adding completes ahead of the deadline.
2913            final AlertDialog dialog = new AlertDialog.Builder(ComposeMessageActivity.this)
2914                .setIcon(android.R.drawable.ic_dialog_alert)
2915                .setTitle(R.string.adding_attachments_title)
2916                .setMessage(R.string.adding_attachments)
2917                .create();
2918            final Runnable showProgress = new Runnable() {
2919                public void run() {
2920                    dialog.show();
2921                }
2922            };
2923            // Schedule it for one second from now.
2924            mAttachmentEditorHandler.postDelayed(showProgress, 1000);
2925
2926            final int numberToImport = importCount;
2927            new Thread(new Runnable() {
2928                public void run() {
2929                    for (int i = 0; i < numberToImport; i++) {
2930                        Parcelable uri = uris.get(i);
2931                        addAttachment(mimeType, (Uri) uri, true);
2932                    }
2933                    // Cancel pending show of the progress alert if necessary.
2934                    mAttachmentEditorHandler.removeCallbacks(showProgress);
2935                    dialog.dismiss();
2936                }
2937            }, "addAttachment").start();
2938            return true;
2939        }
2940
2941        return false;
2942    }
2943
2944    // mVideoUri will look like this: content://media/external/video/media
2945    private static final String mVideoUri = Video.Media.getContentUri("external").toString();
2946    // mImageUri will look like this: content://media/external/images/media
2947    private static final String mImageUri = Images.Media.getContentUri("external").toString();
2948
2949    private void addAttachment(String type, Uri uri, boolean append) {
2950        if (uri != null) {
2951            // When we're handling Intent.ACTION_SEND_MULTIPLE, the passed in items can be
2952            // videos, and/or images, and/or some other unknown types we don't handle. When
2953            // a single attachment is "shared" the type will specify an image or video. When
2954            // there are multiple types, the type passed in is "*/*". In that case, we've got
2955            // to look at the uri to figure out if it is an image or video.
2956            boolean wildcard = "*/*".equals(type);
2957            if (type.startsWith("image/") || (wildcard && uri.toString().startsWith(mImageUri))) {
2958                addImage(uri, append);
2959            } else if (type.startsWith("video/") ||
2960                    (wildcard && uri.toString().startsWith(mVideoUri))) {
2961                addVideo(uri, append);
2962            }
2963        }
2964    }
2965
2966    private String getResourcesString(int id, String mediaName) {
2967        Resources r = getResources();
2968        return r.getString(id, mediaName);
2969    }
2970
2971    private void drawBottomPanel() {
2972        // Reset the counter for text editor.
2973        resetCounter();
2974
2975        if (mWorkingMessage.hasSlideshow()) {
2976            mBottomPanel.setVisibility(View.GONE);
2977            mAttachmentEditor.requestFocus();
2978            return;
2979        }
2980
2981        mBottomPanel.setVisibility(View.VISIBLE);
2982
2983        CharSequence text = mWorkingMessage.getText();
2984
2985        // TextView.setTextKeepState() doesn't like null input.
2986        if (text != null) {
2987            mTextEditor.setTextKeepState(text);
2988        } else {
2989            mTextEditor.setText("");
2990        }
2991    }
2992
2993    private void drawTopPanel(boolean showSubjectEditor) {
2994        boolean showingAttachment = mAttachmentEditor.update(mWorkingMessage);
2995        mAttachmentEditorScrollView.setVisibility(showingAttachment ? View.VISIBLE : View.GONE);
2996        showSubjectEditor(showSubjectEditor || mWorkingMessage.hasSubject());
2997    }
2998
2999    //==========================================================
3000    // Interface methods
3001    //==========================================================
3002
3003    public void onClick(View v) {
3004        if ((v == mSendButtonSms || v == mSendButtonMms) && isPreparedForSending()) {
3005            confirmSendMessageIfNeeded();
3006        } else if ((v == mRecipientsPicker)) {
3007            launchMultiplePhonePicker();
3008        }
3009    }
3010
3011    private void launchMultiplePhonePicker() {
3012        Intent intent = new Intent(Intents.ACTION_GET_MULTIPLE_PHONES);
3013        intent.addCategory("android.intent.category.DEFAULT");
3014        intent.setType(Phone.CONTENT_TYPE);
3015        // We have to wait for the constructing complete.
3016        ContactList contacts = mRecipientsEditor.constructContactsFromInput(true);
3017        int recipientsCount = 0;
3018        int urisCount = 0;
3019        Uri[] uris = new Uri[contacts.size()];
3020        urisCount = 0;
3021        for (Contact contact : contacts) {
3022            if (Contact.CONTACT_METHOD_TYPE_PHONE == contact.getContactMethodType()) {
3023                    uris[urisCount++] = contact.getPhoneUri();
3024            }
3025        }
3026        if (urisCount > 0) {
3027            intent.putExtra(Intents.EXTRA_PHONE_URIS, uris);
3028        }
3029        startActivityForResult(intent, REQUEST_CODE_PICK);
3030    }
3031
3032    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
3033        if (event != null) {
3034            // if shift key is down, then we want to insert the '\n' char in the TextView;
3035            // otherwise, the default action is to send the message.
3036            if (!event.isShiftPressed()) {
3037                if (isPreparedForSending()) {
3038                    confirmSendMessageIfNeeded();
3039                }
3040                return true;
3041            }
3042            return false;
3043        }
3044
3045        if (isPreparedForSending()) {
3046            confirmSendMessageIfNeeded();
3047        }
3048        return true;
3049    }
3050
3051    private final TextWatcher mTextEditorWatcher = new TextWatcher() {
3052        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3053        }
3054
3055        public void onTextChanged(CharSequence s, int start, int before, int count) {
3056            // This is a workaround for bug 1609057.  Since onUserInteraction() is
3057            // not called when the user touches the soft keyboard, we pretend it was
3058            // called when textfields changes.  This should be removed when the bug
3059            // is fixed.
3060            onUserInteraction();
3061
3062            mWorkingMessage.setText(s);
3063
3064            updateSendButtonState();
3065
3066            updateCounter(s, start, before, count);
3067
3068            ensureCorrectButtonHeight();
3069        }
3070
3071        public void afterTextChanged(Editable s) {
3072        }
3073    };
3074
3075    /**
3076     * Ensures that if the text edit box extends past two lines then the
3077     * button will be shifted up to allow enough space for the character
3078     * counter string to be placed beneath it.
3079     */
3080    private void ensureCorrectButtonHeight() {
3081        int currentTextLines = mTextEditor.getLineCount();
3082        if (currentTextLines <= 2) {
3083            mTextCounter.setVisibility(View.GONE);
3084        }
3085        else if (currentTextLines > 2 && mTextCounter.getVisibility() == View.GONE) {
3086            // Making the counter invisible ensures that it is used to correctly
3087            // calculate the position of the send button even if we choose not to
3088            // display the text.
3089            mTextCounter.setVisibility(View.INVISIBLE);
3090        }
3091    }
3092
3093    private final TextWatcher mSubjectEditorWatcher = new TextWatcher() {
3094        public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
3095
3096        public void onTextChanged(CharSequence s, int start, int before, int count) {
3097            mWorkingMessage.setSubject(s, true);
3098        }
3099
3100        public void afterTextChanged(Editable s) { }
3101    };
3102
3103    //==========================================================
3104    // Private methods
3105    //==========================================================
3106
3107    /**
3108     * Initialize all UI elements from resources.
3109     */
3110    private void initResourceRefs() {
3111        mMsgListView = (MessageListView) findViewById(R.id.history);
3112        mMsgListView.setDivider(null);      // no divider so we look like IM conversation.
3113
3114        // called to enable us to show some padding between the message list and the
3115        // input field but when the message list is scrolled that padding area is filled
3116        // in with message content
3117        mMsgListView.setClipToPadding(false);
3118
3119        // turn off children clipping because we draw the border outside of our own
3120        // bounds at the bottom.  The background is also drawn in code to avoid drawing
3121        // the top edge.
3122        mMsgListView.setClipChildren(false);
3123
3124        mBottomPanel = findViewById(R.id.bottom_panel);
3125        mTextEditor = (EditText) findViewById(R.id.embedded_text_editor);
3126        mTextEditor.setOnEditorActionListener(this);
3127        mTextEditor.addTextChangedListener(mTextEditorWatcher);
3128        mTextEditor.setFilters(new InputFilter[] {
3129                new LengthFilter(MmsConfig.getMaxTextLimit())});
3130        mTextCounter = (TextView) findViewById(R.id.text_counter);
3131        mSendButtonMms = (TextView) findViewById(R.id.send_button_mms);
3132        mSendButtonSms = (ImageButton) findViewById(R.id.send_button_sms);
3133        mSendButtonMms.setOnClickListener(this);
3134        mSendButtonSms.setOnClickListener(this);
3135        mTopPanel = findViewById(R.id.recipients_subject_linear);
3136        mTopPanel.setFocusable(false);
3137        mAttachmentEditor = (AttachmentEditor) findViewById(R.id.attachment_editor);
3138        mAttachmentEditor.setHandler(mAttachmentEditorHandler);
3139        mAttachmentEditorScrollView = findViewById(R.id.attachment_editor_scroll_view);
3140    }
3141
3142    private void confirmDeleteDialog(OnClickListener listener, boolean locked) {
3143        AlertDialog.Builder builder = new AlertDialog.Builder(this);
3144        builder.setTitle(locked ? R.string.confirm_dialog_locked_title :
3145            R.string.confirm_dialog_title);
3146        builder.setIcon(android.R.drawable.ic_dialog_alert);
3147        builder.setCancelable(true);
3148        builder.setMessage(locked ? R.string.confirm_delete_locked_message :
3149                    R.string.confirm_delete_message);
3150        builder.setPositiveButton(R.string.delete, listener);
3151        builder.setNegativeButton(R.string.no, null);
3152        builder.show();
3153    }
3154
3155    void undeliveredMessageDialog(long date) {
3156        String body;
3157        LinearLayout dialog = (LinearLayout) LayoutInflater.from(this).inflate(
3158                R.layout.retry_sending_dialog, null);
3159
3160        if (date >= 0) {
3161            body = getString(R.string.undelivered_msg_dialog_body,
3162                    MessageUtils.formatTimeStampString(this, date));
3163        } else {
3164            // FIXME: we can not get sms retry time.
3165            body = getString(R.string.undelivered_sms_dialog_body);
3166        }
3167
3168        ((TextView) dialog.findViewById(R.id.body_text_view)).setText(body);
3169
3170        Toast undeliveredDialog = new Toast(this);
3171        undeliveredDialog.setView(dialog);
3172        undeliveredDialog.setDuration(Toast.LENGTH_LONG);
3173        undeliveredDialog.show();
3174    }
3175
3176    private void startMsgListQuery() {
3177        Uri conversationUri = mConversation.getUri();
3178
3179        if (conversationUri == null) {
3180            log("##### startMsgListQuery: conversationUri is null, bail!");
3181            return;
3182        }
3183
3184        long threadId = mConversation.getThreadId();
3185        if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3186            log("startMsgListQuery for " + conversationUri + ", threadId=" + threadId);
3187        }
3188
3189        // Cancel any pending queries
3190        mBackgroundQueryHandler.cancelOperation(MESSAGE_LIST_QUERY_TOKEN);
3191        try {
3192            // Kick off the new query
3193            mBackgroundQueryHandler.startQuery(
3194                    MESSAGE_LIST_QUERY_TOKEN,
3195                    threadId /* cookie */,
3196                    conversationUri,
3197                    PROJECTION,
3198                    null, null, null);
3199        } catch (SQLiteException e) {
3200            SqliteWrapper.checkSQLiteException(this, e);
3201        }
3202    }
3203
3204    private void initMessageList() {
3205        if (mMsgListAdapter != null) {
3206            return;
3207        }
3208
3209        String highlightString = getIntent().getStringExtra("highlight");
3210        Pattern highlight = highlightString == null
3211            ? null
3212            : Pattern.compile("\\b" + Pattern.quote(highlightString), Pattern.CASE_INSENSITIVE);
3213
3214        // Initialize the list adapter with a null cursor.
3215        mMsgListAdapter = new MessageListAdapter(this, null, mMsgListView, true, highlight);
3216        mMsgListAdapter.setOnDataSetChangedListener(mDataSetChangedListener);
3217        mMsgListAdapter.setMsgListItemHandler(mMessageListItemHandler);
3218        mMsgListView.setAdapter(mMsgListAdapter);
3219        mMsgListView.setItemsCanFocus(false);
3220        mMsgListView.setVisibility(View.VISIBLE);
3221        mMsgListView.setOnCreateContextMenuListener(mMsgListMenuCreateListener);
3222        mMsgListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
3223            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
3224                if (view != null) {
3225                    ((MessageListItem) view).onMessageListItemClick();
3226                }
3227            }
3228        });
3229    }
3230
3231    private void loadDraft() {
3232        if (mWorkingMessage.isWorthSaving()) {
3233            Log.w(TAG, "called with non-empty working message");
3234            return;
3235        }
3236
3237        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3238            log("call WorkingMessage.loadDraft");
3239        }
3240
3241        mWorkingMessage = WorkingMessage.loadDraft(this, mConversation);
3242    }
3243
3244    private void saveDraft(boolean isStopping) {
3245        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3246            LogTag.debug("saveDraft");
3247        }
3248        // TODO: Do something better here.  Maybe make discard() legal
3249        // to call twice and make isEmpty() return true if discarded
3250        // so it is caught in the clause above this one?
3251        if (mWorkingMessage.isDiscarded()) {
3252            return;
3253        }
3254
3255        if (!mWaitingForSubActivity &&
3256                ((!mWorkingMessage.isWorthSaving() && !isRecipientsEditorVisible()) ||
3257                recipientCount() == 0)) {
3258            if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3259                log("not worth saving, discard WorkingMessage and bail");
3260            }
3261            mWorkingMessage.discard();
3262            return;
3263        }
3264
3265        mWorkingMessage.saveDraft(isStopping);
3266
3267        if (mToastForDraftSave) {
3268            Toast.makeText(this, R.string.message_saved_as_draft,
3269                    Toast.LENGTH_SHORT).show();
3270        }
3271    }
3272
3273    private boolean isPreparedForSending() {
3274        int recipientCount = recipientCount();
3275
3276        return recipientCount > 0 && recipientCount <= MmsConfig.getRecipientLimit() &&
3277            (mWorkingMessage.hasAttachment() ||
3278                    mWorkingMessage.hasText() ||
3279                    mWorkingMessage.hasSubject());
3280    }
3281
3282    private int recipientCount() {
3283        int recipientCount;
3284
3285        // To avoid creating a bunch of invalid Contacts when the recipients
3286        // editor is in flux, we keep the recipients list empty.  So if the
3287        // recipients editor is showing, see if there is anything in it rather
3288        // than consulting the empty recipient list.
3289        if (isRecipientsEditorVisible()) {
3290            recipientCount = mRecipientsEditor.getRecipientCount();
3291        } else {
3292            recipientCount = getRecipients().size();
3293        }
3294        return recipientCount;
3295    }
3296
3297    private void sendMessage(boolean bCheckEcmMode) {
3298        if (bCheckEcmMode) {
3299            // TODO: expose this in telephony layer for SDK build
3300            String inEcm = SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE);
3301            if (Boolean.parseBoolean(inEcm)) {
3302                try {
3303                    startActivityForResult(
3304                            new Intent(TelephonyIntents.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS, null),
3305                            REQUEST_CODE_ECM_EXIT_DIALOG);
3306                    return;
3307                } catch (ActivityNotFoundException e) {
3308                    // continue to send message
3309                    Log.e(TAG, "Cannot find EmergencyCallbackModeExitDialog", e);
3310                }
3311            }
3312        }
3313
3314        if (!mSendingMessage) {
3315            if (LogTag.SEVERE_WARNING) {
3316                String sendingRecipients = mConversation.getRecipients().serialize();
3317                if (!sendingRecipients.equals(mDebugRecipients)) {
3318                    String workingRecipients = mWorkingMessage.getWorkingRecipients();
3319                    if (!mDebugRecipients.equals(workingRecipients)) {
3320                        LogTag.warnPossibleRecipientMismatch("ComposeMessageActivity.sendMessage" +
3321                                " recipients in window: \"" +
3322                                mDebugRecipients + "\" differ from recipients from conv: \"" +
3323                                sendingRecipients + "\" and working recipients: " +
3324                                workingRecipients, this);
3325                    }
3326                }
3327                sanityCheckConversation();
3328            }
3329
3330            // send can change the recipients. Make sure we remove the listeners first and then add
3331            // them back once the recipient list has settled.
3332            removeRecipientsListeners();
3333
3334            mWorkingMessage.send(mDebugRecipients);
3335
3336            mSentMessage = true;
3337            mSendingMessage = true;
3338            addRecipientsListeners();
3339        }
3340        // But bail out if we are supposed to exit after the message is sent.
3341        if (mExitOnSent) {
3342            finish();
3343        }
3344    }
3345
3346    private void resetMessage() {
3347        if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3348            log("");
3349        }
3350
3351        // Make the attachment editor hide its view.
3352        mAttachmentEditor.hideView();
3353        mAttachmentEditorScrollView.setVisibility(View.GONE);
3354
3355        // Hide the subject editor.
3356        showSubjectEditor(false);
3357
3358        // Focus to the text editor.
3359        mTextEditor.requestFocus();
3360
3361        // We have to remove the text change listener while the text editor gets cleared and
3362        // we subsequently turn the message back into SMS. When the listener is listening while
3363        // doing the clearing, it's fighting to update its counts and itself try and turn
3364        // the message one way or the other.
3365        mTextEditor.removeTextChangedListener(mTextEditorWatcher);
3366
3367        // Clear the text box.
3368        TextKeyListener.clear(mTextEditor.getText());
3369
3370        mWorkingMessage.clearConversation(mConversation, false);
3371        mWorkingMessage = WorkingMessage.createEmpty(this);
3372        mWorkingMessage.setConversation(mConversation);
3373
3374        hideRecipientEditor();
3375        drawBottomPanel();
3376
3377        // "Or not", in this case.
3378        updateSendButtonState();
3379
3380        // Our changes are done. Let the listener respond to text changes once again.
3381        mTextEditor.addTextChangedListener(mTextEditorWatcher);
3382
3383        // Close the soft on-screen keyboard if we're in landscape mode so the user can see the
3384        // conversation.
3385        if (mIsLandscape) {
3386            InputMethodManager inputMethodManager =
3387                (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
3388
3389            inputMethodManager.hideSoftInputFromWindow(mTextEditor.getWindowToken(), 0);
3390        }
3391
3392        mLastRecipientCount = 0;
3393        mSendingMessage = false;
3394   }
3395
3396    private void updateSendButtonState() {
3397        boolean enable = false;
3398        if (isPreparedForSending()) {
3399            // When the type of attachment is slideshow, we should
3400            // also hide the 'Send' button since the slideshow view
3401            // already has a 'Send' button embedded.
3402            if (!mWorkingMessage.hasSlideshow()) {
3403                enable = true;
3404            } else {
3405                mAttachmentEditor.setCanSend(true);
3406            }
3407        } else if (null != mAttachmentEditor){
3408            mAttachmentEditor.setCanSend(false);
3409        }
3410
3411        View sendButton = showSmsOrMmsSendButton(mWorkingMessage.requiresMms());
3412        sendButton.setEnabled(enable);
3413        sendButton.setFocusable(enable);
3414    }
3415
3416    private long getMessageDate(Uri uri) {
3417        if (uri != null) {
3418            Cursor cursor = SqliteWrapper.query(this, mContentResolver,
3419                    uri, new String[] { Mms.DATE }, null, null, null);
3420            if (cursor != null) {
3421                try {
3422                    if ((cursor.getCount() == 1) && cursor.moveToFirst()) {
3423                        return cursor.getLong(0) * 1000L;
3424                    }
3425                } finally {
3426                    cursor.close();
3427                }
3428            }
3429        }
3430        return NO_DATE_FOR_DIALOG;
3431    }
3432
3433    private void initActivityState(Intent intent) {
3434        // If we have been passed a thread_id, use that to find our conversation.
3435        long threadId = intent.getLongExtra("thread_id", 0);
3436        if (threadId > 0) {
3437            if (LogTag.VERBOSE) log("get mConversation by threadId " + threadId);
3438            mConversation = Conversation.get(this, threadId, false);
3439        } else {
3440            Uri intentData = intent.getData();
3441            if (intentData != null) {
3442                // try to get a conversation based on the data URI passed to our intent.
3443                if (LogTag.VERBOSE) log("get mConversation by intentData " + intentData);
3444                mConversation = Conversation.get(this, intentData, false);
3445                mWorkingMessage.setText(getBody(intentData));
3446            } else {
3447                // special intent extra parameter to specify the address
3448                String address = intent.getStringExtra("address");
3449                if (!TextUtils.isEmpty(address)) {
3450                    if (LogTag.VERBOSE) log("get mConversation by address " + address);
3451                    mConversation = Conversation.get(this, ContactList.getByNumbers(address,
3452                            false /* don't block */, true /* replace number */), false);
3453                } else {
3454                    if (LogTag.VERBOSE) log("create new conversation");
3455                    mConversation = Conversation.createNew(this);
3456                }
3457            }
3458        }
3459        addRecipientsListeners();
3460
3461        mExitOnSent = intent.getBooleanExtra("exit_on_sent", false);
3462        if (intent.hasExtra("sms_body")) {
3463            mWorkingMessage.setText(intent.getStringExtra("sms_body"));
3464        }
3465        mWorkingMessage.setSubject(intent.getStringExtra("subject"), false);
3466    }
3467
3468    private void initFocus() {
3469        if (!mIsKeyboardOpen) {
3470            return;
3471        }
3472
3473        // If the recipients editor is visible, there is nothing in it,
3474        // and the text editor is not already focused, focus the
3475        // recipients editor.
3476        if (isRecipientsEditorVisible()
3477                && TextUtils.isEmpty(mRecipientsEditor.getText())
3478                && !mTextEditor.isFocused()) {
3479            mRecipientsEditor.requestFocus();
3480            return;
3481        }
3482
3483        // If we decided not to focus the recipients editor, focus the text editor.
3484        mTextEditor.requestFocus();
3485    }
3486
3487    private final MessageListAdapter.OnDataSetChangedListener
3488                    mDataSetChangedListener = new MessageListAdapter.OnDataSetChangedListener() {
3489        public void onDataSetChanged(MessageListAdapter adapter) {
3490            mPossiblePendingNotification = true;
3491        }
3492
3493        public void onContentChanged(MessageListAdapter adapter) {
3494            if (LogTag.VERBOSE) {
3495                log("MessageListAdapter.OnDataSetChangedListener.onContentChanged");
3496            }
3497            startMsgListQuery();
3498        }
3499    };
3500
3501    private void checkPendingNotification() {
3502        if (mPossiblePendingNotification && hasWindowFocus()) {
3503            mConversation.markAsRead();
3504            mPossiblePendingNotification = false;
3505        }
3506    }
3507
3508    private final class BackgroundQueryHandler extends AsyncQueryHandler {
3509        public BackgroundQueryHandler(ContentResolver contentResolver) {
3510            super(contentResolver);
3511        }
3512
3513        @Override
3514        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
3515            switch(token) {
3516                case MESSAGE_LIST_QUERY_TOKEN:
3517                    // check consistency between the query result and 'mConversation'
3518                    long tid = (Long) cookie;
3519
3520                    if (LogTag.VERBOSE) {
3521                        log("##### onQueryComplete: msg history result for threadId " + tid);
3522                    }
3523                    if (tid != mConversation.getThreadId()) {
3524                        log("onQueryComplete: msg history query result is for threadId " +
3525                                tid + ", but mConversation has threadId " +
3526                                mConversation.getThreadId() + " starting a new query");
3527                        startMsgListQuery();
3528                        return;
3529                    }
3530
3531                    // check consistency b/t mConversation & mWorkingMessage.mConversation
3532                    ComposeMessageActivity.this.sanityCheckConversation();
3533
3534                    int newSelectionPos = -1;
3535                    long targetMsgId = getIntent().getLongExtra("select_id", -1);
3536                    if (targetMsgId != -1) {
3537                        cursor.moveToPosition(-1);
3538                        while (cursor.moveToNext()) {
3539                            long msgId = cursor.getLong(COLUMN_ID);
3540                            if (msgId == targetMsgId) {
3541                                newSelectionPos = cursor.getPosition();
3542                                break;
3543                            }
3544                        }
3545                    }
3546
3547                    mMsgListAdapter.changeCursor(cursor);
3548                    if (newSelectionPos != -1) {
3549                        mMsgListView.setSelection(newSelectionPos);
3550                    }
3551                    // Adjust the conversation's message count to match reality. The
3552                    // conversation's message count is eventually used in
3553                    // WorkingMessage.clearConversation to determine whether to delete
3554                    // the conversation or not.
3555                    mConversation.setMessageCount(mMsgListAdapter.getCount());
3556
3557                    // Once we have completed the query for the message history, if
3558                    // there is nothing in the cursor and we are not composing a new
3559                    // message, we must be editing a draft in a new conversation (unless
3560                    // mSentMessage is true).
3561                    // Show the recipients editor to give the user a chance to add
3562                    // more people before the conversation begins.
3563                    if (cursor.getCount() == 0 && !isRecipientsEditorVisible() && !mSentMessage) {
3564                        initRecipientsEditor();
3565                    }
3566
3567                    // FIXME: freshing layout changes the focused view to an unexpected
3568                    // one, set it back to TextEditor forcely.
3569                    mTextEditor.requestFocus();
3570
3571                    mConversation.blockMarkAsRead(false);
3572                    return;
3573
3574                case ConversationList.HAVE_LOCKED_MESSAGES_TOKEN:
3575                    ArrayList<Long> threadIds = (ArrayList<Long>)cookie;
3576                    ConversationList.confirmDeleteThreadDialog(
3577                            new ConversationList.DeleteThreadListener(threadIds,
3578                                mBackgroundQueryHandler, ComposeMessageActivity.this),
3579                            threadIds,
3580                            cursor != null && cursor.getCount() > 0,
3581                            ComposeMessageActivity.this);
3582                    break;
3583            }
3584        }
3585
3586        @Override
3587        protected void onDeleteComplete(int token, Object cookie, int result) {
3588            switch(token) {
3589                case ConversationList.DELETE_CONVERSATION_TOKEN:
3590                    mConversation.setMessageCount(0);
3591                    // fall through
3592                case DELETE_MESSAGE_TOKEN:
3593                    // Update the notification for new messages since they
3594                    // may be deleted.
3595                    MessagingNotification.nonBlockingUpdateNewMessageIndicator(
3596                            ComposeMessageActivity.this, false, false);
3597                    // Update the notification for failed messages since they
3598                    // may be deleted.
3599                    updateSendFailedNotification();
3600                    break;
3601            }
3602            // If we're deleting the whole conversation, throw away
3603            // our current working message and bail.
3604            if (token == ConversationList.DELETE_CONVERSATION_TOKEN) {
3605                mWorkingMessage.discard();
3606
3607                // Rebuild the contacts cache now that a thread and its associated unique
3608                // recipients have been deleted.
3609                Contact.init(ComposeMessageActivity.this);
3610
3611                // Make sure the conversation cache reflects the threads in the DB.
3612                Conversation.init(ComposeMessageActivity.this);
3613                finish();
3614            }
3615        }
3616    }
3617
3618    private void showSmileyDialog() {
3619        if (mSmileyDialog == null) {
3620            int[] icons = SmileyParser.DEFAULT_SMILEY_RES_IDS;
3621            String[] names = getResources().getStringArray(
3622                    SmileyParser.DEFAULT_SMILEY_NAMES);
3623            final String[] texts = getResources().getStringArray(
3624                    SmileyParser.DEFAULT_SMILEY_TEXTS);
3625
3626            final int N = names.length;
3627
3628            List<Map<String, ?>> entries = new ArrayList<Map<String, ?>>();
3629            for (int i = 0; i < N; i++) {
3630                // We might have different ASCII for the same icon, skip it if
3631                // the icon is already added.
3632                boolean added = false;
3633                for (int j = 0; j < i; j++) {
3634                    if (icons[i] == icons[j]) {
3635                        added = true;
3636                        break;
3637                    }
3638                }
3639                if (!added) {
3640                    HashMap<String, Object> entry = new HashMap<String, Object>();
3641
3642                    entry. put("icon", icons[i]);
3643                    entry. put("name", names[i]);
3644                    entry.put("text", texts[i]);
3645
3646                    entries.add(entry);
3647                }
3648            }
3649
3650            final SimpleAdapter a = new SimpleAdapter(
3651                    this,
3652                    entries,
3653                    R.layout.smiley_menu_item,
3654                    new String[] {"icon", "name", "text"},
3655                    new int[] {R.id.smiley_icon, R.id.smiley_name, R.id.smiley_text});
3656            SimpleAdapter.ViewBinder viewBinder = new SimpleAdapter.ViewBinder() {
3657                public boolean setViewValue(View view, Object data, String textRepresentation) {
3658                    if (view instanceof ImageView) {
3659                        Drawable img = getResources().getDrawable((Integer)data);
3660                        ((ImageView)view).setImageDrawable(img);
3661                        return true;
3662                    }
3663                    return false;
3664                }
3665            };
3666            a.setViewBinder(viewBinder);
3667
3668            AlertDialog.Builder b = new AlertDialog.Builder(this);
3669
3670            b.setTitle(getString(R.string.menu_insert_smiley));
3671
3672            b.setCancelable(true);
3673            b.setAdapter(a, new DialogInterface.OnClickListener() {
3674                @SuppressWarnings("unchecked")
3675                public final void onClick(DialogInterface dialog, int which) {
3676                    HashMap<String, Object> item = (HashMap<String, Object>) a.getItem(which);
3677                    mTextEditor.append((String)item.get("text"));
3678
3679                    dialog.dismiss();
3680                }
3681            });
3682
3683            mSmileyDialog = b.create();
3684        }
3685
3686        mSmileyDialog.show();
3687    }
3688
3689    public void onUpdate(final Contact updated) {
3690        // Using an existing handler for the post, rather than conjuring up a new one.
3691        mMessageListItemHandler.post(new Runnable() {
3692            public void run() {
3693                ContactList recipients = isRecipientsEditorVisible() ?
3694                        mRecipientsEditor.constructContactsFromInput(false) : getRecipients();
3695                if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
3696                    log("[CMA] onUpdate contact updated: " + updated);
3697                    log("[CMA] onUpdate recipients: " + recipients);
3698                }
3699                updateTitle(recipients);
3700
3701                // The contact information for one (or more) of the recipients has changed.
3702                // Rebuild the message list so each MessageItem will get the last contact info.
3703                ComposeMessageActivity.this.mMsgListAdapter.notifyDataSetChanged();
3704
3705                if (mRecipientsEditor != null) {
3706                    mRecipientsEditor.populate(recipients);
3707                }
3708            }
3709        });
3710    }
3711
3712    private void addRecipientsListeners() {
3713        Contact.addListener(this);
3714    }
3715
3716    private void removeRecipientsListeners() {
3717        Contact.removeListener(this);
3718    }
3719
3720    public static Intent createIntent(Context context, long threadId) {
3721        Intent intent = new Intent(context, ComposeMessageActivity.class);
3722
3723        if (threadId > 0) {
3724            intent.setData(Conversation.getUri(threadId));
3725        }
3726
3727        return intent;
3728   }
3729
3730    private String getBody(Uri uri) {
3731        if (uri == null) {
3732            return null;
3733        }
3734        String urlStr = uri.getSchemeSpecificPart();
3735        if (!urlStr.contains("?")) {
3736            return null;
3737        }
3738        urlStr = urlStr.substring(urlStr.indexOf('?') + 1);
3739        String[] params = urlStr.split("&");
3740        for (String p : params) {
3741            if (p.startsWith("body=")) {
3742                try {
3743                    return URLDecoder.decode(p.substring(5), "UTF-8");
3744                } catch (UnsupportedEncodingException e) { }
3745            }
3746        }
3747        return null;
3748    }
3749}
3750