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