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