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