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