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