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