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 = LogTag.TAG; 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") log(String logMsg)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 editSlideshow()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 viewMmsMessageAttachment(final int requestCode)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 showMessageDetails(MessageItem msgItem)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 */ getMessageItem(String type, long msgId, boolean createFromCursorIfNotInCache)541 private MessageItem getMessageItem(String type, long msgId, 542 boolean createFromCursorIfNotInCache) { 543 return mMsgListAdapter.getCachedMessageItem(type, msgId, 544 createFromCursorIfNotInCache ? mMsgListAdapter.getCursor() : null); 545 } 546 isCursorValid()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 resetCounter()557 private void resetCounter() { 558 mTextCounter.setText(""); 559 mTextCounter.setVisibility(View.GONE); 560 } 561 updateCounter(CharSequence text, int start, int before, int count)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 startActivityForResult(Intent intent, int requestCode)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 showConvertToMmsToast()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 DeleteMessageListener(MessageItem messageItem)641 public DeleteMessageListener(MessageItem messageItem) { 642 mMessageItem = messageItem; 643 } 644 645 @Override onClick(DialogInterface dialog, int whichButton)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 onClick(DialogInterface dialog, int whichButton)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 onClick(DialogInterface dialog, int whichButton)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 onClick(DialogInterface dialog, int whichButton)694 public void onClick(DialogInterface dialog, int whichButton) { 695 if (isRecipientsEditorVisible()) { 696 mRecipientsEditor.requestFocus(); 697 } 698 dialog.dismiss(); 699 } 700 } 701 confirmSendMessageIfNeeded()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 checkForTooManyRecipients()799 private void checkForTooManyRecipients() { 800 final int recipientLimit = MmsConfig.getRecipientLimit(); 801 if (recipientLimit != Integer.MAX_VALUE && recipientLimit > 0) { 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 RecipientsMenuClickListener(Contact recipient)844 RecipientsMenuClickListener(Contact recipient) { 845 mRecipient = recipient; 846 } 847 848 @Override onMenuItemClick(MenuItem item)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 canAddToContacts(Contact contact)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 isSpecialChar(char c)895 private boolean isSpecialChar(char c) { 896 return c == '*' || c == '%' || c == '$'; 897 } 898 addPositionBasedMenuItems(ContextMenu menu, View v, ContextMenuInfo menuInfo)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 getSelectedUriFromMessageList(ListView listView, int position)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 addUriSpecificMenuItems(ContextMenu menu, View v, int position)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 addCallAndContactMenuItems( ContextMenu menu, MsgListMenuClickListener l, MessageItem msgItem)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 getContactUriForEmail(String emailAddress)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 getContactUriForPhoneNumber(String phoneNumber)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 editMessageItem(MessageItem msgItem)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 editSmsMessageItem(MessageItem msgItem)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 editMmsMessageItem(MessageItem msgItem)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 copyToClipboard(String str)1211 private void copyToClipboard(String str) { 1212 ClipboardManager clipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); 1213 clipboard.setPrimaryClip(ClipData.newPlainText(null, str)); 1214 } 1215 forwardMessage(final MessageItem msgItem)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 MsgListMenuClickListener(MessageItem msgItem)1297 public MsgListMenuClickListener(MessageItem msgItem) { 1298 mMsgItem = msgItem; 1299 } 1300 1301 @Override onMenuItemClick(MenuItem item)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 lockMessage(MessageItem msgItem, boolean locked)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 */ haveSomethingToCopyToSDCard(long msgId)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 */ saveRingtone(long msgId)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 */ isDrmRingtoneWithRights(long msgId)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 */ isForwardable(long msgId)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 getDrmMimeMenuStringRsrc(long msgId)1520 private int getDrmMimeMenuStringRsrc(long msgId) { 1521 if (isDrmRingtoneWithRights(msgId)) { 1522 return R.string.save_ringtone; 1523 } 1524 return 0; 1525 } 1526 getDrmMimeSavedStringRsrc(long msgId, boolean success)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 */ copyMedia(long msgId)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 copyPart(PduPart part, String fallback)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 getUniqueDestination(String base, String extension)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 showDeliveryReport(long messageId, String type)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 getRecipients()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 updateTitle(ContactList list)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. initRecipientsEditor()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.setText(null); 1796 mRecipientsEditor.populate(recipients); 1797 mRecipientsEditor.setOnCreateContextMenuListener(mRecipientsMenuCreateListener); 1798 mRecipientsEditor.addTextChangedListener(mRecipientsWatcher); 1799 // TODO : Remove the max length limitation due to the multiple phone picker is added and the 1800 // user is able to select a large number of recipients from the Contacts. The coming 1801 // potential issue is that it is hard for user to edit a recipient from hundred of 1802 // recipients in the editor box. We may redesign the editor box UI for this use case. 1803 // mRecipientsEditor.setFilters(new InputFilter[] { 1804 // new InputFilter.LengthFilter(RECIPIENTS_MAX_LENGTH) }); 1805 1806 mRecipientsEditor.setOnSelectChipRunnable(new Runnable() { 1807 @Override 1808 public void run() { 1809 // After the user selects an item in the pop-up contacts list, move the 1810 // focus to the text editor if there is only one recipient. This helps 1811 // the common case of selecting one recipient and then typing a message, 1812 // but avoids annoying a user who is trying to add five recipients and 1813 // keeps having focus stolen away. 1814 if (mRecipientsEditor.getRecipientCount() == 1) { 1815 // if we're in extract mode then don't request focus 1816 final InputMethodManager inputManager = (InputMethodManager) 1817 getSystemService(Context.INPUT_METHOD_SERVICE); 1818 if (inputManager == null || !inputManager.isFullscreenMode()) { 1819 mTextEditor.requestFocus(); 1820 } 1821 } 1822 } 1823 }); 1824 1825 mRecipientsEditor.setOnFocusChangeListener(new View.OnFocusChangeListener() { 1826 @Override 1827 public void onFocusChange(View v, boolean hasFocus) { 1828 if (!hasFocus) { 1829 RecipientsEditor editor = (RecipientsEditor) v; 1830 ContactList contacts = editor.constructContactsFromInput(false); 1831 updateTitle(contacts); 1832 } 1833 } 1834 }); 1835 1836 PhoneNumberFormatter.setPhoneNumberFormattingTextWatcher(this, mRecipientsEditor); 1837 1838 mTopPanel.setVisibility(View.VISIBLE); 1839 } 1840 1841 //========================================================== 1842 // Activity methods 1843 //========================================================== 1844 cancelFailedToDeliverNotification(Intent intent, Context context)1845 public static boolean cancelFailedToDeliverNotification(Intent intent, Context context) { 1846 if (MessagingNotification.isFailedToDeliver(intent)) { 1847 // Cancel any failed message notifications 1848 MessagingNotification.cancelNotification(context, 1849 MessagingNotification.MESSAGE_FAILED_NOTIFICATION_ID); 1850 return true; 1851 } 1852 return false; 1853 } 1854 cancelFailedDownloadNotification(Intent intent, Context context)1855 public static boolean cancelFailedDownloadNotification(Intent intent, Context context) { 1856 if (MessagingNotification.isFailedToDownload(intent)) { 1857 // Cancel any failed download notifications 1858 MessagingNotification.cancelNotification(context, 1859 MessagingNotification.DOWNLOAD_FAILED_NOTIFICATION_ID); 1860 return true; 1861 } 1862 return false; 1863 } 1864 1865 @Override onCreate(Bundle savedInstanceState)1866 protected void onCreate(Bundle savedInstanceState) { 1867 mIsSmsEnabled = MmsConfig.isSmsEnabled(this); 1868 super.onCreate(savedInstanceState); 1869 1870 resetConfiguration(getResources().getConfiguration()); 1871 1872 setContentView(R.layout.compose_message_activity); 1873 setProgressBarVisibility(false); 1874 1875 // Initialize members for UI elements. 1876 initResourceRefs(); 1877 1878 mContentResolver = getContentResolver(); 1879 mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver); 1880 1881 initialize(savedInstanceState, 0); 1882 1883 if (TRACE) { 1884 android.os.Debug.startMethodTracing("compose"); 1885 } 1886 } 1887 showSubjectEditor(boolean show)1888 private void showSubjectEditor(boolean show) { 1889 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1890 log("" + show); 1891 } 1892 1893 if (mSubjectTextEditor == null) { 1894 // Don't bother to initialize the subject editor if 1895 // we're just going to hide it. 1896 if (show == false) { 1897 return; 1898 } 1899 mSubjectTextEditor = (EditText)findViewById(R.id.subject); 1900 mSubjectTextEditor.setFilters(new InputFilter[] { 1901 new LengthFilter(MmsConfig.getMaxSubjectLength())}); 1902 } 1903 1904 mSubjectTextEditor.setOnKeyListener(show ? mSubjectKeyListener : null); 1905 1906 if (show) { 1907 mSubjectTextEditor.addTextChangedListener(mSubjectEditorWatcher); 1908 } else { 1909 mSubjectTextEditor.removeTextChangedListener(mSubjectEditorWatcher); 1910 } 1911 1912 mSubjectTextEditor.setText(mWorkingMessage.getSubject()); 1913 mSubjectTextEditor.setVisibility(show ? View.VISIBLE : View.GONE); 1914 hideOrShowTopPanel(); 1915 } 1916 hideOrShowTopPanel()1917 private void hideOrShowTopPanel() { 1918 boolean anySubViewsVisible = (isSubjectEditorVisible() || isRecipientsEditorVisible()); 1919 mTopPanel.setVisibility(anySubViewsVisible ? View.VISIBLE : View.GONE); 1920 } 1921 initialize(Bundle savedInstanceState, long originalThreadId)1922 public void initialize(Bundle savedInstanceState, long originalThreadId) { 1923 // Create a new empty working message. 1924 mWorkingMessage = WorkingMessage.createEmpty(this); 1925 1926 // Read parameters or previously saved state of this activity. This will load a new 1927 // mConversation 1928 initActivityState(savedInstanceState); 1929 1930 if (LogTag.SEVERE_WARNING && originalThreadId != 0 && 1931 originalThreadId == mConversation.getThreadId()) { 1932 LogTag.warnPossibleRecipientMismatch("ComposeMessageActivity.initialize: " + 1933 " threadId didn't change from: " + originalThreadId, this); 1934 } 1935 1936 log("savedInstanceState = " + savedInstanceState + 1937 " intent = " + getIntent() + 1938 " mConversation = " + mConversation); 1939 1940 if (cancelFailedToDeliverNotification(getIntent(), this)) { 1941 // Show a pop-up dialog to inform user the message was 1942 // failed to deliver. 1943 undeliveredMessageDialog(getMessageDate(null)); 1944 } 1945 cancelFailedDownloadNotification(getIntent(), this); 1946 1947 // Set up the message history ListAdapter 1948 initMessageList(); 1949 1950 mShouldLoadDraft = true; 1951 1952 // Load the draft for this thread, if we aren't already handling 1953 // existing data, such as a shared picture or forwarded message. 1954 boolean isForwardedMessage = false; 1955 // We don't attempt to handle the Intent.ACTION_SEND when saveInstanceState is non-null. 1956 // saveInstanceState is non-null when this activity is killed. In that case, we already 1957 // handled the attachment or the send, so we don't try and parse the intent again. 1958 if (savedInstanceState == null && (handleSendIntent() || handleForwardedMessage())) { 1959 mShouldLoadDraft = false; 1960 } 1961 1962 // Let the working message know what conversation it belongs to 1963 mWorkingMessage.setConversation(mConversation); 1964 1965 // Show the recipients editor if we don't have a valid thread. Hide it otherwise. 1966 if (mConversation.getThreadId() <= 0) { 1967 // Hide the recipients editor so the call to initRecipientsEditor won't get 1968 // short-circuited. 1969 hideRecipientEditor(); 1970 initRecipientsEditor(); 1971 } else { 1972 hideRecipientEditor(); 1973 } 1974 1975 updateSendButtonState(); 1976 1977 drawTopPanel(false); 1978 if (!mShouldLoadDraft) { 1979 // We're not loading a draft, so we can draw the bottom panel immediately. 1980 drawBottomPanel(); 1981 } 1982 1983 onKeyboardStateChanged(); 1984 1985 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1986 log("update title, mConversation=" + mConversation.toString()); 1987 } 1988 1989 updateTitle(mConversation.getRecipients()); 1990 1991 if (isForwardedMessage && isRecipientsEditorVisible()) { 1992 // The user is forwarding the message to someone. Put the focus on the 1993 // recipient editor rather than in the message editor. 1994 mRecipientsEditor.requestFocus(); 1995 } 1996 1997 mMsgListAdapter.setIsGroupConversation(mConversation.getRecipients().size() > 1); 1998 } 1999 2000 @Override onNewIntent(Intent intent)2001 protected void onNewIntent(Intent intent) { 2002 super.onNewIntent(intent); 2003 2004 setIntent(intent); 2005 2006 Conversation conversation = null; 2007 mSentMessage = false; 2008 2009 // If we have been passed a thread_id, use that to find our 2010 // conversation. 2011 2012 // Note that originalThreadId might be zero but if this is a draft and we save the 2013 // draft, ensureThreadId gets called async from WorkingMessage.asyncUpdateDraftSmsMessage 2014 // the thread will get a threadId behind the UI thread's back. 2015 long originalThreadId = mConversation.getThreadId(); 2016 long threadId = intent.getLongExtra(THREAD_ID, 0); 2017 Uri intentUri = intent.getData(); 2018 2019 boolean sameThread = false; 2020 if (threadId > 0) { 2021 conversation = Conversation.get(this, threadId, false); 2022 } else { 2023 if (mConversation.getThreadId() == 0) { 2024 // We've got a draft. Make sure the working recipients are synched 2025 // to the conversation so when we compare conversations later in this function, 2026 // the compare will work. 2027 mWorkingMessage.syncWorkingRecipients(); 2028 } 2029 // Get the "real" conversation based on the intentUri. The intentUri might specify 2030 // the conversation by a phone number or by a thread id. We'll typically get a threadId 2031 // based uri when the user pulls down a notification while in ComposeMessageActivity and 2032 // we end up here in onNewIntent. mConversation can have a threadId of zero when we're 2033 // working on a draft. When a new message comes in for that same recipient, a 2034 // conversation will get created behind CMA's back when the message is inserted into 2035 // the database and the corresponding entry made in the threads table. The code should 2036 // use the real conversation as soon as it can rather than finding out the threadId 2037 // when sending with "ensureThreadId". 2038 conversation = Conversation.get(this, intentUri, false); 2039 } 2040 2041 if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 2042 log("onNewIntent: data=" + intentUri + ", thread_id extra is " + threadId + 2043 ", new conversation=" + conversation + ", mConversation=" + mConversation); 2044 } 2045 2046 // this is probably paranoid to compare both thread_ids and recipient lists, 2047 // but we want to make double sure because this is a last minute fix for Froyo 2048 // and the previous code checked thread ids only. 2049 // (we cannot just compare thread ids because there is a case where mConversation 2050 // has a stale/obsolete thread id (=1) that could collide against the new thread_id(=1), 2051 // even though the recipient lists are different) 2052 sameThread = ((conversation.getThreadId() == mConversation.getThreadId() || 2053 mConversation.getThreadId() == 0) && 2054 conversation.equals(mConversation)); 2055 2056 if (sameThread) { 2057 log("onNewIntent: same conversation"); 2058 if (mConversation.getThreadId() == 0) { 2059 mConversation = conversation; 2060 mWorkingMessage.setConversation(mConversation); 2061 updateThreadIdIfRunning(); 2062 invalidateOptionsMenu(); 2063 } 2064 } else { 2065 if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 2066 log("onNewIntent: different conversation"); 2067 } 2068 saveDraft(false); // if we've got a draft, save it first 2069 2070 initialize(null, originalThreadId); 2071 } 2072 loadMessagesAndDraft(0); 2073 } 2074 sanityCheckConversation()2075 private void sanityCheckConversation() { 2076 if (mWorkingMessage.getConversation() != mConversation) { 2077 LogTag.warnPossibleRecipientMismatch( 2078 "ComposeMessageActivity: mWorkingMessage.mConversation=" + 2079 mWorkingMessage.getConversation() + ", mConversation=" + 2080 mConversation + ", MISMATCH!", this); 2081 } 2082 } 2083 2084 @Override onRestart()2085 protected void onRestart() { 2086 super.onRestart(); 2087 2088 // hide the compose panel to reduce jank when re-entering this activity. 2089 // if we don't hide it here, the compose panel will flash before the keyboard shows 2090 // (when keyboard is suppose to be shown). 2091 hideBottomPanel(); 2092 2093 if (mWorkingMessage.isDiscarded()) { 2094 // If the message isn't worth saving, don't resurrect it. Doing so can lead to 2095 // a situation where a new incoming message gets the old thread id of the discarded 2096 // draft. This activity can end up displaying the recipients of the old message with 2097 // the contents of the new message. Recognize that dangerous situation and bail out 2098 // to the ConversationList where the user can enter this in a clean manner. 2099 if (mWorkingMessage.isWorthSaving()) { 2100 if (LogTag.VERBOSE) { 2101 log("onRestart: mWorkingMessage.unDiscard()"); 2102 } 2103 mWorkingMessage.unDiscard(); // it was discarded in onStop(). 2104 2105 sanityCheckConversation(); 2106 } else if (isRecipientsEditorVisible() && recipientCount() > 0) { 2107 if (LogTag.VERBOSE) { 2108 log("onRestart: goToConversationList"); 2109 } 2110 goToConversationList(); 2111 } 2112 } 2113 } 2114 2115 @Override onStart()2116 protected void onStart() { 2117 super.onStart(); 2118 boolean isSmsEnabled = MmsConfig.isSmsEnabled(this); 2119 if (isSmsEnabled != mIsSmsEnabled) { 2120 mIsSmsEnabled = isSmsEnabled; 2121 invalidateOptionsMenu(); 2122 } 2123 2124 initFocus(); 2125 2126 // Register a BroadcastReceiver to listen on HTTP I/O process. 2127 registerReceiver(mHttpProgressReceiver, mHttpProgressFilter); 2128 2129 // figure out whether we need to show the keyboard or not. 2130 // if there is draft to be loaded for 'mConversation', we'll show the keyboard; 2131 // otherwise we hide the keyboard. In any event, delay loading 2132 // message history and draft (controlled by DEFER_LOADING_MESSAGES_AND_DRAFT). 2133 int mode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; 2134 2135 if (DraftCache.getInstance().hasDraft(mConversation.getThreadId())) { 2136 mode |= WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE; 2137 } else if (mConversation.getThreadId() <= 0) { 2138 // For composing a new message, bring up the softkeyboard so the user can 2139 // immediately enter recipients. This call won't do anything on devices with 2140 // a hard keyboard. 2141 mode |= WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE; 2142 } else { 2143 mode |= WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN; 2144 } 2145 2146 getWindow().setSoftInputMode(mode); 2147 2148 // reset mMessagesAndDraftLoaded 2149 mMessagesAndDraftLoaded = false; 2150 2151 if (!DEFER_LOADING_MESSAGES_AND_DRAFT) { 2152 loadMessagesAndDraft(1); 2153 } else { 2154 // HACK: force load messages+draft after max delay, if it's not already loaded. 2155 // this is to work around when coming out of sleep mode. WindowManager behaves 2156 // strangely and hides the keyboard when it should be shown, or sometimes initially 2157 // shows it when we want to hide it. In that case, we never get the onSizeChanged() 2158 // callback w/ keyboard shown, so we wouldn't know to load the messages+draft. 2159 mHandler.postDelayed(new Runnable() { 2160 public void run() { 2161 loadMessagesAndDraft(2); 2162 } 2163 }, LOADING_MESSAGES_AND_DRAFT_MAX_DELAY_MS); 2164 } 2165 2166 // Update the fasttrack info in case any of the recipients' contact info changed 2167 // while we were paused. This can happen, for example, if a user changes or adds 2168 // an avatar associated with a contact. 2169 mWorkingMessage.syncWorkingRecipients(); 2170 2171 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 2172 log("update title, mConversation=" + mConversation.toString()); 2173 } 2174 2175 updateTitle(mConversation.getRecipients()); 2176 2177 ActionBar actionBar = getActionBar(); 2178 actionBar.setDisplayHomeAsUpEnabled(true); 2179 } 2180 loadMessageContent()2181 public void loadMessageContent() { 2182 // Don't let any markAsRead DB updates occur before we've loaded the messages for 2183 // the thread. Unblocking occurs when we're done querying for the conversation 2184 // items. 2185 mConversation.blockMarkAsRead(true); 2186 mConversation.markAsRead(); // dismiss any notifications for this convo 2187 startMsgListQuery(); 2188 updateSendFailedNotification(); 2189 } 2190 2191 /** 2192 * Load message history and draft. This method should be called from main thread. 2193 * @param debugFlag shows where this is being called from 2194 */ loadMessagesAndDraft(int debugFlag)2195 private void loadMessagesAndDraft(int debugFlag) { 2196 if (!mSendDiscreetMode && !mMessagesAndDraftLoaded) { 2197 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 2198 Log.v(TAG, "### CMA.loadMessagesAndDraft: flag=" + debugFlag); 2199 } 2200 loadMessageContent(); 2201 boolean drawBottomPanel = true; 2202 if (mShouldLoadDraft) { 2203 if (loadDraft()) { 2204 drawBottomPanel = false; 2205 } 2206 } 2207 if (drawBottomPanel) { 2208 drawBottomPanel(); 2209 } 2210 mMessagesAndDraftLoaded = true; 2211 } 2212 } 2213 updateSendFailedNotification()2214 private void updateSendFailedNotification() { 2215 final long threadId = mConversation.getThreadId(); 2216 if (threadId <= 0) 2217 return; 2218 2219 // updateSendFailedNotificationForThread makes a database call, so do the work off 2220 // of the ui thread. 2221 new Thread(new Runnable() { 2222 @Override 2223 public void run() { 2224 MessagingNotification.updateSendFailedNotificationForThread( 2225 ComposeMessageActivity.this, threadId); 2226 } 2227 }, "ComposeMessageActivity.updateSendFailedNotification").start(); 2228 } 2229 2230 @Override onSaveInstanceState(Bundle outState)2231 public void onSaveInstanceState(Bundle outState) { 2232 super.onSaveInstanceState(outState); 2233 2234 outState.putString(RECIPIENTS, getRecipients().serialize()); 2235 2236 mWorkingMessage.writeStateToBundle(outState); 2237 2238 if (mSendDiscreetMode) { 2239 outState.putBoolean(KEY_EXIT_ON_SENT, mSendDiscreetMode); 2240 } 2241 if (mForwardMessageMode) { 2242 outState.putBoolean(KEY_FORWARDED_MESSAGE, mForwardMessageMode); 2243 } 2244 } 2245 2246 @Override onResume()2247 protected void onResume() { 2248 super.onResume(); 2249 2250 // OLD: get notified of presence updates to update the titlebar. 2251 // NEW: we are using ContactHeaderWidget which displays presence, but updating presence 2252 // there is out of our control. 2253 //Contact.startPresenceObserver(); 2254 2255 addRecipientsListeners(); 2256 2257 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 2258 log("update title, mConversation=" + mConversation.toString()); 2259 } 2260 2261 // There seems to be a bug in the framework such that setting the title 2262 // here gets overwritten to the original title. Do this delayed as a 2263 // workaround. 2264 mMessageListItemHandler.postDelayed(new Runnable() { 2265 @Override 2266 public void run() { 2267 ContactList recipients = isRecipientsEditorVisible() ? 2268 mRecipientsEditor.constructContactsFromInput(false) : getRecipients(); 2269 updateTitle(recipients); 2270 } 2271 }, 100); 2272 2273 mIsRunning = true; 2274 updateThreadIdIfRunning(); 2275 mConversation.markAsRead(); 2276 } 2277 2278 @Override onPause()2279 protected void onPause() { 2280 super.onPause(); 2281 2282 if (DEBUG) { 2283 Log.v(TAG, "onPause: setCurrentlyDisplayedThreadId: " + 2284 MessagingNotification.THREAD_NONE); 2285 } 2286 MessagingNotification.setCurrentlyDisplayedThreadId(MessagingNotification.THREAD_NONE); 2287 2288 // OLD: stop getting notified of presence updates to update the titlebar. 2289 // NEW: we are using ContactHeaderWidget which displays presence, but updating presence 2290 // there is out of our control. 2291 //Contact.stopPresenceObserver(); 2292 2293 removeRecipientsListeners(); 2294 2295 // remove any callback to display a progress spinner 2296 if (mAsyncDialog != null) { 2297 mAsyncDialog.clearPendingProgressDialog(); 2298 } 2299 2300 // Remember whether the list is scrolled to the end when we're paused so we can rescroll 2301 // to the end when resumed. 2302 if (mMsgListAdapter != null && 2303 mMsgListView.getLastVisiblePosition() >= mMsgListAdapter.getCount() - 1) { 2304 mSavedScrollPosition = Integer.MAX_VALUE; 2305 } else { 2306 mSavedScrollPosition = mMsgListView.getFirstVisiblePosition(); 2307 } 2308 if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 2309 Log.v(TAG, "onPause: mSavedScrollPosition=" + mSavedScrollPosition); 2310 } 2311 2312 mConversation.markAsRead(); 2313 mIsRunning = false; 2314 } 2315 2316 @Override onStop()2317 protected void onStop() { 2318 super.onStop(); 2319 2320 // No need to do the querying when finished this activity 2321 mBackgroundQueryHandler.cancelOperation(MESSAGE_LIST_QUERY_TOKEN); 2322 2323 // Allow any blocked calls to update the thread's read status. 2324 mConversation.blockMarkAsRead(false); 2325 2326 if (mMsgListAdapter != null) { 2327 // Close the cursor in the ListAdapter if the activity stopped. 2328 Cursor cursor = mMsgListAdapter.getCursor(); 2329 2330 if (cursor != null && !cursor.isClosed()) { 2331 cursor.close(); 2332 } 2333 2334 mMsgListAdapter.changeCursor(null); 2335 mMsgListAdapter.cancelBackgroundLoading(); 2336 } 2337 2338 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 2339 log("save draft"); 2340 } 2341 saveDraft(true); 2342 2343 // set 'mShouldLoadDraft' to true, so when coming back to ComposeMessageActivity, we would 2344 // load the draft, unless we are coming back to the activity after attaching a photo, etc, 2345 // in which case we should set 'mShouldLoadDraft' to false. 2346 mShouldLoadDraft = true; 2347 2348 // Cleanup the BroadcastReceiver. 2349 unregisterReceiver(mHttpProgressReceiver); 2350 } 2351 2352 @Override onDestroy()2353 protected void onDestroy() { 2354 if (TRACE) { 2355 android.os.Debug.stopMethodTracing(); 2356 } 2357 2358 super.onDestroy(); 2359 } 2360 2361 @Override onConfigurationChanged(Configuration newConfig)2362 public void onConfigurationChanged(Configuration newConfig) { 2363 super.onConfigurationChanged(newConfig); 2364 2365 if (resetConfiguration(newConfig)) { 2366 // Have to re-layout the attachment editor because we have different layouts 2367 // depending on whether we're portrait or landscape. 2368 drawTopPanel(isSubjectEditorVisible()); 2369 } 2370 if (LOCAL_LOGV) { 2371 Log.v(TAG, "CMA.onConfigurationChanged: " + newConfig + 2372 ", mIsKeyboardOpen=" + mIsKeyboardOpen); 2373 } 2374 onKeyboardStateChanged(); 2375 } 2376 2377 // returns true if landscape/portrait configuration has changed resetConfiguration(Configuration config)2378 private boolean resetConfiguration(Configuration config) { 2379 mIsKeyboardOpen = config.keyboardHidden == KEYBOARDHIDDEN_NO; 2380 boolean isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE; 2381 if (mIsLandscape != isLandscape) { 2382 mIsLandscape = isLandscape; 2383 return true; 2384 } 2385 return false; 2386 } 2387 onKeyboardStateChanged()2388 private void onKeyboardStateChanged() { 2389 // If the keyboard is hidden, don't show focus highlights for 2390 // things that cannot receive input. 2391 mTextEditor.setEnabled(mIsSmsEnabled); 2392 if (!mIsSmsEnabled) { 2393 if (mRecipientsEditor != null) { 2394 mRecipientsEditor.setFocusableInTouchMode(false); 2395 } 2396 if (mSubjectTextEditor != null) { 2397 mSubjectTextEditor.setFocusableInTouchMode(false); 2398 } 2399 mTextEditor.setFocusableInTouchMode(false); 2400 mTextEditor.setHint(R.string.sending_disabled_not_default_app); 2401 } else if (mIsKeyboardOpen) { 2402 if (mRecipientsEditor != null) { 2403 mRecipientsEditor.setFocusableInTouchMode(true); 2404 } 2405 if (mSubjectTextEditor != null) { 2406 mSubjectTextEditor.setFocusableInTouchMode(true); 2407 } 2408 mTextEditor.setFocusableInTouchMode(true); 2409 mTextEditor.setHint(R.string.type_to_compose_text_enter_to_send); 2410 } else { 2411 if (mRecipientsEditor != null) { 2412 mRecipientsEditor.setFocusable(false); 2413 } 2414 if (mSubjectTextEditor != null) { 2415 mSubjectTextEditor.setFocusable(false); 2416 } 2417 mTextEditor.setFocusable(false); 2418 mTextEditor.setHint(R.string.open_keyboard_to_compose_message); 2419 } 2420 } 2421 2422 @Override onKeyDown(int keyCode, KeyEvent event)2423 public boolean onKeyDown(int keyCode, KeyEvent event) { 2424 switch (keyCode) { 2425 case KeyEvent.KEYCODE_DEL: 2426 if ((mMsgListAdapter != null) && mMsgListView.isFocused()) { 2427 Cursor cursor; 2428 try { 2429 cursor = (Cursor) mMsgListView.getSelectedItem(); 2430 } catch (ClassCastException e) { 2431 Log.e(TAG, "Unexpected ClassCastException.", e); 2432 return super.onKeyDown(keyCode, event); 2433 } 2434 2435 if (cursor != null) { 2436 String type = cursor.getString(COLUMN_MSG_TYPE); 2437 long msgId = cursor.getLong(COLUMN_ID); 2438 MessageItem msgItem = mMsgListAdapter.getCachedMessageItem(type, msgId, 2439 cursor); 2440 if (msgItem != null) { 2441 DeleteMessageListener l = new DeleteMessageListener(msgItem); 2442 confirmDeleteDialog(l, msgItem.mLocked); 2443 } 2444 return true; 2445 } 2446 } 2447 break; 2448 case KeyEvent.KEYCODE_DPAD_CENTER: 2449 case KeyEvent.KEYCODE_ENTER: 2450 if (isPreparedForSending()) { 2451 confirmSendMessageIfNeeded(); 2452 return true; 2453 } 2454 break; 2455 case KeyEvent.KEYCODE_BACK: 2456 exitComposeMessageActivity(new Runnable() { 2457 @Override 2458 public void run() { 2459 finish(); 2460 } 2461 }); 2462 return true; 2463 } 2464 2465 return super.onKeyDown(keyCode, event); 2466 } 2467 exitComposeMessageActivity(final Runnable exit)2468 private void exitComposeMessageActivity(final Runnable exit) { 2469 // If the message is empty, just quit -- finishing the 2470 // activity will cause an empty draft to be deleted. 2471 if (!mWorkingMessage.isWorthSaving()) { 2472 exit.run(); 2473 return; 2474 } 2475 2476 if (isRecipientsEditorVisible() && 2477 !mRecipientsEditor.hasValidRecipient(mWorkingMessage.requiresMms())) { 2478 MessageUtils.showDiscardDraftConfirmDialog(this, new DiscardDraftListener()); 2479 return; 2480 } 2481 2482 mToastForDraftSave = true; 2483 exit.run(); 2484 } 2485 goToConversationList()2486 private void goToConversationList() { 2487 finish(); 2488 startActivity(new Intent(this, ConversationList.class)); 2489 } 2490 hideRecipientEditor()2491 private void hideRecipientEditor() { 2492 if (mRecipientsEditor != null) { 2493 mRecipientsEditor.removeTextChangedListener(mRecipientsWatcher); 2494 mRecipientsEditor.setVisibility(View.GONE); 2495 hideOrShowTopPanel(); 2496 } 2497 } 2498 isRecipientsEditorVisible()2499 private boolean isRecipientsEditorVisible() { 2500 return (null != mRecipientsEditor) 2501 && (View.VISIBLE == mRecipientsEditor.getVisibility()); 2502 } 2503 isSubjectEditorVisible()2504 private boolean isSubjectEditorVisible() { 2505 return (null != mSubjectTextEditor) 2506 && (View.VISIBLE == mSubjectTextEditor.getVisibility()); 2507 } 2508 2509 @Override onAttachmentChanged()2510 public void onAttachmentChanged() { 2511 // Have to make sure we're on the UI thread. This function can be called off of the UI 2512 // thread when we're adding multi-attachments 2513 runOnUiThread(new Runnable() { 2514 @Override 2515 public void run() { 2516 drawBottomPanel(); 2517 updateSendButtonState(); 2518 drawTopPanel(isSubjectEditorVisible()); 2519 } 2520 }); 2521 } 2522 2523 @Override onProtocolChanged(final boolean convertToMms)2524 public void onProtocolChanged(final boolean convertToMms) { 2525 // Have to make sure we're on the UI thread. This function can be called off of the UI 2526 // thread when we're adding multi-attachments 2527 runOnUiThread(new Runnable() { 2528 @Override 2529 public void run() { 2530 showSmsOrMmsSendButton(convertToMms); 2531 2532 if (convertToMms) { 2533 // In the case we went from a long sms with a counter to an mms because 2534 // the user added an attachment or a subject, hide the counter -- 2535 // it doesn't apply to mms. 2536 mTextCounter.setVisibility(View.GONE); 2537 2538 showConvertToMmsToast(); 2539 } 2540 } 2541 }); 2542 } 2543 2544 // Show or hide the Sms or Mms button as appropriate. Return the view so that the caller 2545 // can adjust the enableness and focusability. showSmsOrMmsSendButton(boolean isMms)2546 private View showSmsOrMmsSendButton(boolean isMms) { 2547 View showButton; 2548 View hideButton; 2549 if (isMms) { 2550 showButton = mSendButtonMms; 2551 hideButton = mSendButtonSms; 2552 } else { 2553 showButton = mSendButtonSms; 2554 hideButton = mSendButtonMms; 2555 } 2556 showButton.setVisibility(View.VISIBLE); 2557 hideButton.setVisibility(View.GONE); 2558 2559 return showButton; 2560 } 2561 2562 Runnable mResetMessageRunnable = new Runnable() { 2563 @Override 2564 public void run() { 2565 resetMessage(); 2566 } 2567 }; 2568 2569 @Override onPreMessageSent()2570 public void onPreMessageSent() { 2571 runOnUiThread(mResetMessageRunnable); 2572 } 2573 2574 @Override onMessageSent()2575 public void onMessageSent() { 2576 // This callback can come in on any thread; put it on the main thread to avoid 2577 // concurrency problems 2578 runOnUiThread(new Runnable() { 2579 @Override 2580 public void run() { 2581 // If we already have messages in the list adapter, it 2582 // will be auto-requerying; don't thrash another query in. 2583 // TODO: relying on auto-requerying seems unreliable when priming an MMS into the 2584 // outbox. Need to investigate. 2585 // if (mMsgListAdapter.getCount() == 0) { 2586 if (LogTag.VERBOSE) { 2587 log("onMessageSent"); 2588 } 2589 startMsgListQuery(); 2590 // } 2591 2592 // The thread ID could have changed if this is a new message that we just inserted 2593 // into the database (and looked up or created a thread for it) 2594 updateThreadIdIfRunning(); 2595 } 2596 }); 2597 } 2598 2599 @Override onMaxPendingMessagesReached()2600 public void onMaxPendingMessagesReached() { 2601 saveDraft(false); 2602 2603 runOnUiThread(new Runnable() { 2604 @Override 2605 public void run() { 2606 Toast.makeText(ComposeMessageActivity.this, R.string.too_many_unsent_mms, 2607 Toast.LENGTH_LONG).show(); 2608 } 2609 }); 2610 } 2611 2612 @Override onAttachmentError(final int error)2613 public void onAttachmentError(final int error) { 2614 runOnUiThread(new Runnable() { 2615 @Override 2616 public void run() { 2617 handleAddAttachmentError(error, R.string.type_picture); 2618 onMessageSent(); // now requery the list of messages 2619 } 2620 }); 2621 } 2622 2623 // We don't want to show the "call" option unless there is only one 2624 // recipient and it's a phone number. isRecipientCallable()2625 private boolean isRecipientCallable() { 2626 ContactList recipients = getRecipients(); 2627 return (recipients.size() == 1 && !recipients.containsEmail()); 2628 } 2629 dialRecipient()2630 private void dialRecipient() { 2631 if (isRecipientCallable()) { 2632 String number = getRecipients().get(0).getNumber(); 2633 Intent dialIntent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + number)); 2634 startActivity(dialIntent); 2635 } 2636 } 2637 2638 @Override onPrepareOptionsMenu(Menu menu)2639 public boolean onPrepareOptionsMenu(Menu menu) { 2640 super.onPrepareOptionsMenu(menu) ; 2641 2642 menu.clear(); 2643 2644 if (mSendDiscreetMode && !mForwardMessageMode) { 2645 // When we're in send-a-single-message mode from the lock screen, don't show 2646 // any menus. 2647 return true; 2648 } 2649 2650 // Don't show the call icon if the device don't support voice calling. 2651 boolean voiceCapable = 2652 getResources().getBoolean(com.android.internal.R.bool.config_voice_capable); 2653 if (isRecipientCallable() && voiceCapable) { 2654 MenuItem item = menu.add(0, MENU_CALL_RECIPIENT, 0, R.string.menu_call) 2655 .setIcon(R.drawable.ic_menu_call) 2656 .setTitle(R.string.menu_call); 2657 if (!isRecipientsEditorVisible()) { 2658 // If we're not composing a new message, show the call icon in the actionbar 2659 item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 2660 } 2661 } 2662 2663 if (MmsConfig.getMmsEnabled() && mIsSmsEnabled) { 2664 if (!isSubjectEditorVisible()) { 2665 menu.add(0, MENU_ADD_SUBJECT, 0, R.string.add_subject).setIcon( 2666 R.drawable.ic_menu_edit); 2667 } 2668 if (!mWorkingMessage.hasAttachment()) { 2669 menu.add(0, MENU_ADD_ATTACHMENT, 0, R.string.add_attachment) 2670 .setIcon(R.drawable.ic_menu_attachment) 2671 .setTitle(R.string.add_attachment) 2672 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); // add to actionbar 2673 } 2674 } 2675 2676 if (isPreparedForSending() && mIsSmsEnabled) { 2677 menu.add(0, MENU_SEND, 0, R.string.send).setIcon(android.R.drawable.ic_menu_send); 2678 } 2679 2680 if (getRecipients().size() > 1) { 2681 menu.add(0, MENU_GROUP_PARTICIPANTS, 0, R.string.menu_group_participants); 2682 } 2683 2684 if (mMsgListAdapter.getCount() > 0 && mIsSmsEnabled) { 2685 // Removed search as part of b/1205708 2686 //menu.add(0, MENU_SEARCH, 0, R.string.menu_search).setIcon( 2687 // R.drawable.ic_menu_search); 2688 Cursor cursor = mMsgListAdapter.getCursor(); 2689 if ((null != cursor) && (cursor.getCount() > 0)) { 2690 menu.add(0, MENU_DELETE_THREAD, 0, R.string.delete_thread).setIcon( 2691 android.R.drawable.ic_menu_delete); 2692 } 2693 } else if (mIsSmsEnabled) { 2694 menu.add(0, MENU_DISCARD, 0, R.string.discard).setIcon(android.R.drawable.ic_menu_delete); 2695 } 2696 2697 buildAddAddressToContactMenuItem(menu); 2698 2699 menu.add(0, MENU_PREFERENCES, 0, R.string.menu_preferences).setIcon( 2700 android.R.drawable.ic_menu_preferences); 2701 2702 if (LogTag.DEBUG_DUMP) { 2703 menu.add(0, MENU_DEBUG_DUMP, 0, R.string.menu_debug_dump); 2704 } 2705 2706 return true; 2707 } 2708 buildAddAddressToContactMenuItem(Menu menu)2709 private void buildAddAddressToContactMenuItem(Menu menu) { 2710 // bug #7087793: for group of recipients, remove "Add to People" action. Rely on 2711 // individually creating contacts for unknown phone numbers by touching the individual 2712 // sender's avatars, one at a time 2713 ContactList contacts = getRecipients(); 2714 if (contacts.size() != 1) { 2715 return; 2716 } 2717 2718 // if we don't have a contact for the recipient, create a menu item to add the number 2719 // to contacts. 2720 Contact c = contacts.get(0); 2721 if (!c.existsInDatabase() && canAddToContacts(c)) { 2722 Intent intent = ConversationList.createAddContactIntent(c.getNumber()); 2723 menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, R.string.menu_add_to_contacts) 2724 .setIcon(android.R.drawable.ic_menu_add) 2725 .setIntent(intent); 2726 } 2727 } 2728 2729 @Override onOptionsItemSelected(MenuItem item)2730 public boolean onOptionsItemSelected(MenuItem item) { 2731 switch (item.getItemId()) { 2732 case MENU_ADD_SUBJECT: 2733 showSubjectEditor(true); 2734 mWorkingMessage.setSubject("", true); 2735 updateSendButtonState(); 2736 mSubjectTextEditor.requestFocus(); 2737 break; 2738 case MENU_ADD_ATTACHMENT: 2739 // Launch the add-attachment list dialog 2740 showAddAttachmentDialog(false); 2741 break; 2742 case MENU_DISCARD: 2743 mWorkingMessage.discard(); 2744 finish(); 2745 break; 2746 case MENU_SEND: 2747 if (isPreparedForSending()) { 2748 confirmSendMessageIfNeeded(); 2749 } 2750 break; 2751 case MENU_SEARCH: 2752 onSearchRequested(); 2753 break; 2754 case MENU_DELETE_THREAD: 2755 confirmDeleteThread(mConversation.getThreadId()); 2756 break; 2757 2758 case android.R.id.home: 2759 case MENU_CONVERSATION_LIST: 2760 exitComposeMessageActivity(new Runnable() { 2761 @Override 2762 public void run() { 2763 goToConversationList(); 2764 } 2765 }); 2766 break; 2767 case MENU_CALL_RECIPIENT: 2768 dialRecipient(); 2769 break; 2770 case MENU_GROUP_PARTICIPANTS: 2771 { 2772 Intent intent = new Intent(this, RecipientListActivity.class); 2773 intent.putExtra(THREAD_ID, mConversation.getThreadId()); 2774 startActivity(intent); 2775 break; 2776 } 2777 case MENU_VIEW_CONTACT: { 2778 // View the contact for the first (and only) recipient. 2779 ContactList list = getRecipients(); 2780 if (list.size() == 1 && list.get(0).existsInDatabase()) { 2781 Uri contactUri = list.get(0).getUri(); 2782 Intent intent = new Intent(Intent.ACTION_VIEW, contactUri); 2783 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 2784 startActivity(intent); 2785 } 2786 break; 2787 } 2788 case MENU_ADD_ADDRESS_TO_CONTACTS: 2789 mAddContactIntent = item.getIntent(); 2790 startActivityForResult(mAddContactIntent, REQUEST_CODE_ADD_CONTACT); 2791 break; 2792 case MENU_PREFERENCES: { 2793 Intent intent = new Intent(this, MessagingPreferenceActivity.class); 2794 startActivityIfNeeded(intent, -1); 2795 break; 2796 } 2797 case MENU_DEBUG_DUMP: 2798 mWorkingMessage.dump(); 2799 Conversation.dump(); 2800 LogTag.dumpInternalTables(this); 2801 break; 2802 } 2803 2804 return true; 2805 } 2806 confirmDeleteThread(long threadId)2807 private void confirmDeleteThread(long threadId) { 2808 Conversation.startQueryHaveLockedMessages(mBackgroundQueryHandler, 2809 threadId, ConversationList.HAVE_LOCKED_MESSAGES_TOKEN); 2810 } 2811 2812 // static class SystemProperties { // TODO, temp class to get unbundling working 2813 // static int getInt(String s, int value) { 2814 // return value; // just return the default value or now 2815 // } 2816 // } 2817 addAttachment(int type, boolean replace)2818 private void addAttachment(int type, boolean replace) { 2819 // Calculate the size of the current slide if we're doing a replace so the 2820 // slide size can optionally be used in computing how much room is left for an attachment. 2821 int currentSlideSize = 0; 2822 SlideshowModel slideShow = mWorkingMessage.getSlideshow(); 2823 if (replace && slideShow != null) { 2824 WorkingMessage.removeThumbnailsFromCache(slideShow); 2825 SlideModel slide = slideShow.get(0); 2826 currentSlideSize = slide.getSlideSize(); 2827 } 2828 switch (type) { 2829 case AttachmentTypeSelectorAdapter.ADD_IMAGE: 2830 MessageUtils.selectImage(this, REQUEST_CODE_ATTACH_IMAGE); 2831 break; 2832 2833 case AttachmentTypeSelectorAdapter.TAKE_PICTURE: { 2834 MessageUtils.capturePicture(this, REQUEST_CODE_TAKE_PICTURE); 2835 break; 2836 } 2837 2838 case AttachmentTypeSelectorAdapter.ADD_VIDEO: 2839 MessageUtils.selectVideo(this, REQUEST_CODE_ATTACH_VIDEO); 2840 break; 2841 2842 case AttachmentTypeSelectorAdapter.RECORD_VIDEO: { 2843 long sizeLimit = computeAttachmentSizeLimit(slideShow, currentSlideSize); 2844 if (sizeLimit > 0) { 2845 MessageUtils.recordVideo(this, REQUEST_CODE_TAKE_VIDEO, sizeLimit); 2846 } else { 2847 Toast.makeText(this, 2848 getString(R.string.message_too_big_for_video), 2849 Toast.LENGTH_SHORT).show(); 2850 } 2851 } 2852 break; 2853 2854 case AttachmentTypeSelectorAdapter.ADD_SOUND: 2855 MessageUtils.selectAudio(this, REQUEST_CODE_ATTACH_SOUND); 2856 break; 2857 2858 case AttachmentTypeSelectorAdapter.RECORD_SOUND: 2859 long sizeLimit = computeAttachmentSizeLimit(slideShow, currentSlideSize); 2860 MessageUtils.recordSound(this, REQUEST_CODE_RECORD_SOUND, sizeLimit); 2861 break; 2862 2863 case AttachmentTypeSelectorAdapter.ADD_SLIDESHOW: 2864 editSlideshow(); 2865 break; 2866 2867 default: 2868 break; 2869 } 2870 } 2871 computeAttachmentSizeLimit(SlideshowModel slideShow, int currentSlideSize)2872 public static long computeAttachmentSizeLimit(SlideshowModel slideShow, int currentSlideSize) { 2873 // Computer attachment size limit. Subtract 1K for some text. 2874 long sizeLimit = MmsConfig.getMaxMessageSize() - SlideshowModel.SLIDESHOW_SLOP; 2875 if (slideShow != null) { 2876 sizeLimit -= slideShow.getCurrentMessageSize(); 2877 2878 // We're about to ask the camera to capture some video (or the sound recorder 2879 // to record some audio) which will eventually replace the content on the current 2880 // slide. Since the current slide already has some content (which was subtracted 2881 // out just above) and that content is going to get replaced, we can add the size of the 2882 // current slide into the available space used to capture a video (or audio). 2883 sizeLimit += currentSlideSize; 2884 } 2885 return sizeLimit; 2886 } 2887 showAddAttachmentDialog(final boolean replace)2888 private void showAddAttachmentDialog(final boolean replace) { 2889 AlertDialog.Builder builder = new AlertDialog.Builder(this); 2890 builder.setIcon(R.drawable.ic_dialog_attach); 2891 builder.setTitle(R.string.add_attachment); 2892 2893 if (mAttachmentTypeSelectorAdapter == null) { 2894 mAttachmentTypeSelectorAdapter = new AttachmentTypeSelectorAdapter( 2895 this, AttachmentTypeSelectorAdapter.MODE_WITH_SLIDESHOW); 2896 } 2897 builder.setAdapter(mAttachmentTypeSelectorAdapter, new DialogInterface.OnClickListener() { 2898 @Override 2899 public void onClick(DialogInterface dialog, int which) { 2900 addAttachment(mAttachmentTypeSelectorAdapter.buttonToCommand(which), replace); 2901 dialog.dismiss(); 2902 } 2903 }); 2904 2905 builder.show(); 2906 } 2907 2908 @Override onActivityResult(int requestCode, int resultCode, Intent data)2909 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 2910 if (LogTag.VERBOSE) { 2911 log("onActivityResult: requestCode=" + requestCode + ", resultCode=" + resultCode + 2912 ", data=" + data); 2913 } 2914 mWaitingForSubActivity = false; // We're back! 2915 mShouldLoadDraft = false; 2916 if (mWorkingMessage.isFakeMmsForDraft()) { 2917 // We no longer have to fake the fact we're an Mms. At this point we are or we aren't, 2918 // based on attachments and other Mms attrs. 2919 mWorkingMessage.removeFakeMmsForDraft(); 2920 } 2921 2922 if (requestCode == REQUEST_CODE_PICK) { 2923 mWorkingMessage.asyncDeleteDraftSmsMessage(mConversation); 2924 } 2925 2926 if (requestCode == REQUEST_CODE_ADD_CONTACT) { 2927 // The user might have added a new contact. When we tell contacts to add a contact 2928 // and tap "Done", we're not returned to Messaging. If we back out to return to 2929 // messaging after adding a contact, the resultCode is RESULT_CANCELED. Therefore, 2930 // assume a contact was added and get the contact and force our cached contact to 2931 // get reloaded with the new info (such as contact name). After the 2932 // contact is reloaded, the function onUpdate() in this file will get called 2933 // and it will update the title bar, etc. 2934 if (mAddContactIntent != null) { 2935 String address = 2936 mAddContactIntent.getStringExtra(ContactsContract.Intents.Insert.EMAIL); 2937 if (address == null) { 2938 address = 2939 mAddContactIntent.getStringExtra(ContactsContract.Intents.Insert.PHONE); 2940 } 2941 if (address != null) { 2942 Contact contact = Contact.get(address, false); 2943 if (contact != null) { 2944 contact.reload(); 2945 } 2946 } 2947 } 2948 } 2949 2950 if (resultCode != RESULT_OK){ 2951 if (LogTag.VERBOSE) log("bail due to resultCode=" + resultCode); 2952 return; 2953 } 2954 2955 switch (requestCode) { 2956 case REQUEST_CODE_CREATE_SLIDESHOW: 2957 if (data != null) { 2958 WorkingMessage newMessage = WorkingMessage.load(this, data.getData()); 2959 if (newMessage != null) { 2960 mWorkingMessage = newMessage; 2961 mWorkingMessage.setConversation(mConversation); 2962 updateThreadIdIfRunning(); 2963 drawTopPanel(false); 2964 updateSendButtonState(); 2965 } 2966 } 2967 break; 2968 2969 case REQUEST_CODE_TAKE_PICTURE: { 2970 // create a file based uri and pass to addImage(). We want to read the JPEG 2971 // data directly from file (using UriImage) instead of decoding it into a Bitmap, 2972 // which takes up too much memory and could easily lead to OOM. 2973 File file = new File(TempFileProvider.getScrapPath(this)); 2974 Uri uri = Uri.fromFile(file); 2975 2976 // Remove the old captured picture's thumbnail from the cache 2977 MmsApp.getApplication().getThumbnailManager().removeThumbnail(uri); 2978 2979 addImageAsync(uri, false); 2980 break; 2981 } 2982 2983 case REQUEST_CODE_ATTACH_IMAGE: { 2984 if (data != null) { 2985 addImageAsync(data.getData(), false); 2986 } 2987 break; 2988 } 2989 2990 case REQUEST_CODE_TAKE_VIDEO: 2991 Uri videoUri = TempFileProvider.renameScrapFile(".3gp", null, this); 2992 // Remove the old captured video's thumbnail from the cache 2993 MmsApp.getApplication().getThumbnailManager().removeThumbnail(videoUri); 2994 2995 addVideoAsync(videoUri, false); // can handle null videoUri 2996 break; 2997 2998 case REQUEST_CODE_ATTACH_VIDEO: 2999 if (data != null) { 3000 addVideoAsync(data.getData(), false); 3001 } 3002 break; 3003 3004 case REQUEST_CODE_ATTACH_SOUND: { 3005 Uri uri = (Uri) data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); 3006 if (Settings.System.DEFAULT_RINGTONE_URI.equals(uri)) { 3007 break; 3008 } 3009 addAudio(uri); 3010 break; 3011 } 3012 3013 case REQUEST_CODE_RECORD_SOUND: 3014 if (data != null) { 3015 addAudio(data.getData()); 3016 } 3017 break; 3018 3019 case REQUEST_CODE_ECM_EXIT_DIALOG: 3020 boolean outOfEmergencyMode = data.getBooleanExtra(EXIT_ECM_RESULT, false); 3021 if (outOfEmergencyMode) { 3022 sendMessage(false); 3023 } 3024 break; 3025 3026 case REQUEST_CODE_PICK: 3027 if (data != null) { 3028 processPickResult(data); 3029 } 3030 break; 3031 3032 default: 3033 if (LogTag.VERBOSE) log("bail due to unknown requestCode=" + requestCode); 3034 break; 3035 } 3036 } 3037 processPickResult(final Intent data)3038 private void processPickResult(final Intent data) { 3039 // The EXTRA_PHONE_URIS stores the phone's urls that were selected by user in the 3040 // multiple phone picker. 3041 final Parcelable[] uris = 3042 data.getParcelableArrayExtra(Intents.EXTRA_PHONE_URIS); 3043 3044 final int recipientCount = uris != null ? uris.length : 0; 3045 3046 final int recipientLimit = MmsConfig.getRecipientLimit(); 3047 if (recipientLimit != Integer.MAX_VALUE && recipientCount > recipientLimit) { 3048 new AlertDialog.Builder(this) 3049 .setMessage(getString(R.string.too_many_recipients, recipientCount, recipientLimit)) 3050 .setPositiveButton(android.R.string.ok, null) 3051 .create().show(); 3052 return; 3053 } 3054 3055 final Handler handler = new Handler(); 3056 final ProgressDialog progressDialog = new ProgressDialog(this); 3057 progressDialog.setTitle(getText(R.string.pick_too_many_recipients)); 3058 progressDialog.setMessage(getText(R.string.adding_recipients)); 3059 progressDialog.setIndeterminate(true); 3060 progressDialog.setCancelable(false); 3061 3062 final Runnable showProgress = new Runnable() { 3063 @Override 3064 public void run() { 3065 progressDialog.show(); 3066 } 3067 }; 3068 // Only show the progress dialog if we can not finish off parsing the return data in 1s, 3069 // otherwise the dialog could flicker. 3070 handler.postDelayed(showProgress, 1000); 3071 3072 new Thread(new Runnable() { 3073 @Override 3074 public void run() { 3075 final ContactList list; 3076 try { 3077 list = ContactList.blockingGetByUris(uris); 3078 } finally { 3079 handler.removeCallbacks(showProgress); 3080 progressDialog.dismiss(); 3081 } 3082 // TODO: there is already code to update the contact header widget and recipients 3083 // editor if the contacts change. we can re-use that code. 3084 final Runnable populateWorker = new Runnable() { 3085 @Override 3086 public void run() { 3087 mRecipientsEditor.populate(list); 3088 updateTitle(list); 3089 } 3090 }; 3091 handler.post(populateWorker); 3092 } 3093 }, "ComoseMessageActivity.processPickResult").start(); 3094 } 3095 3096 private final ResizeImageResultCallback mResizeImageCallback = new ResizeImageResultCallback() { 3097 // TODO: make this produce a Uri, that's what we want anyway 3098 @Override 3099 public void onResizeResult(PduPart part, boolean append) { 3100 if (part == null) { 3101 handleAddAttachmentError(WorkingMessage.UNKNOWN_ERROR, R.string.type_picture); 3102 return; 3103 } 3104 3105 Context context = ComposeMessageActivity.this; 3106 PduPersister persister = PduPersister.getPduPersister(context); 3107 int result; 3108 3109 Uri messageUri = mWorkingMessage.saveAsMms(true); 3110 if (messageUri == null) { 3111 result = WorkingMessage.UNKNOWN_ERROR; 3112 } else { 3113 try { 3114 Uri dataUri = persister.persistPart(part, 3115 ContentUris.parseId(messageUri), null); 3116 result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, dataUri, append); 3117 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3118 log("ResizeImageResultCallback: dataUri=" + dataUri); 3119 } 3120 } catch (MmsException e) { 3121 result = WorkingMessage.UNKNOWN_ERROR; 3122 } 3123 } 3124 3125 handleAddAttachmentError(result, R.string.type_picture); 3126 } 3127 }; 3128 handleAddAttachmentError(final int error, final int mediaTypeStringId)3129 private void handleAddAttachmentError(final int error, final int mediaTypeStringId) { 3130 if (error == WorkingMessage.OK) { 3131 return; 3132 } 3133 Log.d(TAG, "handleAddAttachmentError: " + error); 3134 3135 runOnUiThread(new Runnable() { 3136 @Override 3137 public void run() { 3138 Resources res = getResources(); 3139 String mediaType = res.getString(mediaTypeStringId); 3140 String title, message; 3141 3142 switch(error) { 3143 case WorkingMessage.UNKNOWN_ERROR: 3144 message = res.getString(R.string.failed_to_add_media, mediaType); 3145 Toast.makeText(ComposeMessageActivity.this, message, Toast.LENGTH_SHORT).show(); 3146 return; 3147 case WorkingMessage.UNSUPPORTED_TYPE: 3148 title = res.getString(R.string.unsupported_media_format, mediaType); 3149 message = res.getString(R.string.select_different_media, mediaType); 3150 break; 3151 case WorkingMessage.MESSAGE_SIZE_EXCEEDED: 3152 title = res.getString(R.string.exceed_message_size_limitation, mediaType); 3153 message = res.getString(R.string.failed_to_add_media, mediaType); 3154 break; 3155 case WorkingMessage.IMAGE_TOO_LARGE: 3156 title = res.getString(R.string.failed_to_resize_image); 3157 message = res.getString(R.string.resize_image_error_information); 3158 break; 3159 default: 3160 throw new IllegalArgumentException("unknown error " + error); 3161 } 3162 3163 MessageUtils.showErrorDialog(ComposeMessageActivity.this, title, message); 3164 } 3165 }); 3166 } 3167 addImageAsync(final Uri uri, final boolean append)3168 private void addImageAsync(final Uri uri, final boolean append) { 3169 getAsyncDialog().runAsync(new Runnable() { 3170 @Override 3171 public void run() { 3172 addImage(uri, append); 3173 } 3174 }, null, R.string.adding_attachments_title); 3175 } 3176 addImage(Uri uri, boolean append)3177 private void addImage(Uri uri, boolean append) { 3178 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3179 log("addImage: append=" + append + ", uri=" + uri); 3180 } 3181 3182 int result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, uri, append); 3183 3184 if (result == WorkingMessage.IMAGE_TOO_LARGE || 3185 result == WorkingMessage.MESSAGE_SIZE_EXCEEDED) { 3186 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3187 log("resize image " + uri); 3188 } 3189 MessageUtils.resizeImageAsync(ComposeMessageActivity.this, 3190 uri, mAttachmentEditorHandler, mResizeImageCallback, append); 3191 return; 3192 } 3193 handleAddAttachmentError(result, R.string.type_picture); 3194 } 3195 addVideoAsync(final Uri uri, final boolean append)3196 private void addVideoAsync(final Uri uri, final boolean append) { 3197 getAsyncDialog().runAsync(new Runnable() { 3198 @Override 3199 public void run() { 3200 addVideo(uri, append); 3201 } 3202 }, null, R.string.adding_attachments_title); 3203 } 3204 addVideo(Uri uri, boolean append)3205 private void addVideo(Uri uri, boolean append) { 3206 if (uri != null) { 3207 int result = mWorkingMessage.setAttachment(WorkingMessage.VIDEO, uri, append); 3208 handleAddAttachmentError(result, R.string.type_video); 3209 } 3210 } 3211 addAudio(Uri uri)3212 private void addAudio(Uri uri) { 3213 int result = mWorkingMessage.setAttachment(WorkingMessage.AUDIO, uri, false); 3214 handleAddAttachmentError(result, R.string.type_audio); 3215 } 3216 getAsyncDialog()3217 AsyncDialog getAsyncDialog() { 3218 if (mAsyncDialog == null) { 3219 mAsyncDialog = new AsyncDialog(this); 3220 } 3221 return mAsyncDialog; 3222 } 3223 handleForwardedMessage()3224 private boolean handleForwardedMessage() { 3225 Intent intent = getIntent(); 3226 3227 // If this is a forwarded message, it will have an Intent extra 3228 // indicating so. If not, bail out. 3229 if (!mForwardMessageMode) { 3230 return false; 3231 } 3232 3233 Uri uri = intent.getParcelableExtra("msg_uri"); 3234 3235 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 3236 log("" + uri); 3237 } 3238 3239 if (uri != null) { 3240 mWorkingMessage = WorkingMessage.load(this, uri); 3241 mWorkingMessage.setSubject(intent.getStringExtra("subject"), false); 3242 } else { 3243 mWorkingMessage.setText(intent.getStringExtra("sms_body")); 3244 } 3245 3246 // let's clear the message thread for forwarded messages 3247 mMsgListAdapter.changeCursor(null); 3248 3249 return true; 3250 } 3251 3252 // Handle send actions, where we're told to send a picture(s) or text. handleSendIntent()3253 private boolean handleSendIntent() { 3254 Intent intent = getIntent(); 3255 Bundle extras = intent.getExtras(); 3256 if (extras == null) { 3257 return false; 3258 } 3259 3260 final String mimeType = intent.getType(); 3261 String action = intent.getAction(); 3262 if (Intent.ACTION_SEND.equals(action)) { 3263 if (extras.containsKey(Intent.EXTRA_STREAM)) { 3264 final Uri uri = (Uri)extras.getParcelable(Intent.EXTRA_STREAM); 3265 getAsyncDialog().runAsync(new Runnable() { 3266 @Override 3267 public void run() { 3268 addAttachment(mimeType, uri, false); 3269 } 3270 }, null, R.string.adding_attachments_title); 3271 return true; 3272 } else if (extras.containsKey(Intent.EXTRA_TEXT)) { 3273 mWorkingMessage.setText(extras.getString(Intent.EXTRA_TEXT)); 3274 return true; 3275 } 3276 } else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && 3277 extras.containsKey(Intent.EXTRA_STREAM)) { 3278 SlideshowModel slideShow = mWorkingMessage.getSlideshow(); 3279 final ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM); 3280 int currentSlideCount = slideShow != null ? slideShow.size() : 0; 3281 int importCount = uris.size(); 3282 if (importCount + currentSlideCount > SlideshowEditor.MAX_SLIDE_NUM) { 3283 importCount = Math.min(SlideshowEditor.MAX_SLIDE_NUM - currentSlideCount, 3284 importCount); 3285 Toast.makeText(ComposeMessageActivity.this, 3286 getString(R.string.too_many_attachments, 3287 SlideshowEditor.MAX_SLIDE_NUM, importCount), 3288 Toast.LENGTH_LONG).show(); 3289 } 3290 3291 // Attach all the pictures/videos asynchronously off of the UI thread. 3292 // Show a progress dialog if adding all the slides hasn't finished 3293 // within half a second. 3294 final int numberToImport = importCount; 3295 getAsyncDialog().runAsync(new Runnable() { 3296 @Override 3297 public void run() { 3298 for (int i = 0; i < numberToImport; i++) { 3299 Parcelable uri = uris.get(i); 3300 addAttachment(mimeType, (Uri) uri, true); 3301 } 3302 } 3303 }, null, R.string.adding_attachments_title); 3304 return true; 3305 } 3306 return false; 3307 } 3308 3309 // mVideoUri will look like this: content://media/external/video/media 3310 private static final String mVideoUri = Video.Media.getContentUri("external").toString(); 3311 // mImageUri will look like this: content://media/external/images/media 3312 private static final String mImageUri = Images.Media.getContentUri("external").toString(); 3313 addAttachment(String type, Uri uri, boolean append)3314 private void addAttachment(String type, Uri uri, boolean append) { 3315 if (uri != null) { 3316 // When we're handling Intent.ACTION_SEND_MULTIPLE, the passed in items can be 3317 // videos, and/or images, and/or some other unknown types we don't handle. When 3318 // a single attachment is "shared" the type will specify an image or video. When 3319 // there are multiple types, the type passed in is "*/*". In that case, we've got 3320 // to look at the uri to figure out if it is an image or video. 3321 boolean wildcard = "*/*".equals(type); 3322 if (type.startsWith("image/") || (wildcard && uri.toString().startsWith(mImageUri))) { 3323 addImage(uri, append); 3324 } else if (type.startsWith("video/") || 3325 (wildcard && uri.toString().startsWith(mVideoUri))) { 3326 addVideo(uri, append); 3327 } 3328 } 3329 } 3330 getResourcesString(int id, String mediaName)3331 private String getResourcesString(int id, String mediaName) { 3332 Resources r = getResources(); 3333 return r.getString(id, mediaName); 3334 } 3335 3336 /** 3337 * draw the compose view at the bottom of the screen. 3338 */ drawBottomPanel()3339 private void drawBottomPanel() { 3340 // Reset the counter for text editor. 3341 resetCounter(); 3342 3343 if (mWorkingMessage.hasSlideshow()) { 3344 mBottomPanel.setVisibility(View.GONE); 3345 mAttachmentEditor.requestFocus(); 3346 return; 3347 } 3348 3349 if (LOCAL_LOGV) { 3350 Log.v(TAG, "CMA.drawBottomPanel"); 3351 } 3352 mBottomPanel.setVisibility(View.VISIBLE); 3353 3354 CharSequence text = mWorkingMessage.getText(); 3355 3356 // TextView.setTextKeepState() doesn't like null input. 3357 if (text != null && mIsSmsEnabled) { 3358 mTextEditor.setTextKeepState(text); 3359 3360 // Set the edit caret to the end of the text. 3361 mTextEditor.setSelection(mTextEditor.length()); 3362 } else { 3363 mTextEditor.setText(""); 3364 } 3365 onKeyboardStateChanged(); 3366 } 3367 hideBottomPanel()3368 private void hideBottomPanel() { 3369 if (LOCAL_LOGV) { 3370 Log.v(TAG, "CMA.hideBottomPanel"); 3371 } 3372 mBottomPanel.setVisibility(View.INVISIBLE); 3373 } 3374 drawTopPanel(boolean showSubjectEditor)3375 private void drawTopPanel(boolean showSubjectEditor) { 3376 boolean showingAttachment = mAttachmentEditor.update(mWorkingMessage); 3377 mAttachmentEditorScrollView.setVisibility(showingAttachment ? View.VISIBLE : View.GONE); 3378 showSubjectEditor(showSubjectEditor || mWorkingMessage.hasSubject()); 3379 3380 invalidateOptionsMenu(); 3381 onKeyboardStateChanged(); 3382 } 3383 3384 //========================================================== 3385 // Interface methods 3386 //========================================================== 3387 3388 @Override onClick(View v)3389 public void onClick(View v) { 3390 if ((v == mSendButtonSms || v == mSendButtonMms) && isPreparedForSending()) { 3391 confirmSendMessageIfNeeded(); 3392 } else if ((v == mRecipientsPicker)) { 3393 launchMultiplePhonePicker(); 3394 } 3395 } 3396 launchMultiplePhonePicker()3397 private void launchMultiplePhonePicker() { 3398 Intent intent = new Intent(Intents.ACTION_GET_MULTIPLE_PHONES); 3399 intent.addCategory("android.intent.category.DEFAULT"); 3400 intent.setType(Phone.CONTENT_TYPE); 3401 // We have to wait for the constructing complete. 3402 ContactList contacts = mRecipientsEditor.constructContactsFromInput(true); 3403 int urisCount = 0; 3404 Uri[] uris = new Uri[contacts.size()]; 3405 urisCount = 0; 3406 for (Contact contact : contacts) { 3407 if (Contact.CONTACT_METHOD_TYPE_PHONE == contact.getContactMethodType()) { 3408 uris[urisCount++] = contact.getPhoneUri(); 3409 } 3410 } 3411 if (urisCount > 0) { 3412 intent.putExtra(Intents.EXTRA_PHONE_URIS, uris); 3413 } 3414 startActivityForResult(intent, REQUEST_CODE_PICK); 3415 } 3416 3417 @Override onEditorAction(TextView v, int actionId, KeyEvent event)3418 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 3419 if (event != null) { 3420 // if shift key is down, then we want to insert the '\n' char in the TextView; 3421 // otherwise, the default action is to send the message. 3422 if (!event.isShiftPressed() && event.getAction() == KeyEvent.ACTION_DOWN) { 3423 if (isPreparedForSending()) { 3424 confirmSendMessageIfNeeded(); 3425 } 3426 return true; 3427 } 3428 return false; 3429 } 3430 3431 if (isPreparedForSending()) { 3432 confirmSendMessageIfNeeded(); 3433 } 3434 return true; 3435 } 3436 3437 private final TextWatcher mTextEditorWatcher = new TextWatcher() { 3438 @Override 3439 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 3440 } 3441 3442 @Override 3443 public void onTextChanged(CharSequence s, int start, int before, int count) { 3444 // This is a workaround for bug 1609057. Since onUserInteraction() is 3445 // not called when the user touches the soft keyboard, we pretend it was 3446 // called when textfields changes. This should be removed when the bug 3447 // is fixed. 3448 onUserInteraction(); 3449 3450 mWorkingMessage.setText(s); 3451 3452 updateSendButtonState(); 3453 3454 updateCounter(s, start, before, count); 3455 3456 ensureCorrectButtonHeight(); 3457 } 3458 3459 @Override 3460 public void afterTextChanged(Editable s) { 3461 } 3462 }; 3463 3464 /** 3465 * Ensures that if the text edit box extends past two lines then the 3466 * button will be shifted up to allow enough space for the character 3467 * counter string to be placed beneath it. 3468 */ ensureCorrectButtonHeight()3469 private void ensureCorrectButtonHeight() { 3470 int currentTextLines = mTextEditor.getLineCount(); 3471 if (currentTextLines <= 2) { 3472 mTextCounter.setVisibility(View.GONE); 3473 } 3474 else if (currentTextLines > 2 && mTextCounter.getVisibility() == View.GONE) { 3475 // Making the counter invisible ensures that it is used to correctly 3476 // calculate the position of the send button even if we choose not to 3477 // display the text. 3478 mTextCounter.setVisibility(View.INVISIBLE); 3479 } 3480 } 3481 3482 private final TextWatcher mSubjectEditorWatcher = new TextWatcher() { 3483 @Override 3484 public void beforeTextChanged(CharSequence s, int start, int count, int after) { } 3485 3486 @Override 3487 public void onTextChanged(CharSequence s, int start, int before, int count) { 3488 mWorkingMessage.setSubject(s, true); 3489 updateSendButtonState(); 3490 } 3491 3492 @Override 3493 public void afterTextChanged(Editable s) { } 3494 }; 3495 3496 //========================================================== 3497 // Private methods 3498 //========================================================== 3499 3500 /** 3501 * Initialize all UI elements from resources. 3502 */ initResourceRefs()3503 private void initResourceRefs() { 3504 mMsgListView = (MessageListView) findViewById(R.id.history); 3505 mMsgListView.setDivider(null); // no divider so we look like IM conversation. 3506 3507 // called to enable us to show some padding between the message list and the 3508 // input field but when the message list is scrolled that padding area is filled 3509 // in with message content 3510 mMsgListView.setClipToPadding(false); 3511 3512 mMsgListView.setOnSizeChangedListener(new OnSizeChangedListener() { 3513 public void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { 3514 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3515 Log.v(TAG, "onSizeChanged: w=" + width + " h=" + height + 3516 " oldw=" + oldWidth + " oldh=" + oldHeight); 3517 } 3518 3519 if (!mMessagesAndDraftLoaded && (oldHeight-height > SMOOTH_SCROLL_THRESHOLD)) { 3520 // perform the delayed loading now, after keyboard opens 3521 loadMessagesAndDraft(3); 3522 } 3523 3524 3525 // The message list view changed size, most likely because the keyboard 3526 // appeared or disappeared or the user typed/deleted chars in the message 3527 // box causing it to change its height when expanding/collapsing to hold more 3528 // lines of text. 3529 smoothScrollToEnd(false, height - oldHeight); 3530 } 3531 }); 3532 3533 mBottomPanel = findViewById(R.id.bottom_panel); 3534 mTextEditor = (EditText) findViewById(R.id.embedded_text_editor); 3535 mTextEditor.setOnEditorActionListener(this); 3536 mTextEditor.addTextChangedListener(mTextEditorWatcher); 3537 mTextEditor.setFilters(new InputFilter[] { 3538 new LengthFilter(MmsConfig.getMaxTextLimit())}); 3539 mTextCounter = (TextView) findViewById(R.id.text_counter); 3540 mSendButtonMms = (TextView) findViewById(R.id.send_button_mms); 3541 mSendButtonSms = (ImageButton) findViewById(R.id.send_button_sms); 3542 mSendButtonMms.setOnClickListener(this); 3543 mSendButtonSms.setOnClickListener(this); 3544 mTopPanel = findViewById(R.id.recipients_subject_linear); 3545 mTopPanel.setFocusable(false); 3546 mAttachmentEditor = (AttachmentEditor) findViewById(R.id.attachment_editor); 3547 mAttachmentEditor.setHandler(mAttachmentEditorHandler); 3548 mAttachmentEditorScrollView = findViewById(R.id.attachment_editor_scroll_view); 3549 } 3550 confirmDeleteDialog(OnClickListener listener, boolean locked)3551 private void confirmDeleteDialog(OnClickListener listener, boolean locked) { 3552 AlertDialog.Builder builder = new AlertDialog.Builder(this); 3553 builder.setCancelable(true); 3554 builder.setMessage(locked ? R.string.confirm_delete_locked_message : 3555 R.string.confirm_delete_message); 3556 builder.setPositiveButton(R.string.delete, listener); 3557 builder.setNegativeButton(R.string.no, null); 3558 builder.show(); 3559 } 3560 undeliveredMessageDialog(long date)3561 void undeliveredMessageDialog(long date) { 3562 String body; 3563 3564 if (date >= 0) { 3565 body = getString(R.string.undelivered_msg_dialog_body, 3566 MessageUtils.formatTimeStampString(this, date)); 3567 } else { 3568 // FIXME: we can not get sms retry time. 3569 body = getString(R.string.undelivered_sms_dialog_body); 3570 } 3571 3572 Toast.makeText(this, body, Toast.LENGTH_LONG).show(); 3573 } 3574 startMsgListQuery()3575 private void startMsgListQuery() { 3576 startMsgListQuery(MESSAGE_LIST_QUERY_TOKEN); 3577 } 3578 startMsgListQuery(int token)3579 private void startMsgListQuery(int token) { 3580 if (mSendDiscreetMode) { 3581 return; 3582 } 3583 Uri conversationUri = mConversation.getUri(); 3584 3585 if (conversationUri == null) { 3586 log("##### startMsgListQuery: conversationUri is null, bail!"); 3587 return; 3588 } 3589 3590 long threadId = mConversation.getThreadId(); 3591 if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3592 log("startMsgListQuery for " + conversationUri + ", threadId=" + threadId + 3593 " token: " + token + " mConversation: " + mConversation); 3594 } 3595 3596 // Cancel any pending queries 3597 mBackgroundQueryHandler.cancelOperation(token); 3598 try { 3599 // Kick off the new query 3600 mBackgroundQueryHandler.startQuery( 3601 token, 3602 threadId /* cookie */, 3603 conversationUri, 3604 PROJECTION, 3605 null, null, null); 3606 } catch (SQLiteException e) { 3607 SqliteWrapper.checkSQLiteException(this, e); 3608 } 3609 } 3610 initMessageList()3611 private void initMessageList() { 3612 if (mMsgListAdapter != null) { 3613 return; 3614 } 3615 3616 String highlightString = getIntent().getStringExtra("highlight"); 3617 Pattern highlight = highlightString == null 3618 ? null 3619 : Pattern.compile("\\b" + Pattern.quote(highlightString), Pattern.CASE_INSENSITIVE); 3620 3621 // Initialize the list adapter with a null cursor. 3622 mMsgListAdapter = new MessageListAdapter(this, null, mMsgListView, true, highlight); 3623 mMsgListAdapter.setOnDataSetChangedListener(mDataSetChangedListener); 3624 mMsgListAdapter.setMsgListItemHandler(mMessageListItemHandler); 3625 mMsgListView.setAdapter(mMsgListAdapter); 3626 mMsgListView.setItemsCanFocus(false); 3627 mMsgListView.setVisibility(mSendDiscreetMode ? View.INVISIBLE : View.VISIBLE); 3628 mMsgListView.setOnCreateContextMenuListener(mMsgListMenuCreateListener); 3629 mMsgListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 3630 @Override 3631 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 3632 if (view != null) { 3633 ((MessageListItem) view).onMessageListItemClick(); 3634 } 3635 } 3636 }); 3637 } 3638 3639 /** 3640 * Load the draft 3641 * 3642 * If mWorkingMessage has content in memory that's worth saving, return false. 3643 * Otherwise, call the async operation to load draft and return true. 3644 */ loadDraft()3645 private boolean loadDraft() { 3646 if (mWorkingMessage.isWorthSaving()) { 3647 Log.w(TAG, "CMA.loadDraft: called with non-empty working message, bail"); 3648 return false; 3649 } 3650 3651 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3652 log("CMA.loadDraft"); 3653 } 3654 3655 mWorkingMessage = WorkingMessage.loadDraft(this, mConversation, 3656 new Runnable() { 3657 @Override 3658 public void run() { 3659 drawTopPanel(false); 3660 drawBottomPanel(); 3661 updateSendButtonState(); 3662 } 3663 }); 3664 3665 // WorkingMessage.loadDraft() can return a new WorkingMessage object that doesn't 3666 // have its conversation set. Make sure it is set. 3667 mWorkingMessage.setConversation(mConversation); 3668 3669 return true; 3670 } 3671 saveDraft(boolean isStopping)3672 private void saveDraft(boolean isStopping) { 3673 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3674 LogTag.debug("saveDraft"); 3675 } 3676 // TODO: Do something better here. Maybe make discard() legal 3677 // to call twice and make isEmpty() return true if discarded 3678 // so it is caught in the clause above this one? 3679 if (mWorkingMessage.isDiscarded()) { 3680 return; 3681 } 3682 3683 if (!mWaitingForSubActivity && 3684 !mWorkingMessage.isWorthSaving() && 3685 (!isRecipientsEditorVisible() || recipientCount() == 0)) { 3686 if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3687 log("not worth saving, discard WorkingMessage and bail"); 3688 } 3689 mWorkingMessage.discard(); 3690 return; 3691 } 3692 3693 mWorkingMessage.saveDraft(isStopping); 3694 3695 if (mToastForDraftSave) { 3696 Toast.makeText(this, R.string.message_saved_as_draft, 3697 Toast.LENGTH_SHORT).show(); 3698 } 3699 } 3700 isPreparedForSending()3701 private boolean isPreparedForSending() { 3702 int recipientCount = recipientCount(); 3703 3704 return recipientCount > 0 && 3705 recipientCount <= MmsConfig.getRecipientLimit() && 3706 mIsSmsEnabled && 3707 (mWorkingMessage.hasAttachment() || mWorkingMessage.hasText() || 3708 mWorkingMessage.hasSubject()); 3709 } 3710 recipientCount()3711 private int recipientCount() { 3712 int recipientCount; 3713 3714 // To avoid creating a bunch of invalid Contacts when the recipients 3715 // editor is in flux, we keep the recipients list empty. So if the 3716 // recipients editor is showing, see if there is anything in it rather 3717 // than consulting the empty recipient list. 3718 if (isRecipientsEditorVisible()) { 3719 recipientCount = mRecipientsEditor.getRecipientCount(); 3720 } else { 3721 recipientCount = getRecipients().size(); 3722 } 3723 return recipientCount; 3724 } 3725 sendMessage(boolean bCheckEcmMode)3726 private void sendMessage(boolean bCheckEcmMode) { 3727 if (bCheckEcmMode) { 3728 // TODO: expose this in telephony layer for SDK build 3729 String inEcm = SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE); 3730 if (Boolean.parseBoolean(inEcm)) { 3731 try { 3732 startActivityForResult( 3733 new Intent(TelephonyIntents.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS, null), 3734 REQUEST_CODE_ECM_EXIT_DIALOG); 3735 return; 3736 } catch (ActivityNotFoundException e) { 3737 // continue to send message 3738 Log.e(TAG, "Cannot find EmergencyCallbackModeExitDialog", e); 3739 } 3740 } 3741 } 3742 3743 if (!mSendingMessage) { 3744 if (LogTag.SEVERE_WARNING) { 3745 String sendingRecipients = mConversation.getRecipients().serialize(); 3746 if (!sendingRecipients.equals(mDebugRecipients)) { 3747 String workingRecipients = mWorkingMessage.getWorkingRecipients(); 3748 if (!mDebugRecipients.equals(workingRecipients)) { 3749 LogTag.warnPossibleRecipientMismatch("ComposeMessageActivity.sendMessage" + 3750 " recipients in window: \"" + 3751 mDebugRecipients + "\" differ from recipients from conv: \"" + 3752 sendingRecipients + "\" and working recipients: " + 3753 workingRecipients, this); 3754 } 3755 } 3756 sanityCheckConversation(); 3757 } 3758 3759 // send can change the recipients. Make sure we remove the listeners first and then add 3760 // them back once the recipient list has settled. 3761 removeRecipientsListeners(); 3762 3763 mWorkingMessage.send(mDebugRecipients); 3764 3765 mSentMessage = true; 3766 mSendingMessage = true; 3767 addRecipientsListeners(); 3768 3769 mScrollOnSend = true; // in the next onQueryComplete, scroll the list to the end. 3770 } 3771 // But bail out if we are supposed to exit after the message is sent. 3772 if (mSendDiscreetMode) { 3773 finish(); 3774 } 3775 } 3776 resetMessage()3777 private void resetMessage() { 3778 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3779 log("resetMessage"); 3780 } 3781 3782 // Make the attachment editor hide its view. 3783 mAttachmentEditor.hideView(); 3784 mAttachmentEditorScrollView.setVisibility(View.GONE); 3785 3786 // Hide the subject editor. 3787 showSubjectEditor(false); 3788 3789 // Focus to the text editor. 3790 mTextEditor.requestFocus(); 3791 3792 // We have to remove the text change listener while the text editor gets cleared and 3793 // we subsequently turn the message back into SMS. When the listener is listening while 3794 // doing the clearing, it's fighting to update its counts and itself try and turn 3795 // the message one way or the other. 3796 mTextEditor.removeTextChangedListener(mTextEditorWatcher); 3797 3798 // Clear the text box. 3799 TextKeyListener.clear(mTextEditor.getText()); 3800 3801 mWorkingMessage.clearConversation(mConversation, false); 3802 mWorkingMessage = WorkingMessage.createEmpty(this); 3803 mWorkingMessage.setConversation(mConversation); 3804 3805 hideRecipientEditor(); 3806 drawBottomPanel(); 3807 3808 // "Or not", in this case. 3809 updateSendButtonState(); 3810 3811 // Our changes are done. Let the listener respond to text changes once again. 3812 mTextEditor.addTextChangedListener(mTextEditorWatcher); 3813 3814 // Close the soft on-screen keyboard if we're in landscape mode so the user can see the 3815 // conversation. 3816 if (mIsLandscape) { 3817 hideKeyboard(); 3818 } 3819 3820 mLastRecipientCount = 0; 3821 mSendingMessage = false; 3822 invalidateOptionsMenu(); 3823 } 3824 hideKeyboard()3825 private void hideKeyboard() { 3826 InputMethodManager inputMethodManager = 3827 (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); 3828 inputMethodManager.hideSoftInputFromWindow(mTextEditor.getWindowToken(), 0); 3829 } 3830 updateSendButtonState()3831 private void updateSendButtonState() { 3832 boolean enable = false; 3833 if (isPreparedForSending()) { 3834 // When the type of attachment is slideshow, we should 3835 // also hide the 'Send' button since the slideshow view 3836 // already has a 'Send' button embedded. 3837 if (!mWorkingMessage.hasSlideshow()) { 3838 enable = true; 3839 } else { 3840 mAttachmentEditor.setCanSend(true); 3841 } 3842 } else if (null != mAttachmentEditor){ 3843 mAttachmentEditor.setCanSend(false); 3844 } 3845 3846 boolean requiresMms = mWorkingMessage.requiresMms(); 3847 View sendButton = showSmsOrMmsSendButton(requiresMms); 3848 sendButton.setEnabled(enable); 3849 sendButton.setFocusable(enable); 3850 } 3851 getMessageDate(Uri uri)3852 private long getMessageDate(Uri uri) { 3853 if (uri != null) { 3854 Cursor cursor = SqliteWrapper.query(this, mContentResolver, 3855 uri, new String[] { Mms.DATE }, null, null, null); 3856 if (cursor != null) { 3857 try { 3858 if ((cursor.getCount() == 1) && cursor.moveToFirst()) { 3859 return cursor.getLong(0) * 1000L; 3860 } 3861 } finally { 3862 cursor.close(); 3863 } 3864 } 3865 } 3866 return NO_DATE_FOR_DIALOG; 3867 } 3868 initActivityState(Bundle bundle)3869 private void initActivityState(Bundle bundle) { 3870 Intent intent = getIntent(); 3871 if (bundle != null) { 3872 setIntent(getIntent().setAction(Intent.ACTION_VIEW)); 3873 String recipients = bundle.getString(RECIPIENTS); 3874 if (LogTag.VERBOSE) log("get mConversation by recipients " + recipients); 3875 mConversation = Conversation.get(this, 3876 ContactList.getByNumbers(recipients, 3877 false /* don't block */, true /* replace number */), false); 3878 addRecipientsListeners(); 3879 mSendDiscreetMode = bundle.getBoolean(KEY_EXIT_ON_SENT, false); 3880 mForwardMessageMode = bundle.getBoolean(KEY_FORWARDED_MESSAGE, false); 3881 3882 if (mSendDiscreetMode) { 3883 mMsgListView.setVisibility(View.INVISIBLE); 3884 } 3885 mWorkingMessage.readStateFromBundle(bundle); 3886 3887 return; 3888 } 3889 3890 // If we have been passed a thread_id, use that to find our conversation. 3891 long threadId = intent.getLongExtra(THREAD_ID, 0); 3892 if (threadId > 0) { 3893 if (LogTag.VERBOSE) log("get mConversation by threadId " + threadId); 3894 mConversation = Conversation.get(this, threadId, false); 3895 } else { 3896 Uri intentData = intent.getData(); 3897 if (intentData != null) { 3898 // try to get a conversation based on the data URI passed to our intent. 3899 if (LogTag.VERBOSE) log("get mConversation by intentData " + intentData); 3900 mConversation = Conversation.get(this, intentData, false); 3901 mWorkingMessage.setText(getBody(intentData)); 3902 } else { 3903 // special intent extra parameter to specify the address 3904 String address = intent.getStringExtra("address"); 3905 if (!TextUtils.isEmpty(address)) { 3906 if (LogTag.VERBOSE) log("get mConversation by address " + address); 3907 mConversation = Conversation.get(this, ContactList.getByNumbers(address, 3908 false /* don't block */, true /* replace number */), false); 3909 } else { 3910 if (LogTag.VERBOSE) log("create new conversation"); 3911 mConversation = Conversation.createNew(this); 3912 } 3913 } 3914 } 3915 addRecipientsListeners(); 3916 updateThreadIdIfRunning(); 3917 3918 mSendDiscreetMode = intent.getBooleanExtra(KEY_EXIT_ON_SENT, false); 3919 mForwardMessageMode = intent.getBooleanExtra(KEY_FORWARDED_MESSAGE, false); 3920 if (mSendDiscreetMode) { 3921 mMsgListView.setVisibility(View.INVISIBLE); 3922 } 3923 if (intent.hasExtra("sms_body")) { 3924 mWorkingMessage.setText(intent.getStringExtra("sms_body")); 3925 } 3926 mWorkingMessage.setSubject(intent.getStringExtra("subject"), false); 3927 } 3928 initFocus()3929 private void initFocus() { 3930 if (!mIsKeyboardOpen) { 3931 return; 3932 } 3933 3934 // If the recipients editor is visible, there is nothing in it, 3935 // and the text editor is not already focused, focus the 3936 // recipients editor. 3937 if (isRecipientsEditorVisible() 3938 && TextUtils.isEmpty(mRecipientsEditor.getText()) 3939 && !mTextEditor.isFocused()) { 3940 mRecipientsEditor.requestFocus(); 3941 return; 3942 } 3943 3944 // If we decided not to focus the recipients editor, focus the text editor. 3945 mTextEditor.requestFocus(); 3946 } 3947 3948 private final MessageListAdapter.OnDataSetChangedListener 3949 mDataSetChangedListener = new MessageListAdapter.OnDataSetChangedListener() { 3950 @Override 3951 public void onDataSetChanged(MessageListAdapter adapter) { 3952 } 3953 3954 @Override 3955 public void onContentChanged(MessageListAdapter adapter) { 3956 startMsgListQuery(); 3957 } 3958 }; 3959 3960 /** 3961 * smoothScrollToEnd will scroll the message list to the bottom if the list is already near 3962 * the bottom. Typically this is called to smooth scroll a newly received message into view. 3963 * It's also called when sending to scroll the list to the bottom, regardless of where it is, 3964 * so the user can see the just sent message. This function is also called when the message 3965 * list view changes size because the keyboard state changed or the compose message field grew. 3966 * 3967 * @param force always scroll to the bottom regardless of current list position 3968 * @param listSizeChange the amount the message list view size has vertically changed 3969 */ smoothScrollToEnd(boolean force, int listSizeChange)3970 private void smoothScrollToEnd(boolean force, int listSizeChange) { 3971 int lastItemVisible = mMsgListView.getLastVisiblePosition(); 3972 int lastItemInList = mMsgListAdapter.getCount() - 1; 3973 if (lastItemVisible < 0 || lastItemInList < 0) { 3974 if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3975 Log.v(TAG, "smoothScrollToEnd: lastItemVisible=" + lastItemVisible + 3976 ", lastItemInList=" + lastItemInList + 3977 ", mMsgListView not ready"); 3978 } 3979 return; 3980 } 3981 3982 View lastChildVisible = 3983 mMsgListView.getChildAt(lastItemVisible - mMsgListView.getFirstVisiblePosition()); 3984 int lastVisibleItemBottom = 0; 3985 int lastVisibleItemHeight = 0; 3986 if (lastChildVisible != null) { 3987 lastVisibleItemBottom = lastChildVisible.getBottom(); 3988 lastVisibleItemHeight = lastChildVisible.getHeight(); 3989 } 3990 3991 if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3992 Log.v(TAG, "smoothScrollToEnd newPosition: " + lastItemInList + 3993 " mLastSmoothScrollPosition: " + mLastSmoothScrollPosition + 3994 " first: " + mMsgListView.getFirstVisiblePosition() + 3995 " lastItemVisible: " + lastItemVisible + 3996 " lastVisibleItemBottom: " + lastVisibleItemBottom + 3997 " lastVisibleItemBottom + listSizeChange: " + 3998 (lastVisibleItemBottom + listSizeChange) + 3999 " mMsgListView.getHeight() - mMsgListView.getPaddingBottom(): " + 4000 (mMsgListView.getHeight() - mMsgListView.getPaddingBottom()) + 4001 " listSizeChange: " + listSizeChange); 4002 } 4003 // Only scroll if the list if we're responding to a newly sent message (force == true) or 4004 // the list is already scrolled to the end. This code also has to handle the case where 4005 // the listview has changed size (from the keyboard coming up or down or the message entry 4006 // field growing/shrinking) and it uses that grow/shrink factor in listSizeChange to 4007 // compute whether the list was at the end before the resize took place. 4008 // For example, when the keyboard comes up, listSizeChange will be negative, something 4009 // like -524. The lastChild listitem's bottom value will be the old value before the 4010 // keyboard became visible but the size of the list will have changed. The test below 4011 // add listSizeChange to bottom to figure out if the old position was already scrolled 4012 // to the bottom. We also scroll the list if the last item is taller than the size of the 4013 // list. This happens when the keyboard is up and the last item is an mms with an 4014 // attachment thumbnail, such as picture. In this situation, we want to scroll the list so 4015 // the bottom of the thumbnail is visible and the top of the item is scroll off the screen. 4016 int listHeight = mMsgListView.getHeight(); 4017 boolean lastItemTooTall = lastVisibleItemHeight > listHeight; 4018 boolean willScroll = force || 4019 ((listSizeChange != 0 || lastItemInList != mLastSmoothScrollPosition) && 4020 lastVisibleItemBottom + listSizeChange <= 4021 listHeight - mMsgListView.getPaddingBottom()); 4022 if (willScroll || (lastItemTooTall && lastItemInList == lastItemVisible)) { 4023 if (Math.abs(listSizeChange) > SMOOTH_SCROLL_THRESHOLD) { 4024 // When the keyboard comes up, the window manager initiates a cross fade 4025 // animation that conflicts with smooth scroll. Handle that case by jumping the 4026 // list directly to the end. 4027 if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 4028 Log.v(TAG, "keyboard state changed. setSelection=" + lastItemInList); 4029 } 4030 if (lastItemTooTall) { 4031 // If the height of the last item is taller than the whole height of the list, 4032 // we need to scroll that item so that its top is negative or above the top of 4033 // the list. That way, the bottom of the last item will be exposed above the 4034 // keyboard. 4035 mMsgListView.setSelectionFromTop(lastItemInList, 4036 listHeight - lastVisibleItemHeight); 4037 } else { 4038 mMsgListView.setSelection(lastItemInList); 4039 } 4040 } else if (lastItemInList - lastItemVisible > MAX_ITEMS_TO_INVOKE_SCROLL_SHORTCUT) { 4041 if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 4042 Log.v(TAG, "too many to scroll, setSelection=" + lastItemInList); 4043 } 4044 mMsgListView.setSelection(lastItemInList); 4045 } else { 4046 if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 4047 Log.v(TAG, "smooth scroll to " + lastItemInList); 4048 } 4049 if (lastItemTooTall) { 4050 // If the height of the last item is taller than the whole height of the list, 4051 // we need to scroll that item so that its top is negative or above the top of 4052 // the list. That way, the bottom of the last item will be exposed above the 4053 // keyboard. We should use smoothScrollToPositionFromTop here, but it doesn't 4054 // seem to work -- the list ends up scrolling to a random position. 4055 mMsgListView.setSelectionFromTop(lastItemInList, 4056 listHeight - lastVisibleItemHeight); 4057 } else { 4058 mMsgListView.smoothScrollToPosition(lastItemInList); 4059 } 4060 mLastSmoothScrollPosition = lastItemInList; 4061 } 4062 } 4063 } 4064 4065 private final class BackgroundQueryHandler extends ConversationQueryHandler { BackgroundQueryHandler(ContentResolver contentResolver)4066 public BackgroundQueryHandler(ContentResolver contentResolver) { 4067 super(contentResolver); 4068 } 4069 4070 @Override onQueryComplete(int token, Object cookie, Cursor cursor)4071 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 4072 switch(token) { 4073 case MESSAGE_LIST_QUERY_TOKEN: 4074 mConversation.blockMarkAsRead(false); 4075 4076 // check consistency between the query result and 'mConversation' 4077 long tid = (Long) cookie; 4078 4079 if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 4080 log("##### onQueryComplete: msg history result for threadId " + tid); 4081 } 4082 if (tid != mConversation.getThreadId()) { 4083 log("onQueryComplete: msg history query result is for threadId " + 4084 tid + ", but mConversation has threadId " + 4085 mConversation.getThreadId() + " starting a new query"); 4086 if (cursor != null) { 4087 cursor.close(); 4088 } 4089 startMsgListQuery(); 4090 return; 4091 } 4092 4093 // check consistency b/t mConversation & mWorkingMessage.mConversation 4094 ComposeMessageActivity.this.sanityCheckConversation(); 4095 4096 int newSelectionPos = -1; 4097 long targetMsgId = getIntent().getLongExtra("select_id", -1); 4098 if (targetMsgId != -1) { 4099 if (cursor != null) { 4100 cursor.moveToPosition(-1); 4101 while (cursor.moveToNext()) { 4102 long msgId = cursor.getLong(COLUMN_ID); 4103 if (msgId == targetMsgId) { 4104 newSelectionPos = cursor.getPosition(); 4105 break; 4106 } 4107 } 4108 } 4109 } else if (mSavedScrollPosition != -1) { 4110 // mSavedScrollPosition is set when this activity pauses. If equals maxint, 4111 // it means the message list was scrolled to the end. Meanwhile, messages 4112 // could have been received. When the activity resumes and we were 4113 // previously scrolled to the end, jump the list so any new messages are 4114 // visible. 4115 if (mSavedScrollPosition == Integer.MAX_VALUE) { 4116 int cnt = mMsgListAdapter.getCount(); 4117 if (cnt > 0) { 4118 // Have to wait until the adapter is loaded before jumping to 4119 // the end. 4120 newSelectionPos = cnt - 1; 4121 mSavedScrollPosition = -1; 4122 } 4123 } else { 4124 // remember the saved scroll position before the activity is paused. 4125 // reset it after the message list query is done 4126 newSelectionPos = mSavedScrollPosition; 4127 mSavedScrollPosition = -1; 4128 } 4129 } 4130 4131 mMsgListAdapter.changeCursor(cursor); 4132 4133 if (newSelectionPos != -1) { 4134 mMsgListView.setSelection(newSelectionPos); // jump the list to the pos 4135 } else { 4136 int count = mMsgListAdapter.getCount(); 4137 long lastMsgId = 0; 4138 if (cursor != null && count > 0) { 4139 cursor.moveToLast(); 4140 lastMsgId = cursor.getLong(COLUMN_ID); 4141 } 4142 // mScrollOnSend is set when we send a message. We always want to scroll 4143 // the message list to the end when we send a message, but have to wait 4144 // until the DB has changed. We also want to scroll the list when a 4145 // new message has arrived. 4146 smoothScrollToEnd(mScrollOnSend || lastMsgId != mLastMessageId, 0); 4147 mLastMessageId = lastMsgId; 4148 mScrollOnSend = false; 4149 } 4150 // Adjust the conversation's message count to match reality. The 4151 // conversation's message count is eventually used in 4152 // WorkingMessage.clearConversation to determine whether to delete 4153 // the conversation or not. 4154 mConversation.setMessageCount(mMsgListAdapter.getCount()); 4155 4156 // Once we have completed the query for the message history, if 4157 // there is nothing in the cursor and we are not composing a new 4158 // message, we must be editing a draft in a new conversation (unless 4159 // mSentMessage is true). 4160 // Show the recipients editor to give the user a chance to add 4161 // more people before the conversation begins. 4162 if (cursor != null && cursor.getCount() == 0 4163 && !isRecipientsEditorVisible() && !mSentMessage) { 4164 initRecipientsEditor(); 4165 } 4166 4167 // FIXME: freshing layout changes the focused view to an unexpected 4168 // one, set it back to TextEditor forcely. 4169 mTextEditor.requestFocus(); 4170 4171 invalidateOptionsMenu(); // some menu items depend on the adapter's count 4172 return; 4173 4174 case ConversationList.HAVE_LOCKED_MESSAGES_TOKEN: 4175 if (ComposeMessageActivity.this.isFinishing()) { 4176 Log.w(TAG, "ComposeMessageActivity is finished, do nothing "); 4177 if (cursor != null) { 4178 cursor.close(); 4179 } 4180 return ; 4181 } 4182 @SuppressWarnings("unchecked") 4183 ArrayList<Long> threadIds = (ArrayList<Long>)cookie; 4184 ConversationList.confirmDeleteThreadDialog( 4185 new ConversationList.DeleteThreadListener(threadIds, 4186 mBackgroundQueryHandler, ComposeMessageActivity.this), 4187 threadIds, 4188 cursor != null && cursor.getCount() > 0, 4189 ComposeMessageActivity.this); 4190 if (cursor != null) { 4191 cursor.close(); 4192 } 4193 break; 4194 4195 case MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN: 4196 // check consistency between the query result and 'mConversation' 4197 tid = (Long) cookie; 4198 4199 if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 4200 log("##### onQueryComplete (after delete): msg history result for threadId " 4201 + tid); 4202 } 4203 if (cursor == null) { 4204 return; 4205 } 4206 if (tid > 0 && cursor.getCount() == 0) { 4207 // We just deleted the last message and the thread will get deleted 4208 // by a trigger in the database. Clear the threadId so next time we 4209 // need the threadId a new thread will get created. 4210 log("##### MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN clearing thread id: " 4211 + tid); 4212 Conversation conv = Conversation.get(ComposeMessageActivity.this, tid, 4213 false); 4214 if (conv != null) { 4215 conv.clearThreadId(); 4216 conv.setDraftState(false); 4217 } 4218 // The last message in this converation was just deleted. Send the user 4219 // to the conversation list. 4220 exitComposeMessageActivity(new Runnable() { 4221 @Override 4222 public void run() { 4223 goToConversationList(); 4224 } 4225 }); 4226 } 4227 cursor.close(); 4228 } 4229 } 4230 4231 @Override onDeleteComplete(int token, Object cookie, int result)4232 protected void onDeleteComplete(int token, Object cookie, int result) { 4233 super.onDeleteComplete(token, cookie, result); 4234 switch(token) { 4235 case ConversationList.DELETE_CONVERSATION_TOKEN: 4236 mConversation.setMessageCount(0); 4237 // fall through 4238 case DELETE_MESSAGE_TOKEN: 4239 if (cookie instanceof Boolean && ((Boolean)cookie).booleanValue()) { 4240 // If we just deleted the last message, reset the saved id. 4241 mLastMessageId = 0; 4242 } 4243 // Update the notification for new messages since they 4244 // may be deleted. 4245 MessagingNotification.nonBlockingUpdateNewMessageIndicator( 4246 ComposeMessageActivity.this, MessagingNotification.THREAD_NONE, false); 4247 // Update the notification for failed messages since they 4248 // may be deleted. 4249 updateSendFailedNotification(); 4250 break; 4251 } 4252 // If we're deleting the whole conversation, throw away 4253 // our current working message and bail. 4254 if (token == ConversationList.DELETE_CONVERSATION_TOKEN) { 4255 ContactList recipients = mConversation.getRecipients(); 4256 mWorkingMessage.discard(); 4257 4258 // Remove any recipients referenced by this single thread from the 4259 // contacts cache. It's possible for two or more threads to reference 4260 // the same contact. That's ok if we remove it. We'll recreate that contact 4261 // when we init all Conversations below. 4262 if (recipients != null) { 4263 for (Contact contact : recipients) { 4264 contact.removeFromCache(); 4265 } 4266 } 4267 4268 // Make sure the conversation cache reflects the threads in the DB. 4269 Conversation.init(ComposeMessageActivity.this); 4270 finish(); 4271 } else if (token == DELETE_MESSAGE_TOKEN) { 4272 // Check to see if we just deleted the last message 4273 startMsgListQuery(MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN); 4274 } 4275 4276 MmsWidgetProvider.notifyDatasetChanged(getApplicationContext()); 4277 } 4278 } 4279 4280 @Override onUpdate(final Contact updated)4281 public void onUpdate(final Contact updated) { 4282 // Using an existing handler for the post, rather than conjuring up a new one. 4283 mMessageListItemHandler.post(new Runnable() { 4284 @Override 4285 public void run() { 4286 ContactList recipients = isRecipientsEditorVisible() ? 4287 mRecipientsEditor.constructContactsFromInput(false) : getRecipients(); 4288 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 4289 log("[CMA] onUpdate contact updated: " + updated); 4290 log("[CMA] onUpdate recipients: " + recipients); 4291 } 4292 updateTitle(recipients); 4293 4294 // The contact information for one (or more) of the recipients has changed. 4295 // Rebuild the message list so each MessageItem will get the last contact info. 4296 ComposeMessageActivity.this.mMsgListAdapter.notifyDataSetChanged(); 4297 4298 // Don't do this anymore. When we're showing chips, we don't want to switch from 4299 // chips to text. 4300 // if (mRecipientsEditor != null) { 4301 // mRecipientsEditor.populate(recipients); 4302 // } 4303 } 4304 }); 4305 } 4306 addRecipientsListeners()4307 private void addRecipientsListeners() { 4308 Contact.addListener(this); 4309 } 4310 removeRecipientsListeners()4311 private void removeRecipientsListeners() { 4312 Contact.removeListener(this); 4313 } 4314 createIntent(Context context, long threadId)4315 public static Intent createIntent(Context context, long threadId) { 4316 Intent intent = new Intent(context, ComposeMessageActivity.class); 4317 4318 if (threadId > 0) { 4319 intent.setData(Conversation.getUri(threadId)); 4320 } 4321 4322 return intent; 4323 } 4324 getBody(Uri uri)4325 private String getBody(Uri uri) { 4326 if (uri == null) { 4327 return null; 4328 } 4329 String urlStr = uri.getSchemeSpecificPart(); 4330 if (!urlStr.contains("?")) { 4331 return null; 4332 } 4333 urlStr = urlStr.substring(urlStr.indexOf('?') + 1); 4334 String[] params = urlStr.split("&"); 4335 for (String p : params) { 4336 if (p.startsWith("body=")) { 4337 try { 4338 return URLDecoder.decode(p.substring(5), "UTF-8"); 4339 } catch (UnsupportedEncodingException e) { } 4340 } 4341 } 4342 return null; 4343 } 4344 updateThreadIdIfRunning()4345 private void updateThreadIdIfRunning() { 4346 if (mIsRunning && mConversation != null) { 4347 if (DEBUG) { 4348 Log.v(TAG, "updateThreadIdIfRunning: threadId: " + 4349 mConversation.getThreadId()); 4350 } 4351 MessagingNotification.setCurrentlyDisplayedThreadId(mConversation.getThreadId()); 4352 } else { 4353 if (DEBUG) { 4354 Log.v(TAG, "updateThreadIdIfRunning: mIsRunning: " + mIsRunning + 4355 " mConversation: " + mConversation); 4356 } 4357 } 4358 // If we're not running, but resume later, the current thread ID will be set in onResume() 4359 } 4360 } 4361