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