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