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