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