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