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