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