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