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