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