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