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