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