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