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