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