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