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