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