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