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