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