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