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