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