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