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