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