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