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