ComposeMessageActivity.java revision 458dea8d1858e418b6918f477d0139335b14ff45
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 com.android.mms.MmsConfig; 30import com.android.mms.R; 31import com.android.mms.data.Contact; 32import com.android.mms.data.ContactList; 33import com.android.mms.data.Conversation; 34import com.android.mms.data.WorkingMessage; 35import com.android.mms.data.WorkingMessage.MessageStatusListener; 36import com.android.mms.model.SlideshowModel; 37import com.android.mms.transaction.MessagingNotification; 38import com.android.mms.ui.MessageUtils.ResizeImageResultCallback; 39import com.android.mms.ui.RecipientsEditor.RecipientContextMenuInfo; 40import com.android.mms.util.SendingProgressTokenManager; 41import com.android.mms.util.SmileyParser; 42 43import com.google.android.mms.ContentType; 44import com.google.android.mms.MmsException; 45import com.google.android.mms.pdu.EncodedStringValue; 46import com.google.android.mms.pdu.PduBody; 47import com.google.android.mms.pdu.PduPart; 48import com.google.android.mms.pdu.PduPersister; 49import com.google.android.mms.pdu.SendReq; 50import com.google.android.mms.util.SqliteWrapper; 51 52import android.app.Activity; 53import android.app.AlertDialog; 54import android.content.AsyncQueryHandler; 55import android.content.BroadcastReceiver; 56import android.content.ContentResolver; 57import android.content.ContentUris; 58import android.content.ContentValues; 59import android.content.Context; 60import android.content.DialogInterface; 61import android.content.Intent; 62import android.content.IntentFilter; 63import android.content.DialogInterface.OnClickListener; 64import android.content.res.Configuration; 65import android.content.res.Resources; 66import android.database.ContentObserver; 67import android.database.Cursor; 68import android.database.DatabaseUtils; 69import android.database.sqlite.SQLiteException; 70import android.graphics.Bitmap; 71import android.graphics.drawable.Drawable; 72import android.media.RingtoneManager; 73import android.net.Uri; 74import android.os.Bundle; 75import android.os.Handler; 76import android.os.Message; 77import android.provider.Contacts; 78import android.provider.Contacts.People; 79import android.provider.Contacts.Presence; 80import android.provider.MediaStore; 81import android.provider.Settings; 82import android.provider.Telephony.Mms; 83import android.provider.Telephony.Sms; 84import android.telephony.SmsMessage; 85import android.text.ClipboardManager; 86import android.text.Editable; 87import android.text.InputFilter; 88import android.text.SpannableString; 89import android.text.Spanned; 90import android.text.TextUtils; 91import android.text.TextWatcher; 92import android.text.method.TextKeyListener; 93import android.text.style.URLSpan; 94import android.text.util.Linkify; 95import android.util.Config; 96import android.util.Log; 97import android.view.ContextMenu; 98import android.view.KeyEvent; 99import android.view.LayoutInflater; 100import android.view.Menu; 101import android.view.MenuItem; 102import android.view.View; 103import android.view.ViewStub; 104import android.view.Window; 105import android.view.ContextMenu.ContextMenuInfo; 106import android.view.View.OnCreateContextMenuListener; 107import android.view.View.OnKeyListener; 108import android.view.inputmethod.InputMethodManager; 109import android.widget.AdapterView; 110import android.widget.Button; 111import android.widget.EditText; 112import android.widget.ImageView; 113import android.widget.LinearLayout; 114import android.widget.ListView; 115import android.widget.SimpleAdapter; 116import android.widget.TextView; 117import android.widget.Toast; 118 119import java.io.InputStream; 120import java.io.IOException; 121import java.io.File; 122import java.io.FileInputStream; 123import java.io.FileOutputStream; 124import java.util.ArrayList; 125import java.util.HashMap; 126import java.util.List; 127import java.util.Map; 128 129import android.webkit.MimeTypeMap; 130 131/** 132 * This is the main UI for: 133 * 1. Composing a new message; 134 * 2. Viewing/managing message history of a conversation. 135 * 136 * This activity can handle following parameters from the intent 137 * by which it's launched. 138 * thread_id long Identify the conversation to be viewed. When creating a 139 * new message, this parameter shouldn't be present. 140 * msg_uri Uri The message which should be opened for editing in the editor. 141 * address String The addresses of the recipients in current conversation. 142 * exit_on_sent boolean Exit this activity after the message is sent. 143 */ 144public class ComposeMessageActivity extends Activity 145 implements View.OnClickListener, TextView.OnEditorActionListener, 146 MessageStatusListener, Contact.UpdateListener { 147 public static final int REQUEST_CODE_ATTACH_IMAGE = 10; 148 public static final int REQUEST_CODE_TAKE_PICTURE = 11; 149 public static final int REQUEST_CODE_ATTACH_VIDEO = 12; 150 public static final int REQUEST_CODE_TAKE_VIDEO = 13; 151 public static final int REQUEST_CODE_ATTACH_SOUND = 14; 152 public static final int REQUEST_CODE_RECORD_SOUND = 15; 153 public static final int REQUEST_CODE_CREATE_SLIDESHOW = 16; 154 155 private static final String TAG = "ComposeMessageActivity"; 156 private static final boolean DEBUG = false; 157 private static final boolean TRACE = false; 158 private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; 159 160 // Menu ID 161 private static final int MENU_ADD_SUBJECT = 0; 162 private static final int MENU_DELETE_THREAD = 1; 163 private static final int MENU_ADD_ATTACHMENT = 2; 164 private static final int MENU_DISCARD = 3; 165 private static final int MENU_SEND = 4; 166 private static final int MENU_CALL_RECIPIENT = 5; 167 private static final int MENU_CONVERSATION_LIST = 6; 168 169 // Context menu ID 170 private static final int MENU_VIEW_CONTACT = 12; 171 private static final int MENU_ADD_TO_CONTACTS = 13; 172 173 private static final int MENU_EDIT_MESSAGE = 14; 174 private static final int MENU_VIEW_SLIDESHOW = 16; 175 private static final int MENU_VIEW_MESSAGE_DETAILS = 17; 176 private static final int MENU_DELETE_MESSAGE = 18; 177 private static final int MENU_SEARCH = 19; 178 private static final int MENU_DELIVERY_REPORT = 20; 179 private static final int MENU_FORWARD_MESSAGE = 21; 180 private static final int MENU_CALL_BACK = 22; 181 private static final int MENU_SEND_EMAIL = 23; 182 private static final int MENU_COPY_MESSAGE_TEXT = 24; 183 private static final int MENU_COPY_TO_SDCARD = 25; 184 private static final int MENU_INSERT_SMILEY = 26; 185 private static final int MENU_ADD_ADDRESS_TO_CONTACTS = 27; 186 private static final int MENU_LOCK_MESSAGE = 28; 187 private static final int MENU_UNLOCK_MESSAGE = 29; 188 189 private static final int RECIPIENTS_MAX_LENGTH = 312; 190 191 private static final int MESSAGE_LIST_QUERY_TOKEN = 9527; 192 193 private static final int DELETE_MESSAGE_TOKEN = 9700; 194 private static final int DELETE_CONVERSATION_TOKEN = 9701; 195 196 private static final int CALLER_ID_QUERY_TOKEN = 9800; 197 private static final int EMAIL_CONTACT_QUERY_TOKEN = 9801; 198 199 private static final int MARK_AS_READ_TOKEN = 9900; 200 201 private static final int MMS_THRESHOLD = 4; 202 203 private static final int CHARS_REMAINING_BEFORE_COUNTER_SHOWN = 10; 204 205 private static final long NO_DATE_FOR_DIALOG = -1L; 206 207 private static final int REFRESH_PRESENCE = 45236; 208 209 210 // caller id query params 211 private static final String[] CALLER_ID_PROJECTION = new String[] { 212 People.PRESENCE_STATUS, // 0 213 }; 214 private static final int PRESENCE_STATUS_COLUMN = 0; 215 216 private static final String NUMBER_LOOKUP = "PHONE_NUMBERS_EQUAL(" 217 + Contacts.Phones.NUMBER + ",?)"; 218 private static final Uri PHONES_WITH_PRESENCE_URI 219 = Uri.parse(Contacts.Phones.CONTENT_URI + "_with_presence"); 220 221 // email contact query params 222 private static final String[] EMAIL_QUERY_PROJECTION = new String[] { 223 Contacts.People.PRESENCE_STATUS, // 0 224 }; 225 226 private static final String METHOD_LOOKUP = Contacts.ContactMethods.DATA + "=?"; 227 private static final Uri METHOD_WITH_PRESENCE_URI = 228 Uri.withAppendedPath(Contacts.ContactMethods.CONTENT_URI, "with_presence"); 229 230 231 232 private ContentResolver mContentResolver; 233 234 private BackgroundQueryHandler mBackgroundQueryHandler; 235 236 private Conversation mConversation; // Conversation we are working in 237 238 private boolean mExitOnSent; // Should we finish() after sending a message? 239 240 private View mTopPanel; // View containing the recipient and subject editors 241 private View mBottomPanel; // View containing the text editor, send button, ec. 242 private EditText mTextEditor; // Text editor to type your message into 243 private TextView mTextCounter; // Shows the number of characters used in text editor 244 private Button mSendButton; // Press to detonate 245 private EditText mSubjectTextEditor; // Text editor for MMS subject 246 247 private AttachmentEditor mAttachmentEditor; 248 249 private MessageListView mMsgListView; // ListView for messages in this conversation 250 private MessageListAdapter mMsgListAdapter; // and its corresponding ListAdapter 251 252 private RecipientsEditor mRecipientsEditor; // UI control for editing recipients 253 254 private boolean mIsKeyboardOpen; // Whether the hardware keyboard is visible 255 private boolean mIsLandscape; // Whether we're in landscape mode 256 257 private boolean mPossiblePendingNotification; // If the message list has changed, we may have 258 // a pending notification to deal with. 259 260 private boolean mToastForDraftSave; // Whether to notify the user that a draft is 261 // being saved. 262 263 private WorkingMessage mWorkingMessage; // The message currently being composed. 264 265 private AlertDialog mSmileyDialog; 266 267 // Everything needed to deal with presence 268 private Cursor mContactInfoCursor; 269 private int mPresenceStatus; 270 private String[] mContactInfoSelectionArgs = new String[1]; 271 272 private boolean mWaitingForSubActivity; 273 274 @SuppressWarnings("unused") 275 private static void log(String format, Object... args) { 276 Thread current = Thread.currentThread(); 277 long tid = current.getId(); 278 StackTraceElement[] stack = current.getStackTrace(); 279 String methodName = stack[3].getMethodName(); 280 // Prepend current thread ID and name of calling method to the message. 281 format = "[" + tid + "] [" + methodName + "] " + format; 282 String logMsg = String.format(format, args); 283 Log.d(TAG, logMsg); 284 } 285 286 //========================================================== 287 // Inner classes 288 //========================================================== 289 290 private void editSlideshow() { 291 Uri dataUri = mWorkingMessage.saveAsMms(); 292 Intent intent = new Intent(this, SlideshowEditActivity.class); 293 intent.setData(dataUri); 294 startActivityForResult(intent, REQUEST_CODE_CREATE_SLIDESHOW); 295 } 296 297 private final Handler mAttachmentEditorHandler = new Handler() { 298 @Override 299 public void handleMessage(Message msg) { 300 switch (msg.what) { 301 case AttachmentEditor.MSG_EDIT_SLIDESHOW: { 302 editSlideshow(); 303 break; 304 } 305 case AttachmentEditor.MSG_SEND_SLIDESHOW: { 306 if (isPreparedForSending()) { 307 ComposeMessageActivity.this.confirmSendMessageIfNeeded(); 308 } 309 break; 310 } 311 case AttachmentEditor.MSG_VIEW_IMAGE: 312 case AttachmentEditor.MSG_PLAY_VIDEO: 313 case AttachmentEditor.MSG_PLAY_AUDIO: 314 case AttachmentEditor.MSG_PLAY_SLIDESHOW: 315 MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this, 316 mWorkingMessage); 317 break; 318 319 case AttachmentEditor.MSG_REPLACE_IMAGE: 320 case AttachmentEditor.MSG_REPLACE_VIDEO: 321 case AttachmentEditor.MSG_REPLACE_AUDIO: 322 showAddAttachmentDialog(); 323 break; 324 325 case AttachmentEditor.MSG_REMOVE_ATTACHMENT: 326 mWorkingMessage.setAttachment(WorkingMessage.TEXT, null); 327 break; 328 329 default: 330 break; 331 } 332 } 333 }; 334 335 private final Handler mMessageListItemHandler = new Handler() { 336 @Override 337 public void handleMessage(Message msg) { 338 String type; 339 switch (msg.what) { 340 case MessageListItem.MSG_LIST_EDIT_MMS: 341 type = "mms"; 342 break; 343 case MessageListItem.MSG_LIST_EDIT_SMS: 344 type = "sms"; 345 break; 346 default: 347 Log.w(TAG, "Unknown message: " + msg.what); 348 return; 349 } 350 351 MessageItem msgItem = getMessageItem(type, (Long) msg.obj); 352 if (msgItem != null) { 353 editMessageItem(msgItem); 354 drawBottomPanel(); 355 } 356 } 357 }; 358 359 private final OnKeyListener mSubjectKeyListener = new OnKeyListener() { 360 public boolean onKey(View v, int keyCode, KeyEvent event) { 361 if (event.getAction() != KeyEvent.ACTION_DOWN) { 362 return false; 363 } 364 365 // When the subject editor is empty, press "DEL" to hide the input field. 366 if ((keyCode == KeyEvent.KEYCODE_DEL) && (mSubjectTextEditor.length() == 0)) { 367 showSubjectEditor(false); 368 mWorkingMessage.setSubject(null); 369 return true; 370 } 371 372 return false; 373 } 374 }; 375 376 private MessageItem getMessageItem(String type, long msgId) { 377 // Check whether the cursor is valid or not. 378 Cursor cursor = mMsgListAdapter.getCursor(); 379 if (cursor.isClosed() || cursor.isBeforeFirst() || cursor.isAfterLast()) { 380 Log.e(TAG, "Bad cursor.", new RuntimeException()); 381 return null; 382 } 383 384 return mMsgListAdapter.getCachedMessageItem(type, msgId, cursor); 385 } 386 387 private void resetCounter() { 388 mTextCounter.setText(""); 389 mTextCounter.setVisibility(View.GONE); 390 } 391 392 private void updateCounter(CharSequence text, int start, int before, int count) { 393 // The worst case before we begin showing the text counter would be 394 // a UCS-2 message, providing space for 70 characters, minus 395 // CHARS_REMAINING_BEFORE_COUNTER_SHOWN. Don't bother calling 396 // the relatively expensive SmsMessage.calculateLength() until that 397 // point is reached. 398 if (text.length() < (70-CHARS_REMAINING_BEFORE_COUNTER_SHOWN)) { 399 mTextCounter.setVisibility(View.GONE); 400 return; 401 } 402 403 // If we're not removing text (i.e. no chance of converting back to SMS 404 // because of this change) and we're in MMS mode, just bail out. 405 final boolean textAdded = (before < count); 406 if (textAdded && mWorkingMessage.requiresMms()) { 407 mTextCounter.setVisibility(View.GONE); 408 return; 409 } 410 411 int[] params = SmsMessage.calculateLength(text, false); 412 /* SmsMessage.calculateLength returns an int[4] with: 413 * int[0] being the number of SMS's required, 414 * int[1] the number of code units used, 415 * int[2] is the number of code units remaining until the next message. 416 * int[3] is the encoding type that should be used for the message. 417 */ 418 int msgCount = params[0]; 419 int remainingInCurrentMessage = params[2]; 420 421 // Force send as MMS once the number of SMSes required reaches MMS_THRESHOLD. 422 mWorkingMessage.setLengthRequiresMms(msgCount >= MMS_THRESHOLD); 423 424 // Show the counter only if: 425 // - We are not in MMS mode 426 // - We are going to send more than one message OR we are getting close 427 boolean showCounter = false; 428 if (!mWorkingMessage.requiresMms() && 429 (msgCount > 1 || remainingInCurrentMessage <= CHARS_REMAINING_BEFORE_COUNTER_SHOWN)) { 430 showCounter = true; 431 } 432 433 if (showCounter) { 434 // Update the remaining characters and number of messages required. 435 mTextCounter.setText(remainingInCurrentMessage + " / " + msgCount); 436 mTextCounter.setVisibility(View.VISIBLE); 437 } else { 438 mTextCounter.setVisibility(View.GONE); 439 } 440 } 441 442 @Override 443 public void startActivityForResult(Intent intent, int requestCode) 444 { 445 // requestCode >= 0 means the activity in question is a sub-activity. 446 if (requestCode >= 0) { 447 mWaitingForSubActivity = true; 448 } 449 450 super.startActivityForResult(intent, requestCode); 451 } 452 453 private void toastConvertInfo(boolean toMms) { 454 int resId = toMms ? R.string.converting_to_picture_message 455 : R.string.converting_to_text_message; 456 Toast.makeText(this, resId, Toast.LENGTH_SHORT).show(); 457 } 458 459 private class DeleteMessageListener implements OnClickListener { 460 private final Uri mDeleteUri; 461 private final boolean mDeleteAll; 462 463 public DeleteMessageListener(Uri uri, boolean all) { 464 mDeleteUri = uri; 465 mDeleteAll = all; 466 } 467 468 public DeleteMessageListener(long msgId, String type) { 469 if ("mms".equals(type)) { 470 mDeleteUri = ContentUris.withAppendedId( 471 Mms.CONTENT_URI, msgId); 472 } else { 473 mDeleteUri = ContentUris.withAppendedId( 474 Sms.CONTENT_URI, msgId); 475 } 476 mDeleteAll = false; 477 } 478 479 public void onClick(DialogInterface dialog, int whichButton) { 480 int token = mDeleteAll ? DELETE_CONVERSATION_TOKEN 481 : DELETE_MESSAGE_TOKEN; 482 mBackgroundQueryHandler.startDelete(token, 483 null, mDeleteUri, null, null); 484 } 485 } 486 487 private class DiscardDraftListener implements OnClickListener { 488 public void onClick(DialogInterface dialog, int whichButton) { 489 mWorkingMessage.discard(); 490 goToConversationList(); 491 } 492 } 493 494 private class SendIgnoreInvalidRecipientListener implements OnClickListener { 495 public void onClick(DialogInterface dialog, int whichButton) { 496 sendMessage(); 497 } 498 } 499 500 private class CancelSendingListener implements OnClickListener { 501 public void onClick(DialogInterface dialog, int whichButton) { 502 if (isRecipientsEditorVisible()) { 503 mRecipientsEditor.requestFocus(); 504 } 505 } 506 } 507 508 private void confirmSendMessageIfNeeded() { 509 if (!isRecipientsEditorVisible()) { 510 sendMessage(); 511 return; 512 } 513 514 if (mRecipientsEditor.hasInvalidRecipient()) { 515 if (mRecipientsEditor.hasValidRecipient()) { 516 String title = getResourcesString(R.string.has_invalid_recipient, 517 mRecipientsEditor.formatInvalidNumbers()); 518 new AlertDialog.Builder(this) 519 .setIcon(android.R.drawable.ic_dialog_alert) 520 .setTitle(title) 521 .setMessage(R.string.invalid_recipient_message) 522 .setPositiveButton(R.string.try_to_send, 523 new SendIgnoreInvalidRecipientListener()) 524 .setNegativeButton(R.string.no, new CancelSendingListener()) 525 .show(); 526 } else { 527 new AlertDialog.Builder(this) 528 .setIcon(android.R.drawable.ic_dialog_alert) 529 .setTitle(R.string.cannot_send_message) 530 .setMessage(R.string.cannot_send_message_reason) 531 .setPositiveButton(R.string.yes, new CancelSendingListener()) 532 .show(); 533 } 534 } else { 535 sendMessage(); 536 } 537 } 538 539 private final TextWatcher mRecipientsWatcher = new TextWatcher() { 540 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 541 } 542 543 public void onTextChanged(CharSequence s, int start, int before, int count) { 544 // This is a workaround for bug 1609057. Since onUserInteraction() is 545 // not called when the user touches the soft keyboard, we pretend it was 546 // called when textfields changes. This should be removed when the bug 547 // is fixed. 548 onUserInteraction(); 549 } 550 551 public void afterTextChanged(Editable s) { 552 // Bug 1474782 describes a situation in which we send to 553 // the wrong recipient. We have been unable to reproduce this, 554 // but the best theory we have so far is that the contents of 555 // mRecipientList somehow become stale when entering 556 // ComposeMessageActivity via onNewIntent(). This assertion is 557 // meant to catch one possible path to that, of a non-visible 558 // mRecipientsEditor having its TextWatcher fire and refreshing 559 // mRecipientList with its stale contents. 560 if (!isRecipientsEditorVisible()) { 561 IllegalStateException e = new IllegalStateException( 562 "afterTextChanged called with invisible mRecipientsEditor"); 563 // Make sure the crash is uploaded to the service so we 564 // can see if this is happening in the field. 565 Log.e(TAG, "RecipientsWatcher called incorrectly", e); 566 throw e; 567 } 568 569 mWorkingMessage.setWorkingRecipients(mRecipientsEditor.getNumbers()); 570 mWorkingMessage.setHasEmail(mRecipientsEditor.containsEmail()); 571 572 // Walk backwards in the text box, skipping spaces. If the last 573 // character is a comma, update the title bar. 574 for (int pos = s.length() - 1; pos >= 0; pos--) { 575 char c = s.charAt(pos); 576 if (c == ' ') 577 continue; 578 579 if (c == ',') { 580 updateWindowTitle(); 581 startQueryForContactInfo(); 582 } 583 break; 584 } 585 586 // If we have gone to zero recipients, disable send button. 587 updateSendButtonState(); 588 } 589 }; 590 591 private final OnCreateContextMenuListener mRecipientsMenuCreateListener = 592 new OnCreateContextMenuListener() { 593 public void onCreateContextMenu(ContextMenu menu, View v, 594 ContextMenuInfo menuInfo) { 595 if (menuInfo != null) { 596 Contact c = ((RecipientContextMenuInfo) menuInfo).recipient; 597 RecipientsMenuClickListener l = new RecipientsMenuClickListener(c); 598 599 menu.setHeaderTitle(c.getName()); 600 601 if (c.existsInDatabase()) { 602 menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact) 603 .setOnMenuItemClickListener(l); 604 } else { 605 menu.add(0, MENU_ADD_TO_CONTACTS, 0, R.string.menu_add_to_contacts) 606 .setOnMenuItemClickListener(l); 607 } 608 } 609 } 610 }; 611 612 private final class RecipientsMenuClickListener implements MenuItem.OnMenuItemClickListener { 613 private final Contact mRecipient; 614 615 RecipientsMenuClickListener(Contact recipient) { 616 mRecipient = recipient; 617 } 618 619 public boolean onMenuItemClick(MenuItem item) { 620 switch (item.getItemId()) { 621 // Context menu handlers for the recipients editor. 622 case MENU_VIEW_CONTACT: { 623 Uri contactUri = mRecipient.getUri(); 624 startActivity(new Intent(Intent.ACTION_VIEW, contactUri)); 625 return true; 626 } 627 case MENU_ADD_TO_CONTACTS: { 628 Intent intent = ConversationList.createAddContactIntent( 629 mRecipient.getNumber()); 630 ComposeMessageActivity.this.startActivity(intent); 631 return true; 632 } 633 } 634 return false; 635 } 636 } 637 638 private void addPositionBasedMenuItems(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 639 AdapterView.AdapterContextMenuInfo info; 640 641 try { 642 info = (AdapterView.AdapterContextMenuInfo) menuInfo; 643 } catch (ClassCastException e) { 644 Log.e(TAG, "bad menuInfo"); 645 return; 646 } 647 final int position = info.position; 648 649 addUriSpecificMenuItems(menu, v, position); 650 } 651 652 private Uri getSelectedUriFromMessageList(ListView listView, int position) { 653 // If the context menu was opened over a uri, get that uri. 654 MessageListItem msglistItem = (MessageListItem) listView.getChildAt(position); 655 if (msglistItem == null) { 656 // FIXME: Should get the correct view. No such interface in ListView currently 657 // to get the view by position. The ListView.getChildAt(position) cannot 658 // get correct view since the list doesn't create one child for each item. 659 // And if setSelection(position) then getSelectedView(), 660 // cannot get corrent view when in touch mode. 661 return null; 662 } 663 664 TextView textView; 665 CharSequence text = null; 666 int selStart = -1; 667 int selEnd = -1; 668 669 //check if message sender is selected 670 textView = (TextView) msglistItem.findViewById(R.id.text_view); 671 if (textView != null) { 672 text = textView.getText(); 673 selStart = textView.getSelectionStart(); 674 selEnd = textView.getSelectionEnd(); 675 } 676 677 if (selStart == -1) { 678 //sender is not being selected, it may be within the message body 679 textView = (TextView) msglistItem.findViewById(R.id.body_text_view); 680 if (textView != null) { 681 text = textView.getText(); 682 selStart = textView.getSelectionStart(); 683 selEnd = textView.getSelectionEnd(); 684 } 685 } 686 687 // Check that some text is actually selected, rather than the cursor 688 // just being placed within the TextView. 689 if (selStart != selEnd) { 690 int min = Math.min(selStart, selEnd); 691 int max = Math.max(selStart, selEnd); 692 693 URLSpan[] urls = ((Spanned) text).getSpans(min, max, 694 URLSpan.class); 695 696 if (urls.length == 1) { 697 return Uri.parse(urls[0].getURL()); 698 } 699 } 700 701 //no uri was selected 702 return null; 703 } 704 705 private void addUriSpecificMenuItems(ContextMenu menu, View v, int position) { 706 Uri uri = getSelectedUriFromMessageList((ListView) v, position); 707 708 if (uri != null) { 709 Intent intent = new Intent(null, uri); 710 intent.addCategory(Intent.CATEGORY_SELECTED_ALTERNATIVE); 711 menu.addIntentOptions(0, 0, 0, 712 new android.content.ComponentName(this, ComposeMessageActivity.class), 713 null, intent, 0, null); 714 } 715 } 716 717 private final void addCallAndContactMenuItems( 718 ContextMenu menu, MsgListMenuClickListener l, MessageItem msgItem) { 719 // Add all possible links in the address & message 720 StringBuilder textToSpannify = new StringBuilder(); 721 if (msgItem.mBoxId == Mms.MESSAGE_BOX_INBOX) { 722 textToSpannify.append(msgItem.mAddress + ": "); 723 } 724 textToSpannify.append(msgItem.mBody); 725 726 SpannableString msg = new SpannableString(textToSpannify.toString()); 727 Linkify.addLinks(msg, Linkify.ALL); 728 ArrayList<String> uris = 729 MessageUtils.extractUris(msg.getSpans(0, msg.length(), URLSpan.class)); 730 731 while (uris.size() > 0) { 732 String uriString = uris.remove(0); 733 // Remove any dupes so they don't get added to the menu multiple times 734 while (uris.contains(uriString)) { 735 uris.remove(uriString); 736 } 737 738 int sep = uriString.indexOf(":"); 739 String prefix = null; 740 if (sep >= 0) { 741 prefix = uriString.substring(0, sep); 742 uriString = uriString.substring(sep + 1); 743 } 744 boolean addToContacts = false; 745 if ("mailto".equalsIgnoreCase(prefix)) { 746 String sendEmailString = getString( 747 R.string.menu_send_email).replace("%s", uriString); 748 menu.add(0, MENU_SEND_EMAIL, 0, sendEmailString) 749 .setOnMenuItemClickListener(l) 750 .setIntent(new Intent( 751 Intent.ACTION_VIEW, 752 Uri.parse("mailto:" + uriString))); 753 addToContacts = !haveEmailContact(uriString); 754 } else if ("tel".equalsIgnoreCase(prefix)) { 755 String callBackString = getString( 756 R.string.menu_call_back).replace("%s", uriString); 757 menu.add(0, MENU_CALL_BACK, 0, callBackString) 758 .setOnMenuItemClickListener(l) 759 .setIntent(new Intent( 760 Intent.ACTION_DIAL, 761 Uri.parse("tel:" + uriString))); 762 addToContacts = !isNumberInContacts(uriString); 763 } 764 if (addToContacts) { 765 Intent intent = ConversationList.createAddContactIntent(uriString); 766 String addContactString = getString( 767 R.string.menu_add_address_to_contacts).replace("%s", uriString); 768 menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, addContactString) 769 .setOnMenuItemClickListener(l) 770 .setIntent(intent); 771 } 772 } 773 } 774 775 private boolean haveEmailContact(String emailAddress) { 776 Cursor cursor = SqliteWrapper.query(this, getContentResolver(), 777 Contacts.ContactMethods.CONTENT_EMAIL_URI, 778 new String[] { Contacts.ContactMethods.NAME }, 779 Contacts.ContactMethods.DATA + " = " + DatabaseUtils.sqlEscapeString(emailAddress), 780 null, null); 781 782 if (cursor != null) { 783 try { 784 while (cursor.moveToNext()) { 785 String name = cursor.getString(0); 786 if (!TextUtils.isEmpty(name)) { 787 return true; 788 } 789 } 790 } finally { 791 cursor.close(); 792 } 793 } 794 return false; 795 } 796 797 private boolean isNumberInContacts(String phoneNumber) { 798 return Contact.get(phoneNumber, true).existsInDatabase(); 799 } 800 801 private final OnCreateContextMenuListener mMsgListMenuCreateListener = 802 new OnCreateContextMenuListener() { 803 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 804 Cursor cursor = mMsgListAdapter.getCursor(); 805 String type = cursor.getString(COLUMN_MSG_TYPE); 806 long msgId = cursor.getLong(COLUMN_ID); 807 808 addPositionBasedMenuItems(menu, v, menuInfo); 809 810 MessageItem msgItem = mMsgListAdapter.getCachedMessageItem(type, msgId, cursor); 811 if (msgItem == null) { 812 Log.e(TAG, "Cannot load message item for type = " + type 813 + ", msgId = " + msgId); 814 return; 815 } 816 817 menu.setHeaderTitle(R.string.message_options); 818 819 MsgListMenuClickListener l = new MsgListMenuClickListener(); 820 821 if (msgItem.mLocked) { 822 menu.add(0, MENU_UNLOCK_MESSAGE, 0, R.string.menu_unlock) 823 .setOnMenuItemClickListener(l); 824 } else { 825 menu.add(0, MENU_LOCK_MESSAGE, 0, R.string.menu_lock) 826 .setOnMenuItemClickListener(l); 827 } 828 829 if (msgItem.isMms()) { 830 switch (msgItem.mBoxId) { 831 case Mms.MESSAGE_BOX_INBOX: 832 break; 833 case Mms.MESSAGE_BOX_OUTBOX: 834 // Since we currently break outgoing messages to multiple 835 // recipients into one message per recipient, only allow 836 // editing a message for single-recipient conversations. 837 if (getRecipients().size() == 1) { 838 menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit) 839 .setOnMenuItemClickListener(l); 840 } 841 break; 842 } 843 switch (msgItem.mAttachmentType) { 844 case WorkingMessage.TEXT: 845 break; 846 case WorkingMessage.VIDEO: 847 case WorkingMessage.IMAGE: 848 if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) { 849 menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard) 850 .setOnMenuItemClickListener(l); 851 } 852 break; 853 case WorkingMessage.SLIDESHOW: 854 default: 855 menu.add(0, MENU_VIEW_SLIDESHOW, 0, R.string.view_slideshow) 856 .setOnMenuItemClickListener(l); 857 if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) { 858 menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard) 859 .setOnMenuItemClickListener(l); 860 } 861 break; 862 } 863 } else { 864 // Message type is sms. Only allow "edit" if the message has a single recipient 865 if (getRecipients().size() == 1 && 866 (msgItem.mBoxId == Sms.MESSAGE_TYPE_OUTBOX || 867 msgItem.mBoxId == Sms.MESSAGE_TYPE_FAILED)) { 868 menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit) 869 .setOnMenuItemClickListener(l); 870 } 871 } 872 873 addCallAndContactMenuItems(menu, l, msgItem); 874 875 // Forward is not available for undownloaded messages. 876 if (msgItem.isDownloaded()) { 877 menu.add(0, MENU_FORWARD_MESSAGE, 0, R.string.menu_forward) 878 .setOnMenuItemClickListener(l); 879 } 880 881 // It is unclear what would make most sense for copying an MMS message 882 // to the clipboard, so we currently do SMS only. 883 if (msgItem.isSms()) { 884 menu.add(0, MENU_COPY_MESSAGE_TEXT, 0, R.string.copy_message_text) 885 .setOnMenuItemClickListener(l); 886 } 887 888 menu.add(0, MENU_VIEW_MESSAGE_DETAILS, 0, R.string.view_message_details) 889 .setOnMenuItemClickListener(l); 890 menu.add(0, MENU_DELETE_MESSAGE, 0, R.string.delete_message) 891 .setOnMenuItemClickListener(l); 892 if (msgItem.mDeliveryReport || msgItem.mReadReport) { 893 menu.add(0, MENU_DELIVERY_REPORT, 0, R.string.view_delivery_report) 894 .setOnMenuItemClickListener(l); 895 } 896 } 897 }; 898 899 private void editMessageItem(MessageItem msgItem) { 900 if ("sms".equals(msgItem.mType)) { 901 editSmsMessageItem(msgItem); 902 } else { 903 editMmsMessageItem(msgItem); 904 } 905 if (MessageListItem.isFailedMessage(msgItem) && mMsgListAdapter.getCount() <= 1) { 906 // For messages with bad addresses, let the user re-edit the recipients. 907 initRecipientsEditor(); 908 } 909 } 910 911 private void editSmsMessageItem(MessageItem msgItem) { 912 // Delete the old undelivered SMS and load its content. 913 Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgItem.mMsgId); 914 SqliteWrapper.delete(ComposeMessageActivity.this, 915 mContentResolver, uri, null, null); 916 mWorkingMessage.setText(msgItem.mBody); 917 } 918 919 private void editMmsMessageItem(MessageItem msgItem) { 920 // Discard the current message in progress. 921 mWorkingMessage.discard(); 922 923 // Load the selected message in as the working message. 924 mWorkingMessage = WorkingMessage.load(this, msgItem.mMessageUri); 925 mWorkingMessage.setConversation(mConversation); 926 927 mAttachmentEditor.update(mWorkingMessage); 928 drawTopPanel(); 929 } 930 931 private void copyToClipboard(String str) { 932 ClipboardManager clip = 933 (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); 934 clip.setText(str); 935 } 936 937 private void forwardMessage(MessageItem msgItem) { 938 Intent intent = new Intent(ComposeMessageActivity.this, 939 ComposeMessageActivity.class); 940 941 intent.putExtra("exit_on_sent", true); 942 intent.putExtra("forwarded_message", true); 943 if (msgItem.mType.equals("sms")) { 944 intent.putExtra("sms_body", msgItem.mBody); 945 } else { 946 SendReq sendReq = new SendReq(); 947 String subject = getString(R.string.forward_prefix); 948 if (msgItem.mSubject != null) { 949 subject += msgItem.mSubject; 950 } 951 sendReq.setSubject(new EncodedStringValue(subject)); 952 sendReq.setBody(msgItem.mSlideshow.makeCopy( 953 ComposeMessageActivity.this)); 954 955 Uri uri = null; 956 try { 957 PduPersister persister = PduPersister.getPduPersister(this); 958 // Copy the parts of the message here. 959 uri = persister.persist(sendReq, Mms.Draft.CONTENT_URI); 960 } catch (MmsException e) { 961 Log.e(TAG, "Failed to copy message: " + msgItem.mMessageUri, e); 962 Toast.makeText(ComposeMessageActivity.this, 963 R.string.cannot_save_message, Toast.LENGTH_SHORT).show(); 964 return; 965 } 966 967 intent.putExtra("msg_uri", uri); 968 intent.putExtra("subject", subject); 969 } 970 startActivityIfNeeded(intent, -1); 971 } 972 973 /** 974 * Context menu handlers for the message list view. 975 */ 976 private final class MsgListMenuClickListener implements MenuItem.OnMenuItemClickListener { 977 public boolean onMenuItemClick(MenuItem item) { 978 Cursor cursor = mMsgListAdapter.getCursor(); 979 String type = cursor.getString(COLUMN_MSG_TYPE); 980 long msgId = cursor.getLong(COLUMN_ID); 981 MessageItem msgItem = getMessageItem(type, msgId); 982 983 if (msgItem == null) { 984 return false; 985 } 986 987 switch (item.getItemId()) { 988 case MENU_EDIT_MESSAGE: 989 editMessageItem(msgItem); 990 drawBottomPanel(); 991 return true; 992 993 case MENU_COPY_MESSAGE_TEXT: 994 copyToClipboard(msgItem.mBody); 995 return true; 996 997 case MENU_FORWARD_MESSAGE: 998 forwardMessage(msgItem); 999 return true; 1000 1001 case MENU_VIEW_SLIDESHOW: 1002 MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this, 1003 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId), null); 1004 return true; 1005 1006 case MENU_VIEW_MESSAGE_DETAILS: { 1007 String messageDetails = MessageUtils.getMessageDetails( 1008 ComposeMessageActivity.this, cursor, msgItem.mMessageSize); 1009 new AlertDialog.Builder(ComposeMessageActivity.this) 1010 .setTitle(R.string.message_details_title) 1011 .setMessage(messageDetails) 1012 .setPositiveButton(android.R.string.ok, null) 1013 .setCancelable(true) 1014 .show(); 1015 return true; 1016 } 1017 case MENU_DELETE_MESSAGE: { 1018 DeleteMessageListener l = new DeleteMessageListener( 1019 msgItem.mMessageUri, false); 1020 confirmDeleteDialog(l, false); 1021 return true; 1022 } 1023 case MENU_DELIVERY_REPORT: 1024 showDeliveryReport(msgId, type); 1025 return true; 1026 1027 case MENU_COPY_TO_SDCARD: { 1028 int resId = copyMedia(msgId) ? R.string.copy_to_sdcard_success : 1029 R.string.copy_to_sdcard_fail; 1030 Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show(); 1031 return true; 1032 } 1033 1034 case MENU_LOCK_MESSAGE: { 1035 lockMessage(msgItem, true); 1036 return true; 1037 } 1038 1039 case MENU_UNLOCK_MESSAGE: { 1040 lockMessage(msgItem, false); 1041 return true; 1042 } 1043 1044 default: 1045 return false; 1046 } 1047 } 1048 } 1049 1050 private void lockMessage(MessageItem msgItem, boolean locked) { 1051 Uri uri; 1052 if ("sms".equals(msgItem.mType)) { 1053 uri = Sms.CONTENT_URI; 1054 } else { 1055 uri = Mms.CONTENT_URI; 1056 } 1057 final Uri lockUri = ContentUris.withAppendedId(uri, msgItem.mMsgId);; 1058 1059 final ContentValues values = new ContentValues(1); 1060 values.put("locked", locked ? 1 : 0); 1061 1062 new Thread(new Runnable() { 1063 public void run() { 1064 getContentResolver().update(lockUri, 1065 values, null, null); 1066 } 1067 }).start(); 1068 } 1069 1070 /** 1071 * Looks to see if there are any valid parts of the attachment that can be copied to a SD card. 1072 * @param msgId 1073 */ 1074 private boolean haveSomethingToCopyToSDCard(long msgId) { 1075 PduBody body; 1076 try { 1077 body = SlideshowModel.getPduBody(this, 1078 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId)); 1079 } catch (MmsException e) { 1080 Log.e(TAG, e.getMessage(), e); 1081 return false; 1082 } 1083 1084 boolean result = false; 1085 int partNum = body.getPartsNum(); 1086 for(int i = 0; i < partNum; i++) { 1087 PduPart part = body.getPart(i); 1088 String type = new String(part.getContentType()); 1089 1090 if ((ContentType.isImageType(type) || ContentType.isVideoType(type) || 1091 ContentType.isAudioType(type))) { 1092 result = true; 1093 break; 1094 } 1095 } 1096 return result; 1097 } 1098 1099 /** 1100 * Copies media from an Mms to the "download" directory on the SD card 1101 * @param msgId 1102 */ 1103 private boolean copyMedia(long msgId) { 1104 PduBody body; 1105 boolean result = true; 1106 try { 1107 body = SlideshowModel.getPduBody(this, ContentUris.withAppendedId(Mms.CONTENT_URI, msgId)); 1108 } catch (MmsException e) { 1109 Log.e(TAG, e.getMessage(), e); 1110 return false; 1111 } 1112 1113 int partNum = body.getPartsNum(); 1114 for(int i = 0; i < partNum; i++) { 1115 PduPart part = body.getPart(i); 1116 String type = new String(part.getContentType()); 1117 1118 if ((ContentType.isImageType(type) || ContentType.isVideoType(type) || 1119 ContentType.isAudioType(type))) { 1120 result &= copyPart(part); // all parts have to be successful for a valid result. 1121 } 1122 } 1123 return result; 1124 } 1125 1126 private boolean copyPart(PduPart part) { 1127 Uri uri = part.getDataUri(); 1128 1129 InputStream input = null; 1130 FileOutputStream fout = null; 1131 try { 1132 input = mContentResolver.openInputStream(uri); 1133 if (input instanceof FileInputStream) { 1134 FileInputStream fin = (FileInputStream) input; 1135 1136 byte[] location = part.getName(); 1137 if (location == null) { 1138 location = part.getFilename(); 1139 } 1140 if (location == null) { 1141 location = part.getContentLocation(); 1142 } 1143 1144 // Depending on the location, there may be an 1145 // extension already on the name or not 1146 String fileName = new String(location); 1147 String dir = "/sdcard/download/"; 1148 String extension; 1149 int index; 1150 if ((index = fileName.indexOf(".")) == -1) { 1151 String type = new String(part.getContentType()); 1152 extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type); 1153 } else { 1154 extension = fileName.substring(index + 1, fileName.length()); 1155 fileName = fileName.substring(0, index); 1156 } 1157 1158 File file = getUniqueDestination(dir + fileName, extension); 1159 1160 // make sure the path is valid and directories created for this file. 1161 File parentFile = file.getParentFile(); 1162 if (!parentFile.exists() && !parentFile.mkdirs()) { 1163 Log.e(TAG, "[MMS] copyPart: mkdirs for " + parentFile.getPath() + " failed!"); 1164 return false; 1165 } 1166 1167 fout = new FileOutputStream(file); 1168 1169 byte[] buffer = new byte[8000]; 1170 while(fin.read(buffer) != -1) { 1171 fout.write(buffer); 1172 } 1173 1174 // Notify other applications listening to scanner events 1175 // that a media file has been added to the sd card 1176 sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, 1177 Uri.fromFile(file))); 1178 } 1179 } catch (IOException e) { 1180 // Ignore 1181 Log.e(TAG, "IOException caught while opening or reading stream", e); 1182 return false; 1183 } finally { 1184 if (null != input) { 1185 try { 1186 input.close(); 1187 } catch (IOException e) { 1188 // Ignore 1189 Log.e(TAG, "IOException caught while closing stream", e); 1190 return false; 1191 } 1192 } 1193 if (null != fout) { 1194 try { 1195 fout.close(); 1196 } catch (IOException e) { 1197 // Ignore 1198 Log.e(TAG, "IOException caught while closing stream", e); 1199 return false; 1200 } 1201 } 1202 } 1203 return true; 1204 } 1205 1206 private File getUniqueDestination(String base, String extension) { 1207 File file = new File(base + "." + extension); 1208 1209 for (int i = 2; file.exists(); i++) { 1210 file = new File(base + "_" + i + "." + extension); 1211 } 1212 return file; 1213 } 1214 1215 private void showDeliveryReport(long messageId, String type) { 1216 Intent intent = new Intent(this, DeliveryReportActivity.class); 1217 intent.putExtra("message_id", messageId); 1218 intent.putExtra("message_type", type); 1219 1220 startActivity(intent); 1221 } 1222 1223 private final IntentFilter mHttpProgressFilter = new IntentFilter(PROGRESS_STATUS_ACTION); 1224 1225 private final BroadcastReceiver mHttpProgressReceiver = new BroadcastReceiver() { 1226 @Override 1227 public void onReceive(Context context, Intent intent) { 1228 if (PROGRESS_STATUS_ACTION.equals(intent.getAction())) { 1229 long token = intent.getLongExtra("token", 1230 SendingProgressTokenManager.NO_TOKEN); 1231 if (token != mConversation.getThreadId()) { 1232 return; 1233 } 1234 1235 int progress = intent.getIntExtra("progress", 0); 1236 switch (progress) { 1237 case PROGRESS_START: 1238 setProgressBarVisibility(true); 1239 break; 1240 case PROGRESS_ABORT: 1241 case PROGRESS_COMPLETE: 1242 setProgressBarVisibility(false); 1243 break; 1244 default: 1245 setProgress(100 * progress); 1246 } 1247 } 1248 } 1249 }; 1250 1251 private static ContactList sEmptyContactList; 1252 1253 private ContactList getRecipients() { 1254 // If the recipients editor is visible, the conversation has 1255 // not really officially 'started' yet. Recipients will be set 1256 // on the conversation once it has been saved or sent. In the 1257 // meantime, let anyone who needs the recipient list think it 1258 // is empty rather than giving them a stale one. 1259 if (isRecipientsEditorVisible()) { 1260 if (sEmptyContactList == null) { 1261 sEmptyContactList = new ContactList(); 1262 } 1263 return sEmptyContactList; 1264 } 1265 return mConversation.getRecipients(); 1266 } 1267 1268 // Get the recipients editor ready to be displayed onscreen. 1269 private void initRecipientsEditor() { 1270 if (isRecipientsEditorVisible()) { 1271 return; 1272 } 1273 ViewStub stub = (ViewStub)findViewById(R.id.recipients_editor_stub); 1274 if (stub != null) { 1275 mRecipientsEditor = (RecipientsEditor) stub.inflate(); 1276 } else { 1277 mRecipientsEditor = (RecipientsEditor)findViewById(R.id.recipients_editor); 1278 mRecipientsEditor.setVisibility(View.VISIBLE); 1279 } 1280 1281 mRecipientsEditor.setAdapter(new RecipientsAdapter(this)); 1282 mRecipientsEditor.populate(getRecipients()); 1283 mRecipientsEditor.setOnCreateContextMenuListener(mRecipientsMenuCreateListener); 1284 mRecipientsEditor.addTextChangedListener(mRecipientsWatcher); 1285 mRecipientsEditor.setFilters(new InputFilter[] { 1286 new InputFilter.LengthFilter(RECIPIENTS_MAX_LENGTH) }); 1287 mRecipientsEditor.setOnItemClickListener(new AdapterView.OnItemClickListener() { 1288 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1289 // After the user selects an item in the pop-up contacts list, move the 1290 // focus to the text editor if there is only one recipient. This helps 1291 // the common case of selecting one recipient and then typing a message, 1292 // but avoids annoying a user who is trying to add five recipients and 1293 // keeps having focus stolen away. 1294 if (mRecipientsEditor.getRecipientCount() == 1) { 1295 // if we're in extract mode then don't request focus 1296 final InputMethodManager inputManager = (InputMethodManager) 1297 getSystemService(Context.INPUT_METHOD_SERVICE); 1298 if (inputManager == null || !inputManager.isFullscreenMode()) { 1299 mTextEditor.requestFocus(); 1300 } 1301 } 1302 } 1303 }); 1304 1305 mTopPanel.setVisibility(View.VISIBLE); 1306 } 1307 1308 //========================================================== 1309 // Activity methods 1310 //========================================================== 1311 1312 private void setPresenceIcon(int iconId) { 1313 Drawable icon = iconId == 0 ? null : this.getResources().getDrawable(iconId); 1314 getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, icon); 1315 } 1316 1317 public static boolean cancelFailedToDeliverNotification(Intent intent, Context context) { 1318 if (MessagingNotification.isFailedToDeliver(intent)) { 1319 // Cancel any failed message notifications 1320 MessagingNotification.cancelNotification(context, 1321 MessagingNotification.MESSAGE_FAILED_NOTIFICATION_ID); 1322 return true; 1323 } 1324 return false; 1325 } 1326 1327 public static boolean cancelFailedDownloadNotification(Intent intent, Context context) { 1328 if (MessagingNotification.isFailedToDownload(intent)) { 1329 // Cancel any failed download notifications 1330 MessagingNotification.cancelNotification(context, 1331 MessagingNotification.DOWNLOAD_FAILED_NOTIFICATION_ID); 1332 return true; 1333 } 1334 return false; 1335 } 1336 1337 @Override 1338 protected void onCreate(Bundle savedInstanceState) { 1339 super.onCreate(savedInstanceState); 1340 requestWindowFeature(Window.FEATURE_PROGRESS); 1341 requestWindowFeature(Window.FEATURE_LEFT_ICON); 1342 1343 setContentView(R.layout.compose_message_activity); 1344 setProgressBarVisibility(false); 1345 1346 setTitle(""); 1347 1348 // Initialize members for UI elements. 1349 initResourceRefs(); 1350 1351 mContentResolver = getContentResolver(); 1352 mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver); 1353 1354 // Create a new empty working message. 1355 mWorkingMessage = WorkingMessage.createEmpty(this); 1356 1357 // Read parameters or previously saved state of this activity. 1358 initActivityState(savedInstanceState, getIntent()); 1359 1360 if (LOCAL_LOGV) { 1361 Log.v(TAG, "onCreate(): savedInstanceState = " + savedInstanceState); 1362 Log.v(TAG, "onCreate(): intent = " + getIntent()); 1363 } 1364 1365 if (cancelFailedToDeliverNotification(getIntent(), this)) { 1366 // Show a pop-up dialog to inform user the message was 1367 // failed to deliver. 1368 undeliveredMessageDialog(getMessageDate(null)); 1369 } 1370 cancelFailedDownloadNotification(getIntent(), this); 1371 1372 // Set up the message history ListAdapter 1373 initMessageList(); 1374 1375 // Mark the current thread as read. 1376 mConversation.markAsRead(); 1377 1378 // Load the draft for this thread, if we aren't already handling 1379 // existing data, such as a shared picture or forwarded message. 1380 if (!handleSendIntent(getIntent()) && !handleForwardedMessage()) { 1381 loadDraft(); 1382 } 1383 1384 // Let the working message know what conversation it belongs to. 1385 mWorkingMessage.setConversation(mConversation); 1386 1387 // Show the recipients editor if we don't have a valid thread. 1388 if (mConversation.getThreadId() <= 0) { 1389 initRecipientsEditor(); 1390 } 1391 1392 updateSendButtonState(); 1393 1394 drawTopPanel(); 1395 drawBottomPanel(); 1396 mAttachmentEditor.update(mWorkingMessage); 1397 1398 Configuration config = getResources().getConfiguration(); 1399 mIsKeyboardOpen = config.keyboardHidden == KEYBOARDHIDDEN_NO; 1400 mIsLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE; 1401 onKeyboardStateChanged(mIsKeyboardOpen); 1402 1403 if (TRACE) { 1404 android.os.Debug.startMethodTracing("compose"); 1405 } 1406 } 1407 1408 private void showSubjectEditor(boolean show) { 1409 if (mSubjectTextEditor == null) { 1410 // Don't bother to initialize the subject editor if 1411 // we're just going to hide it. 1412 if (show == false) { 1413 return; 1414 } 1415 mSubjectTextEditor = (EditText)findViewById(R.id.subject); 1416 mSubjectTextEditor.setOnKeyListener(mSubjectKeyListener); 1417 mSubjectTextEditor.addTextChangedListener(mSubjectEditorWatcher); 1418 } 1419 mSubjectTextEditor.setText(mWorkingMessage.getSubject()); 1420 mSubjectTextEditor.setVisibility(show ? View.VISIBLE : View.GONE); 1421 hideOrShowTopPanel(); 1422 } 1423 1424 private void hideOrShowTopPanel() { 1425 boolean anySubViewsVisible = (isSubjectEditorVisible() || isRecipientsEditorVisible()); 1426 mTopPanel.setVisibility(anySubViewsVisible ? View.VISIBLE : View.GONE); 1427 } 1428 1429 @Override 1430 protected void onRestart() { 1431 super.onRestart(); 1432 1433 mConversation.markAsRead(); 1434 } 1435 1436 @Override 1437 protected void onStart() { 1438 super.onStart(); 1439 1440 updateWindowTitle(); 1441 initFocus(); 1442 1443 // Register a BroadcastReceiver to listen on HTTP I/O process. 1444 registerReceiver(mHttpProgressReceiver, mHttpProgressFilter); 1445 1446 startMsgListQuery(); 1447 startQueryForContactInfo(); 1448 updateSendFailedNotification(); 1449 1450 } 1451 1452 @Override 1453 protected void onResume() { 1454 super.onResume(); 1455 // Register to get notified of presence changes so we can update the presence indicator. 1456 Contact.startPresenceObserver(); 1457 addRecipientsListeners(); 1458 } 1459 1460 private void updateSendFailedNotification() { 1461 final long threadId = mConversation.getThreadId(); 1462 if (threadId <= 0) 1463 return; 1464 1465 // updateSendFailedNotificationForThread makes a database call, so do the work off 1466 // of the ui thread. 1467 new Thread(new Runnable() { 1468 public void run() { 1469 MessagingNotification.updateSendFailedNotificationForThread( 1470 ComposeMessageActivity.this, threadId); 1471 } 1472 }).run(); 1473 } 1474 1475 @Override 1476 public void onSaveInstanceState(Bundle outState) { 1477 super.onSaveInstanceState(outState); 1478 1479 outState.putString("recipients", getRecipients().serialize()); 1480 1481 mWorkingMessage.writeStateToBundle(outState); 1482 1483 if (mExitOnSent) { 1484 outState.putBoolean("exit_on_sent", mExitOnSent); 1485 } 1486 } 1487 1488 @Override 1489 protected void onPause() { 1490 super.onPause(); 1491 Contact.stopPresenceObserver(); 1492 removeRecipientsListeners(); 1493 } 1494 1495 @Override 1496 protected void onStop() { 1497 super.onStop(); 1498 1499 if (mMsgListAdapter != null) { 1500 mMsgListAdapter.changeCursor(null); 1501 } 1502 1503 saveDraft(); 1504 1505 // Cleanup the BroadcastReceiver. 1506 unregisterReceiver(mHttpProgressReceiver); 1507 1508 cleanupContactInfoCursor(); 1509 } 1510 1511 @Override 1512 protected void onDestroy() { 1513 if (TRACE) { 1514 android.os.Debug.stopMethodTracing(); 1515 } 1516 1517 super.onDestroy(); 1518 } 1519 1520 @Override 1521 public void onConfigurationChanged(Configuration newConfig) { 1522 super.onConfigurationChanged(newConfig); 1523 if (LOCAL_LOGV) { 1524 Log.v(TAG, "onConfigurationChanged: " + newConfig); 1525 } 1526 1527 mIsKeyboardOpen = newConfig.keyboardHidden == KEYBOARDHIDDEN_NO; 1528 mIsLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE; 1529 onKeyboardStateChanged(mIsKeyboardOpen); 1530 } 1531 1532 private void onKeyboardStateChanged(boolean isKeyboardOpen) { 1533 // If the keyboard is hidden, don't show focus highlights for 1534 // things that cannot receive input. 1535 if (isKeyboardOpen) { 1536 if (mRecipientsEditor != null) { 1537 mRecipientsEditor.setFocusableInTouchMode(true); 1538 } 1539 if (mSubjectTextEditor != null) { 1540 mSubjectTextEditor.setFocusableInTouchMode(true); 1541 } 1542 mTextEditor.setFocusableInTouchMode(true); 1543 mTextEditor.setHint(R.string.type_to_compose_text_enter_to_send); 1544 initFocus(); 1545 } else { 1546 if (mRecipientsEditor != null) { 1547 mRecipientsEditor.setFocusable(false); 1548 } 1549 if (mSubjectTextEditor != null) { 1550 mSubjectTextEditor.setFocusable(false); 1551 } 1552 mTextEditor.setFocusable(false); 1553 mTextEditor.setHint(R.string.open_keyboard_to_compose_message); 1554 } 1555 } 1556 1557 @Override 1558 public void onUserInteraction() { 1559 checkPendingNotification(); 1560 } 1561 1562 @Override 1563 public void onWindowFocusChanged(boolean hasFocus) { 1564 if (hasFocus) { 1565 checkPendingNotification(); 1566 } 1567 } 1568 1569 @Override 1570 public boolean onKeyDown(int keyCode, KeyEvent event) { 1571 switch (keyCode) { 1572 case KeyEvent.KEYCODE_DEL: 1573 if ((mMsgListAdapter != null) && mMsgListView.isFocused()) { 1574 Cursor cursor; 1575 try { 1576 cursor = (Cursor) mMsgListView.getSelectedItem(); 1577 } catch (ClassCastException e) { 1578 Log.e(TAG, "Unexpected ClassCastException.", e); 1579 return super.onKeyDown(keyCode, event); 1580 } 1581 1582 if (cursor != null) { 1583 DeleteMessageListener l = new DeleteMessageListener( 1584 cursor.getLong(COLUMN_ID), 1585 cursor.getString(COLUMN_MSG_TYPE)); 1586 confirmDeleteDialog(l, false); 1587 return true; 1588 } 1589 } 1590 break; 1591 case KeyEvent.KEYCODE_DPAD_CENTER: 1592 case KeyEvent.KEYCODE_ENTER: 1593 if (isPreparedForSending()) { 1594 confirmSendMessageIfNeeded(); 1595 return true; 1596 } 1597 break; 1598 case KeyEvent.KEYCODE_BACK: 1599 exitComposeMessageActivity(new Runnable() { 1600 public void run() { 1601 finish(); 1602 } 1603 }); 1604 return true; 1605 } 1606 1607 return super.onKeyDown(keyCode, event); 1608 } 1609 1610 private void exitComposeMessageActivity(final Runnable exit) { 1611 // If the message is empty, just quit -- finishing the 1612 // activity will cause an empty draft to be deleted. 1613 if (!mWorkingMessage.isWorthSaving()) { 1614 exit.run(); 1615 return; 1616 } 1617 1618 if (isRecipientsEditorVisible() 1619 && !mRecipientsEditor.hasValidRecipient()) { 1620 MessageUtils.showDiscardDraftConfirmDialog(this, 1621 new DiscardDraftListener()); 1622 return; 1623 } 1624 1625 mToastForDraftSave = true; 1626 exit.run(); 1627 } 1628 1629 private void goToConversationList() { 1630 finish(); 1631 startActivity(new Intent(this, ConversationList.class)); 1632 } 1633 1634 private boolean isRecipientsEditorVisible() { 1635 return (null != mRecipientsEditor) 1636 && (View.VISIBLE == mRecipientsEditor.getVisibility()); 1637 } 1638 1639 private boolean isSubjectEditorVisible() { 1640 return (null != mSubjectTextEditor) 1641 && (View.VISIBLE == mSubjectTextEditor.getVisibility()); 1642 } 1643 1644 public void onAttachmentChanged() { 1645 drawBottomPanel(); 1646 updateSendButtonState(); 1647 mAttachmentEditor.update(mWorkingMessage); 1648 } 1649 1650 public void onProtocolChanged(boolean mms) { 1651 toastConvertInfo(mms); 1652 } 1653 1654 public void onMessageSent() { 1655 // If we already have messages in the list adapter, it 1656 // will be auto-requerying; don't thrash another query in. 1657 if (mMsgListAdapter.getCount() == 0) { 1658 startMsgListQuery(); 1659 } 1660 } 1661 1662 // We don't want to show the "call" option unless there is only one 1663 // recipient and it's a phone number. 1664 private boolean isRecipientCallable() { 1665 ContactList recipients = getRecipients(); 1666 return (recipients.size() == 1 && !recipients.containsEmail()); 1667 } 1668 1669 private void dialRecipient() { 1670 String number = getRecipients().get(0).getNumber(); 1671 Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + number)); 1672 startActivity(dialIntent); 1673 } 1674 1675 @Override 1676 public boolean onPrepareOptionsMenu(Menu menu) { 1677 menu.clear(); 1678 1679 if (isRecipientCallable()) { 1680 menu.add(0, MENU_CALL_RECIPIENT, 0, R.string.menu_call).setIcon( 1681 com.android.internal.R.drawable.ic_menu_call); 1682 } 1683 1684 // Only add the "View contact" menu item when there's a single recipient and that 1685 // recipient is someone in contacts. 1686 ContactList recipients = getRecipients(); 1687 if (recipients.size() == 1 && recipients.get(0).existsInDatabase()) { 1688 menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact).setIcon( 1689 R.drawable.ic_menu_contact); 1690 } 1691 1692 if (MmsConfig.getMmsEnabled()) { 1693 if (!isSubjectEditorVisible()) { 1694 menu.add(0, MENU_ADD_SUBJECT, 0, R.string.add_subject).setIcon( 1695 com.android.internal.R.drawable.ic_menu_edit); 1696 } 1697 1698 if (!mWorkingMessage.hasAttachment()) { 1699 menu.add(0, MENU_ADD_ATTACHMENT, 0, R.string.add_attachment).setIcon( 1700 R.drawable.ic_menu_attachment); 1701 } 1702 } 1703 1704 if (isPreparedForSending()) { 1705 menu.add(0, MENU_SEND, 0, R.string.send).setIcon(android.R.drawable.ic_menu_send); 1706 } 1707 1708 menu.add(0, MENU_INSERT_SMILEY, 0, R.string.menu_insert_smiley).setIcon( 1709 com.android.internal.R.drawable.ic_menu_emoticons); 1710 1711 if (mMsgListAdapter.getCount() > 0) { 1712 // Removed search as part of b/1205708 1713 //menu.add(0, MENU_SEARCH, 0, R.string.menu_search).setIcon( 1714 // R.drawable.ic_menu_search); 1715 Cursor cursor = mMsgListAdapter.getCursor(); 1716 if ((null != cursor) && (cursor.getCount() > 0)) { 1717 menu.add(0, MENU_DELETE_THREAD, 0, R.string.delete_thread).setIcon( 1718 android.R.drawable.ic_menu_delete); 1719 } 1720 } else { 1721 menu.add(0, MENU_DISCARD, 0, R.string.discard).setIcon(android.R.drawable.ic_menu_delete); 1722 } 1723 1724 menu.add(0, MENU_CONVERSATION_LIST, 0, R.string.all_threads).setIcon( 1725 com.android.internal.R.drawable.ic_menu_friendslist); 1726 1727 buildAddAddressToContactMenuItem(menu); 1728 return true; 1729 } 1730 1731 private void buildAddAddressToContactMenuItem(Menu menu) { 1732 // Look for the first recipient we don't have a contact for and create a menu item to 1733 // add the number to contacts. 1734 for (Contact c : getRecipients()) { 1735 if (!c.existsInDatabase()) { 1736 Intent intent = ConversationList.createAddContactIntent(c.getNumber()); 1737 menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, R.string.menu_add_to_contacts) 1738 .setIcon(android.R.drawable.ic_menu_add) 1739 .setIntent(intent); 1740 break; 1741 } 1742 } 1743 } 1744 1745 @Override 1746 public boolean onOptionsItemSelected(MenuItem item) { 1747 switch (item.getItemId()) { 1748 case MENU_ADD_SUBJECT: 1749 showSubjectEditor(true); 1750 mWorkingMessage.setSubject(""); 1751 mSubjectTextEditor.requestFocus(); 1752 break; 1753 case MENU_ADD_ATTACHMENT: 1754 // Launch the add-attachment list dialog 1755 showAddAttachmentDialog(); 1756 break; 1757 case MENU_DISCARD: 1758 mWorkingMessage.discard(); 1759 finish(); 1760 break; 1761 case MENU_SEND: 1762 if (isPreparedForSending()) { 1763 confirmSendMessageIfNeeded(); 1764 } 1765 break; 1766 case MENU_SEARCH: 1767 onSearchRequested(); 1768 break; 1769 case MENU_DELETE_THREAD: 1770 DeleteMessageListener l = new DeleteMessageListener( 1771 mConversation.getUri(), true); 1772 confirmDeleteDialog(l, true); 1773 break; 1774 case MENU_CONVERSATION_LIST: 1775 exitComposeMessageActivity(new Runnable() { 1776 public void run() { 1777 goToConversationList(); 1778 } 1779 }); 1780 break; 1781 case MENU_CALL_RECIPIENT: 1782 dialRecipient(); 1783 break; 1784 case MENU_INSERT_SMILEY: 1785 showSmileyDialog(); 1786 break; 1787 case MENU_VIEW_CONTACT: { 1788 // View the contact for the first (and only) recipient. 1789 ContactList list = getRecipients(); 1790 if (list.size() == 1 && list.get(0).existsInDatabase()) { 1791 Uri contactUri = list.get(0).getUri(); 1792 startActivity(new Intent(Intent.ACTION_VIEW, contactUri)); 1793 } 1794 break; 1795 } 1796 case MENU_ADD_ADDRESS_TO_CONTACTS: 1797 return false; // so the intent attached to the menu item will get launched. 1798 } 1799 1800 return true; 1801 } 1802 1803 private void addAttachment(int type) { 1804 switch (type) { 1805 case AttachmentTypeSelectorAdapter.ADD_IMAGE: 1806 MessageUtils.selectImage(this, REQUEST_CODE_ATTACH_IMAGE); 1807 break; 1808 1809 case AttachmentTypeSelectorAdapter.TAKE_PICTURE: { 1810 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 1811 startActivityForResult(intent, REQUEST_CODE_TAKE_PICTURE); 1812 } 1813 break; 1814 1815 case AttachmentTypeSelectorAdapter.ADD_VIDEO: 1816 MessageUtils.selectVideo(this, REQUEST_CODE_ATTACH_VIDEO); 1817 break; 1818 1819 case AttachmentTypeSelectorAdapter.RECORD_VIDEO: { 1820 Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); 1821 intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); 1822 startActivityForResult(intent, REQUEST_CODE_TAKE_VIDEO); 1823 } 1824 break; 1825 1826 case AttachmentTypeSelectorAdapter.ADD_SOUND: 1827 MessageUtils.selectAudio(this, REQUEST_CODE_ATTACH_SOUND); 1828 break; 1829 1830 case AttachmentTypeSelectorAdapter.RECORD_SOUND: 1831 MessageUtils.recordSound(this, REQUEST_CODE_RECORD_SOUND); 1832 break; 1833 1834 case AttachmentTypeSelectorAdapter.ADD_SLIDESHOW: 1835 editSlideshow(); 1836 break; 1837 1838 default: 1839 break; 1840 } 1841 } 1842 1843 private void showAddAttachmentDialog() { 1844 AlertDialog.Builder builder = new AlertDialog.Builder(this); 1845 builder.setIcon(R.drawable.ic_dialog_attach); 1846 builder.setTitle(R.string.add_attachment); 1847 1848 AttachmentTypeSelectorAdapter adapter = new AttachmentTypeSelectorAdapter( 1849 this, AttachmentTypeSelectorAdapter.MODE_WITH_SLIDESHOW); 1850 1851 builder.setAdapter(adapter, new DialogInterface.OnClickListener() { 1852 public void onClick(DialogInterface dialog, int which) { 1853 addAttachment(which); 1854 } 1855 }); 1856 1857 builder.show(); 1858 } 1859 1860 @Override 1861 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 1862 if (LOCAL_LOGV) { 1863 Log.v(TAG, "onActivityResult: requestCode=" + requestCode 1864 + ", resultCode=" + resultCode + ", data=" + data); 1865 } 1866 mWaitingForSubActivity = false; // We're back! 1867 1868 // If there's no data (because the user didn't select a picture and 1869 // just hit BACK, for example), there's nothing to do. 1870 if (data == null) { 1871 return; 1872 } 1873 1874 switch(requestCode) { 1875 case REQUEST_CODE_CREATE_SLIDESHOW: 1876 if (data != null) { 1877 mWorkingMessage = WorkingMessage.load(this, data.getData()); 1878 mWorkingMessage.setConversation(mConversation); 1879 mAttachmentEditor.update(mWorkingMessage); 1880 drawTopPanel(); 1881 } 1882 break; 1883 1884 case REQUEST_CODE_TAKE_PICTURE: 1885 Bitmap bitmap = (Bitmap) data.getParcelableExtra("data"); 1886 if (bitmap == null) { 1887 handleAddAttachmentError(WorkingMessage.UNKNOWN_ERROR, R.string.type_picture); 1888 return; 1889 } 1890 addImage(bitmap); 1891 break; 1892 1893 case REQUEST_CODE_ATTACH_IMAGE: 1894 addImage(data.getData()); 1895 break; 1896 1897 case REQUEST_CODE_TAKE_VIDEO: 1898 case REQUEST_CODE_ATTACH_VIDEO: 1899 addVideo(data.getData()); 1900 break; 1901 1902 case REQUEST_CODE_ATTACH_SOUND: 1903 Uri uri = (Uri) data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); 1904 if (Settings.System.DEFAULT_RINGTONE_URI.equals(uri)) { 1905 break; 1906 } 1907 addAudio(uri); 1908 break; 1909 1910 case REQUEST_CODE_RECORD_SOUND: 1911 addAudio(data.getData()); 1912 break; 1913 1914 default: 1915 // TODO 1916 break; 1917 } 1918 } 1919 1920 private void addImage(Bitmap bitmap) { 1921 try { 1922 Uri messageUri = mWorkingMessage.saveAsMms(); 1923 addImage(MessageUtils.saveBitmapAsPart(this, messageUri, bitmap)); 1924 } catch (MmsException e) { 1925 handleAddAttachmentError(WorkingMessage.UNKNOWN_ERROR, R.string.type_picture); 1926 } 1927 } 1928 1929 private final ResizeImageResultCallback mResizeImageCallback = new ResizeImageResultCallback() { 1930 // TODO: make this produce a Uri, that's what we want anyway 1931 public void onResizeResult(PduPart part) { 1932 if (part == null) { 1933 handleAddAttachmentError(WorkingMessage.UNKNOWN_ERROR, R.string.type_picture); 1934 return; 1935 } 1936 1937 Context context = ComposeMessageActivity.this; 1938 PduPersister persister = PduPersister.getPduPersister(context); 1939 int result; 1940 1941 Uri messageUri = mWorkingMessage.saveAsMms(); 1942 try { 1943 Uri dataUri = persister.persistPart(part, ContentUris.parseId(messageUri)); 1944 result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, dataUri); 1945 } catch (MmsException e) { 1946 result = WorkingMessage.UNKNOWN_ERROR; 1947 } 1948 1949 handleAddAttachmentError(result, R.string.type_picture); 1950 } 1951 }; 1952 1953 private void handleAddAttachmentError(int error, int mediaTypeStringId) { 1954 if (error == WorkingMessage.OK) { 1955 return; 1956 } 1957 1958 Resources res = getResources(); 1959 String mediaType = res.getString(mediaTypeStringId); 1960 String title, message; 1961 1962 switch(error) { 1963 case WorkingMessage.UNKNOWN_ERROR: 1964 message = res.getString(R.string.failed_to_add_media, mediaType); 1965 Toast.makeText(this, message, Toast.LENGTH_SHORT); 1966 return; 1967 case WorkingMessage.UNSUPPORTED_TYPE: 1968 title = res.getString(R.string.unsupported_media_format, mediaType); 1969 message = res.getString(R.string.select_different_media, mediaType); 1970 break; 1971 case WorkingMessage.MESSAGE_SIZE_EXCEEDED: 1972 title = res.getString(R.string.exceed_message_size_limitation, mediaType); 1973 message = res.getString(R.string.failed_to_add_media, mediaType); 1974 break; 1975 case WorkingMessage.IMAGE_TOO_LARGE: 1976 title = res.getString(R.string.failed_to_resize_image); 1977 message = res.getString(R.string.resize_image_error_information); 1978 break; 1979 default: 1980 throw new IllegalArgumentException("unknown error " + error); 1981 } 1982 1983 MessageUtils.showErrorDialog(this, title, message); 1984 } 1985 1986 private void addImage(Uri uri) { 1987 int result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, uri); 1988 1989 if (result == WorkingMessage.IMAGE_TOO_LARGE) { 1990 MessageUtils.resizeImageAsync(this, 1991 uri, mAttachmentEditorHandler, mResizeImageCallback); 1992 return; 1993 } 1994 handleAddAttachmentError(result, R.string.type_picture); 1995 } 1996 1997 private void addVideo(Uri uri) { 1998 int result = mWorkingMessage.setAttachment(WorkingMessage.VIDEO, uri); 1999 handleAddAttachmentError(result, R.string.type_video); 2000 } 2001 2002 private void addAudio(Uri uri) { 2003 int result = mWorkingMessage.setAttachment(WorkingMessage.AUDIO, uri); 2004 handleAddAttachmentError(result, R.string.type_audio); 2005 } 2006 2007 private boolean handleForwardedMessage() { 2008 Intent intent = getIntent(); 2009 2010 // If this is a forwarded message, it will have an Intent extra 2011 // indicating so. If not, bail out. 2012 if (intent.getBooleanExtra("forwarded_message", false) == false) { 2013 return false; 2014 } 2015 2016 Uri uri = intent.getParcelableExtra("msg_uri"); 2017 if (uri != null) { 2018 mWorkingMessage = WorkingMessage.load(this, uri); 2019 mWorkingMessage.setSubject(intent.getStringExtra("subject")); 2020 } else { 2021 mWorkingMessage.setText(intent.getStringExtra("sms_body")); 2022 } 2023 2024 return true; 2025 } 2026 2027 private boolean handleSendIntent(Intent intent) { 2028 Bundle extras = intent.getExtras(); 2029 2030 if (!Intent.ACTION_SEND.equals(intent.getAction()) || (extras == null)) { 2031 return false; 2032 } 2033 2034 if (extras.containsKey(Intent.EXTRA_STREAM)) { 2035 Uri uri = (Uri)extras.getParcelable(Intent.EXTRA_STREAM); 2036 if (uri != null) { 2037 if (intent.getType().startsWith("image/")) { 2038 addImage(uri); 2039 } else if (intent.getType().startsWith("video/")) { 2040 addVideo(uri); 2041 } 2042 } 2043 return true; 2044 } else if (extras.containsKey(Intent.EXTRA_TEXT)) { 2045 mWorkingMessage.setText(extras.getString(Intent.EXTRA_TEXT)); 2046 return true; 2047 } 2048 2049 return false; 2050 } 2051 2052 private String getResourcesString(int id, String mediaName) { 2053 Resources r = getResources(); 2054 return r.getString(id, mediaName); 2055 } 2056 2057 private void drawBottomPanel() { 2058 // Reset the counter for text editor. 2059 resetCounter(); 2060 2061 if (mWorkingMessage.hasSlideshow()) { 2062 mBottomPanel.setVisibility(View.GONE); 2063 mAttachmentEditor.requestFocus(); 2064 return; 2065 } 2066 2067 mBottomPanel.setVisibility(View.VISIBLE); 2068 mTextEditor.setText(mWorkingMessage.getText()); 2069 } 2070 2071 private void drawTopPanel() { 2072 showSubjectEditor(mWorkingMessage.hasSubject()); 2073 } 2074 2075 //========================================================== 2076 // Interface methods 2077 //========================================================== 2078 2079 public void onClick(View v) { 2080 if ((v == mSendButton) && isPreparedForSending()) { 2081 confirmSendMessageIfNeeded(); 2082 } 2083 } 2084 2085 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 2086 if (event != null) { 2087 if (!event.isShiftPressed()) { 2088 if (isPreparedForSending()) { 2089 sendMessage(); 2090 } 2091 return true; 2092 } 2093 return false; 2094 } 2095 2096 if (isPreparedForSending()) { 2097 confirmSendMessageIfNeeded(); 2098 } 2099 return true; 2100 } 2101 2102 private final TextWatcher mTextEditorWatcher = new TextWatcher() { 2103 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 2104 } 2105 2106 public void onTextChanged(CharSequence s, int start, int before, int count) { 2107 // This is a workaround for bug 1609057. Since onUserInteraction() is 2108 // not called when the user touches the soft keyboard, we pretend it was 2109 // called when textfields changes. This should be removed when the bug 2110 // is fixed. 2111 onUserInteraction(); 2112 2113 mWorkingMessage.setText(s); 2114 2115 updateSendButtonState(); 2116 2117 updateCounter(s, start, before, count); 2118 } 2119 2120 public void afterTextChanged(Editable s) { 2121 } 2122 }; 2123 2124 private final TextWatcher mSubjectEditorWatcher = new TextWatcher() { 2125 public void beforeTextChanged(CharSequence s, int start, int count, int after) { } 2126 2127 public void onTextChanged(CharSequence s, int start, int before, int count) { 2128 mWorkingMessage.setSubject(s); 2129 } 2130 2131 public void afterTextChanged(Editable s) { } 2132 }; 2133 2134 //========================================================== 2135 // Private methods 2136 //========================================================== 2137 2138 /** 2139 * Initialize all UI elements from resources. 2140 */ 2141 private void initResourceRefs() { 2142 mMsgListView = (MessageListView) findViewById(R.id.history); 2143 mMsgListView.setDivider(null); // no divider so we look like IM conversation. 2144 mBottomPanel = findViewById(R.id.bottom_panel); 2145 mTextEditor = (EditText) findViewById(R.id.embedded_text_editor); 2146 mTextEditor.setOnEditorActionListener(this); 2147 mTextEditor.addTextChangedListener(mTextEditorWatcher); 2148 mTextCounter = (TextView) findViewById(R.id.text_counter); 2149 mSendButton = (Button) findViewById(R.id.send_button); 2150 mSendButton.setOnClickListener(this); 2151 mTopPanel = findViewById(R.id.recipients_subject_linear); 2152 mTopPanel.setFocusable(false); 2153 mAttachmentEditor = (AttachmentEditor) findViewById(R.id.attachment_editor); 2154 mAttachmentEditor.setHandler(mAttachmentEditorHandler); 2155 } 2156 2157 private void confirmDeleteDialog(OnClickListener listener, boolean allMessages) { 2158 AlertDialog.Builder builder = new AlertDialog.Builder(this); 2159 builder.setTitle(R.string.confirm_dialog_title); 2160 builder.setIcon(android.R.drawable.ic_dialog_alert); 2161 builder.setCancelable(true); 2162 builder.setMessage(allMessages 2163 ? R.string.confirm_delete_conversation 2164 : R.string.confirm_delete_message); 2165 builder.setPositiveButton(R.string.yes, listener); 2166 builder.setNegativeButton(R.string.no, null); 2167 builder.show(); 2168 } 2169 2170 void undeliveredMessageDialog(long date) { 2171 String body; 2172 LinearLayout dialog = (LinearLayout) LayoutInflater.from(this).inflate( 2173 R.layout.retry_sending_dialog, null); 2174 2175 if (date >= 0) { 2176 body = getString(R.string.undelivered_msg_dialog_body, 2177 MessageUtils.formatTimeStampString(this, date)); 2178 } else { 2179 // FIXME: we can not get sms retry time. 2180 body = getString(R.string.undelivered_sms_dialog_body); 2181 } 2182 2183 ((TextView) dialog.findViewById(R.id.body_text_view)).setText(body); 2184 2185 Toast undeliveredDialog = new Toast(this); 2186 undeliveredDialog.setView(dialog); 2187 undeliveredDialog.setDuration(Toast.LENGTH_LONG); 2188 undeliveredDialog.show(); 2189 } 2190 2191 private void startMsgListQuery() { 2192 Uri conversationUri = mConversation.getUri(); 2193 if (conversationUri == null) { 2194 return; 2195 } 2196 2197 // Cancel any pending queries 2198 mBackgroundQueryHandler.cancelOperation(MESSAGE_LIST_QUERY_TOKEN); 2199 try { 2200 // Kick off the new query 2201 mBackgroundQueryHandler.startQuery( 2202 MESSAGE_LIST_QUERY_TOKEN, null, conversationUri, 2203 PROJECTION, null, null, null); 2204 } catch (SQLiteException e) { 2205 SqliteWrapper.checkSQLiteException(this, e); 2206 } 2207 } 2208 2209 private void initMessageList() { 2210 if (mMsgListAdapter != null) { 2211 return; 2212 } 2213 2214 // Initialize the list adapter with a null cursor. 2215 mMsgListAdapter = new MessageListAdapter(this, null, mMsgListView, true); 2216 mMsgListAdapter.setOnDataSetChangedListener(mDataSetChangedListener); 2217 mMsgListAdapter.setMsgListItemHandler(mMessageListItemHandler); 2218 mMsgListView.setAdapter(mMsgListAdapter); 2219 mMsgListView.setItemsCanFocus(false); 2220 mMsgListView.setVisibility(View.VISIBLE); 2221 mMsgListView.setOnCreateContextMenuListener(mMsgListMenuCreateListener); 2222 mMsgListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 2223 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 2224 ((MessageListItem) view).onMessageListItemClick(); 2225 } 2226 }); 2227 } 2228 2229 private void loadDraft() { 2230 if (mWorkingMessage.isWorthSaving()) { 2231 Log.w(TAG, "loadDraft() called with non-empty working message"); 2232 return; 2233 } 2234 2235 mWorkingMessage = WorkingMessage.loadDraft(this, mConversation); 2236 } 2237 2238 private void saveDraft() { 2239 // TODO: Do something better here. Maybe make discard() legal 2240 // to call twice and make isEmpty() return true if discarded 2241 // so it is caught in the clause above this one? 2242 if (mWorkingMessage.isDiscarded()) { 2243 return; 2244 } 2245 2246 if (!mWaitingForSubActivity && !mWorkingMessage.isWorthSaving()) { 2247 mWorkingMessage.discard(); 2248 return; 2249 } 2250 mWorkingMessage.saveDraft(); 2251 2252 if (mToastForDraftSave) { 2253 Toast.makeText(this, R.string.message_saved_as_draft, 2254 Toast.LENGTH_SHORT).show(); 2255 } 2256 } 2257 2258 private boolean isPreparedForSending() { 2259 boolean hasRecipient; 2260 2261 // To avoid creating a bunch of invalid Contacts when the recipients 2262 // editor is in flux, we keep the recipients list empty. So if the 2263 // recipients editor is showing, see if there is anything in it rather 2264 // than consulting the empty recipient list. 2265 if (isRecipientsEditorVisible()) { 2266 hasRecipient = mRecipientsEditor.getText().length() > 0; 2267 } else { 2268 hasRecipient = getRecipients().size() > 0; 2269 } 2270 2271 return hasRecipient && (mWorkingMessage.hasAttachment() || mWorkingMessage.hasText()); 2272 } 2273 2274 private void sendMessage() { 2275 // send can change the recipients. Make sure we remove the listeners first and then add 2276 // them back once the recipient list has settled. 2277 removeRecipientsListeners(); 2278 mWorkingMessage.send(); 2279 addRecipientsListeners(); 2280 2281 // Reset the UI to be ready for the next message. 2282 resetMessage(); 2283 2284 // But bail out if we are supposed to exit after the message is sent. 2285 if (mExitOnSent) { 2286 finish(); 2287 } 2288 } 2289 2290 private void resetMessage() { 2291 // Make the attachment editor hide its view. 2292 mAttachmentEditor.hideView(); 2293 2294 // Hide the subject editor. 2295 showSubjectEditor(false); 2296 2297 // Focus to the text editor. 2298 mTextEditor.requestFocus(); 2299 2300 // We have to remove the text change listener while the text editor gets cleared and 2301 // we subsequently turn the message back into SMS. When the listener is listening while 2302 // doing the clearing, it's fighting to update its counts and itself try and turn 2303 // the message one way or the other. 2304 mTextEditor.removeTextChangedListener(mTextEditorWatcher); 2305 2306 // Clear the text box. 2307 TextKeyListener.clear(mTextEditor.getText()); 2308 2309 mWorkingMessage = WorkingMessage.createEmpty(this); 2310 mWorkingMessage.setConversation(mConversation); 2311 2312 // Hide the recipients editor. 2313 if (mRecipientsEditor != null) { 2314 mRecipientsEditor.setVisibility(View.GONE); 2315 hideOrShowTopPanel(); 2316 } 2317 2318 drawBottomPanel(); 2319 updateWindowTitle(); 2320 2321 // "Or not", in this case. 2322 updateSendButtonState(); 2323 2324 // Our changes are done. Let the listener respond to text changes once again. 2325 mTextEditor.addTextChangedListener(mTextEditorWatcher); 2326 2327 // Close the soft on-screen keyboard if we're in landscape mode so the user can see the 2328 // conversation. 2329 if (mIsLandscape) { 2330 InputMethodManager inputMethodManager = 2331 (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); 2332 2333 inputMethodManager.hideSoftInputFromWindow(mTextEditor.getWindowToken(), 0); 2334 } 2335 } 2336 2337 private void updateSendButtonState() { 2338 boolean enable = false; 2339 if (isPreparedForSending()) { 2340 // When the type of attachment is slideshow, we should 2341 // also hide the 'Send' button since the slideshow view 2342 // already has a 'Send' button embedded. 2343 if (!mWorkingMessage.hasSlideshow()) { 2344 enable = true; 2345 } else { 2346 mAttachmentEditor.setCanSend(true); 2347 } 2348 } else if (null != mAttachmentEditor){ 2349 mAttachmentEditor.setCanSend(false); 2350 } 2351 2352 mSendButton.setEnabled(enable); 2353 mSendButton.setFocusable(enable); 2354 } 2355 2356 private long getMessageDate(Uri uri) { 2357 if (uri != null) { 2358 Cursor cursor = SqliteWrapper.query(this, mContentResolver, 2359 uri, new String[] { Mms.DATE }, null, null, null); 2360 if (cursor != null) { 2361 try { 2362 if ((cursor.getCount() == 1) && cursor.moveToFirst()) { 2363 return cursor.getLong(0) * 1000L; 2364 } 2365 } finally { 2366 cursor.close(); 2367 } 2368 } 2369 } 2370 return NO_DATE_FOR_DIALOG; 2371 } 2372 2373 private void initActivityState(Bundle bundle, Intent intent) { 2374 if (bundle != null) { 2375 String recipients = bundle.getString("recipients"); 2376 mConversation = Conversation.get(this, ContactList.getByNumbers(recipients, false)); 2377 mExitOnSent = bundle.getBoolean("exit_on_sent", false); 2378 mWorkingMessage.readStateFromBundle(bundle); 2379 return; 2380 } 2381 2382 // If we have been passed a thread_id, use that to find our 2383 // conversation. 2384 long threadId = intent.getLongExtra("thread_id", 0); 2385 if (threadId > 0) { 2386 mConversation = Conversation.get(this, threadId); 2387 } else { 2388 // Otherwise, try to get a conversation based on the 2389 // data URI passed to our intent. 2390 mConversation = Conversation.get(this, intent.getData()); 2391 } 2392 2393 mExitOnSent = intent.getBooleanExtra("exit_on_sent", false); 2394 mWorkingMessage.setText(intent.getStringExtra("sms_body")); 2395 mWorkingMessage.setSubject(intent.getStringExtra("subject")); 2396 } 2397 2398 private void updateWindowTitle() { 2399 ContactList recipients = getRecipients(); 2400 if (recipients.size() > 0) { 2401 setTitle(recipients.formatNamesAndNumbers(", ")); 2402 } else { 2403 setTitle(getString(R.string.compose_title)); 2404 } 2405 } 2406 2407 private void initFocus() { 2408 if (!mIsKeyboardOpen) { 2409 return; 2410 } 2411 2412 // If the recipients editor is visible, there is nothing in it, 2413 // and the text editor is not already focused, focus the 2414 // recipients editor. 2415 if (isRecipientsEditorVisible() && TextUtils.isEmpty(mRecipientsEditor.getText()) 2416 && !mTextEditor.isFocused()) { 2417 mRecipientsEditor.requestFocus(); 2418 return; 2419 } 2420 2421 // If we decided not to focus the recipients editor, focus the text editor. 2422 mTextEditor.requestFocus(); 2423 } 2424 2425 private final MessageListAdapter.OnDataSetChangedListener 2426 mDataSetChangedListener = new MessageListAdapter.OnDataSetChangedListener() { 2427 public void onDataSetChanged(MessageListAdapter adapter) { 2428 mPossiblePendingNotification = true; 2429 } 2430 }; 2431 2432 private void checkPendingNotification() { 2433 if (mPossiblePendingNotification && hasWindowFocus()) { 2434 mConversation.markAsRead(); 2435 mPossiblePendingNotification = false; 2436 } 2437 } 2438 2439 private final class BackgroundQueryHandler extends AsyncQueryHandler { 2440 public BackgroundQueryHandler(ContentResolver contentResolver) { 2441 super(contentResolver); 2442 } 2443 2444 @Override 2445 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 2446 switch(token) { 2447 case MESSAGE_LIST_QUERY_TOKEN: 2448 mMsgListAdapter.changeCursor(cursor); 2449 2450 // Once we have completed the query for the message history, if 2451 // there is nothing in the cursor and we are not composing a new 2452 // message, we must be editing a draft in a new conversation. 2453 // Show the recipients editor to give the user a chance to add 2454 // more people before the conversation begins. 2455 if (cursor.getCount() == 0 && !isRecipientsEditorVisible()) { 2456 initRecipientsEditor(); 2457 } 2458 2459 // FIXME: freshing layout changes the focused view to an unexpected 2460 // one, set it back to TextEditor forcely. 2461 mTextEditor.requestFocus(); 2462 2463 return; 2464 2465 case CALLER_ID_QUERY_TOKEN: 2466 case EMAIL_CONTACT_QUERY_TOKEN: 2467 cleanupContactInfoCursor(); 2468 mContactInfoCursor = cursor; 2469 updateContactInfo(); 2470 return; 2471 2472 } 2473 } 2474 2475 @Override 2476 protected void onDeleteComplete(int token, Object cookie, int result) { 2477 switch(token) { 2478 case DELETE_MESSAGE_TOKEN: 2479 case DELETE_CONVERSATION_TOKEN: 2480 // Update the notification for new messages since they 2481 // may be deleted. 2482 MessagingNotification.updateNewMessageIndicator( 2483 ComposeMessageActivity.this); 2484 // Update the notification for failed messages since they 2485 // may be deleted. 2486 updateSendFailedNotification(); 2487 break; 2488 } 2489 2490 // If we're deleting the whole conversation, throw away 2491 // our current working message and bail. 2492 if (token == DELETE_CONVERSATION_TOKEN) { 2493 mWorkingMessage.discard(); 2494 finish(); 2495 } 2496 } 2497 2498 @Override 2499 protected void onUpdateComplete(int token, Object cookie, int result) { 2500 switch(token) { 2501 case MARK_AS_READ_TOKEN: 2502 MessagingNotification.updateAllNotifications(ComposeMessageActivity.this); 2503 break; 2504 } 2505 } 2506 } 2507 2508 private void showSmileyDialog() { 2509 if (mSmileyDialog == null) { 2510 int[] icons = SmileyParser.DEFAULT_SMILEY_RES_IDS; 2511 String[] names = getResources().getStringArray( 2512 SmileyParser.DEFAULT_SMILEY_NAMES); 2513 final String[] texts = getResources().getStringArray( 2514 SmileyParser.DEFAULT_SMILEY_TEXTS); 2515 2516 final int N = names.length; 2517 2518 List<Map<String, ?>> entries = new ArrayList<Map<String, ?>>(); 2519 for (int i = 0; i < N; i++) { 2520 // We might have different ASCII for the same icon, skip it if 2521 // the icon is already added. 2522 boolean added = false; 2523 for (int j = 0; j < i; j++) { 2524 if (icons[i] == icons[j]) { 2525 added = true; 2526 break; 2527 } 2528 } 2529 if (!added) { 2530 HashMap<String, Object> entry = new HashMap<String, Object>(); 2531 2532 entry. put("icon", icons[i]); 2533 entry. put("name", names[i]); 2534 entry.put("text", texts[i]); 2535 2536 entries.add(entry); 2537 } 2538 } 2539 2540 final SimpleAdapter a = new SimpleAdapter( 2541 this, 2542 entries, 2543 R.layout.smiley_menu_item, 2544 new String[] {"icon", "name", "text"}, 2545 new int[] {R.id.smiley_icon, R.id.smiley_name, R.id.smiley_text}); 2546 SimpleAdapter.ViewBinder viewBinder = new SimpleAdapter.ViewBinder() { 2547 public boolean setViewValue(View view, Object data, String textRepresentation) { 2548 if (view instanceof ImageView) { 2549 Drawable img = getResources().getDrawable((Integer)data); 2550 ((ImageView)view).setImageDrawable(img); 2551 return true; 2552 } 2553 return false; 2554 } 2555 }; 2556 a.setViewBinder(viewBinder); 2557 2558 AlertDialog.Builder b = new AlertDialog.Builder(this); 2559 2560 b.setTitle(getString(R.string.menu_insert_smiley)); 2561 2562 b.setCancelable(true); 2563 b.setAdapter(a, new DialogInterface.OnClickListener() { 2564 @SuppressWarnings("unchecked") 2565 public final void onClick(DialogInterface dialog, int which) { 2566 HashMap<String, Object> item = (HashMap<String, Object>) a.getItem(which); 2567 mTextEditor.append((String)item.get("text")); 2568 } 2569 }); 2570 2571 mSmileyDialog = b.create(); 2572 } 2573 2574 mSmileyDialog.show(); 2575 } 2576 2577 private void cleanupContactInfoCursor() { 2578 if (mContactInfoCursor != null) { 2579 mContactInfoCursor.close(); 2580 } 2581 } 2582 2583 private void startQueryForContactInfo() { 2584 ContactList recipients = getRecipients(); 2585 2586 if (recipients.size() != 1) { 2587 setPresenceIcon(0); 2588 return; 2589 } 2590 2591 String number = recipients.get(0).getNumber(); 2592 mContactInfoSelectionArgs[0] = number; 2593 2594 if (Mms.isEmailAddress(number)) { 2595 // Cancel any pending queries 2596 mBackgroundQueryHandler.cancelOperation(EMAIL_CONTACT_QUERY_TOKEN); 2597 2598 mBackgroundQueryHandler.startQuery(EMAIL_CONTACT_QUERY_TOKEN, null, 2599 METHOD_WITH_PRESENCE_URI, 2600 EMAIL_QUERY_PROJECTION, 2601 METHOD_LOOKUP, 2602 mContactInfoSelectionArgs, 2603 null); 2604 } else { 2605 // Cancel any pending queries 2606 mBackgroundQueryHandler.cancelOperation(CALLER_ID_QUERY_TOKEN); 2607 2608 mBackgroundQueryHandler.startQuery(CALLER_ID_QUERY_TOKEN, null, 2609 PHONES_WITH_PRESENCE_URI, 2610 CALLER_ID_PROJECTION, 2611 NUMBER_LOOKUP, 2612 mContactInfoSelectionArgs, 2613 null); 2614 } 2615 } 2616 2617 private void updateContactInfo() { 2618 boolean updated = false; 2619 if (mContactInfoCursor != null && mContactInfoCursor.moveToFirst()) { 2620 mPresenceStatus = mContactInfoCursor.getInt(PRESENCE_STATUS_COLUMN); 2621 if (mPresenceStatus != Contacts.People.OFFLINE) { 2622 int presenceIcon = Presence.getPresenceIconResourceId(mPresenceStatus); 2623 setPresenceIcon(presenceIcon); 2624 updated = true; 2625 } 2626 } 2627 if (!updated) { 2628 setPresenceIcon(0); 2629 } 2630 } 2631 2632 public void onUpdate(final Contact updated) { 2633 // Using an existing handler for the post, rather than conjuring up a new one. 2634 mMessageListItemHandler.post(new Runnable() { 2635 public void run() { 2636 setPresenceIcon(updated.getPresenceResId()); 2637 } 2638 }); 2639 } 2640 2641 private void addRecipientsListeners() { 2642 ContactList recipients = getRecipients(); 2643 recipients.addListeners(this); 2644 } 2645 2646 private void removeRecipientsListeners() { 2647 ContactList recipients = getRecipients(); 2648 recipients.removeListeners(this); 2649 } 2650} 2651 2652 2653 2654 2655 2656