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