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