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