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