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