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