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