ComposeMessageActivity.java revision 71cf4b0b2901590fffd04c7cdeba8d1c05c4c214
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.ExceedMessageSizeException; 30import com.android.mms.MmsConfig; 31import com.android.mms.R; 32import com.android.mms.ResolutionException; 33import com.android.mms.UnsupportContentTypeException; 34import com.android.mms.model.SlideModel; 35import com.android.mms.model.SlideshowModel; 36import com.android.mms.model.TextModel; 37import com.android.mms.transaction.MessageSender; 38import com.android.mms.transaction.MessagingNotification; 39import com.android.mms.transaction.MmsMessageSender; 40import com.android.mms.transaction.SmsMessageSender; 41import com.android.mms.ui.AttachmentEditor.OnAttachmentChangedListener; 42import com.android.mms.ui.MessageUtils.ResizeImageResultCallback; 43import com.android.mms.ui.RecipientList.Recipient; 44import com.android.mms.ui.RecipientsEditor.RecipientContextMenuInfo; 45import com.android.mms.util.ContactInfoCache; 46import com.android.mms.util.DraftCache; 47import com.android.mms.util.SendingProgressTokenManager; 48import com.android.mms.util.SmileyParser; 49 50import com.google.android.mms.ContentType; 51import com.google.android.mms.MmsException; 52import com.google.android.mms.pdu.EncodedStringValue; 53import com.google.android.mms.pdu.PduBody; 54import com.google.android.mms.pdu.PduPart; 55import com.google.android.mms.pdu.PduPersister; 56import com.google.android.mms.pdu.SendReq; 57import com.google.android.mms.util.SqliteWrapper; 58 59import android.app.Activity; 60import android.app.AlertDialog; 61import android.content.AsyncQueryHandler; 62import android.content.BroadcastReceiver; 63import android.content.ContentResolver; 64import android.content.ContentUris; 65import android.content.ContentValues; 66import android.content.Context; 67import android.content.DialogInterface; 68import android.content.Intent; 69import android.content.IntentFilter; 70import android.content.DialogInterface.OnClickListener; 71import android.content.res.Configuration; 72import android.content.res.Resources; 73import android.database.Cursor; 74import android.database.DatabaseUtils; 75import android.database.sqlite.SQLiteException; 76import android.graphics.Bitmap; 77import android.graphics.drawable.Drawable; 78import android.media.RingtoneManager; 79import android.net.Uri; 80import android.os.Bundle; 81import android.os.Handler; 82import android.os.Message; 83import android.provider.Contacts; 84import android.provider.Contacts.People; 85import android.provider.Contacts.Presence; 86import android.provider.MediaStore; 87import android.provider.Settings; 88import android.provider.Telephony.Mms; 89import android.provider.Telephony.Sms; 90import android.provider.Telephony.Threads; 91import android.telephony.gsm.SmsMessage; 92import android.text.ClipboardManager; 93import android.text.Editable; 94import android.text.InputFilter; 95import android.text.SpannableString; 96import android.text.Spanned; 97import android.text.TextUtils; 98import android.text.TextWatcher; 99import android.text.method.TextKeyListener; 100import android.text.style.URLSpan; 101import android.text.util.Linkify; 102import android.util.Config; 103import android.util.Log; 104import android.view.ContextMenu; 105import android.view.KeyEvent; 106import android.view.LayoutInflater; 107import android.view.Menu; 108import android.view.MenuItem; 109import android.view.View; 110import android.view.ViewStub; 111import android.view.Window; 112import android.view.ContextMenu.ContextMenuInfo; 113import android.view.View.OnCreateContextMenuListener; 114import android.view.View.OnFocusChangeListener; 115import android.view.View.OnKeyListener; 116import android.view.inputmethod.InputMethodManager; 117import android.widget.AdapterView; 118import android.widget.Button; 119import android.widget.EditText; 120import android.widget.ImageView; 121import android.widget.LinearLayout; 122import android.widget.ListView; 123import android.widget.SimpleAdapter; 124import android.widget.TextView; 125import android.widget.Toast; 126 127import java.io.InputStream; 128import java.io.IOException; 129import java.io.File; 130import java.io.FileInputStream; 131import java.io.FileOutputStream; 132import java.util.ArrayList; 133import java.util.Arrays; 134import java.util.HashMap; 135import java.util.HashSet; 136import java.util.Iterator; 137import java.util.List; 138import java.util.Map; 139 140import android.webkit.MimeTypeMap; 141 142/** 143 * This is the main UI for: 144 * 1. Composing a new message; 145 * 2. Viewing/managing message history of a conversation. 146 * 147 * This activity can handle following parameters from the intent 148 * by which it's launched. 149 * thread_id long Identify the conversation to be viewed. When creating a 150 * new message, this parameter shouldn't be present. 151 * msg_uri Uri The message which should be opened for editing in the editor. 152 * address String The addresses of the recipients in current conversation. 153 * exit_on_sent boolean Exit this activity after the message is sent. 154 */ 155public class ComposeMessageActivity extends Activity 156 implements View.OnClickListener, TextView.OnEditorActionListener, 157 OnAttachmentChangedListener { 158 public static final int REQUEST_CODE_ATTACH_IMAGE = 10; 159 public static final int REQUEST_CODE_TAKE_PICTURE = 11; 160 public static final int REQUEST_CODE_ATTACH_VIDEO = 12; 161 public static final int REQUEST_CODE_TAKE_VIDEO = 13; 162 public static final int REQUEST_CODE_ATTACH_SOUND = 14; 163 public static final int REQUEST_CODE_RECORD_SOUND = 15; 164 public static final int REQUEST_CODE_CREATE_SLIDESHOW = 16; 165 166 private static final String TAG = "ComposeMessageActivity"; 167 private static final boolean DEBUG = false; 168 private static final boolean TRACE = false; 169 private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; 170 171 // Menu ID 172 private static final int MENU_ADD_SUBJECT = 0; 173 private static final int MENU_DELETE_THREAD = 1; 174 private static final int MENU_ADD_ATTACHMENT = 2; 175 private static final int MENU_DISCARD = 3; 176 private static final int MENU_SEND = 4; 177 private static final int MENU_CALL_RECIPIENT = 5; 178 private static final int MENU_CONVERSATION_LIST = 6; 179 180 // Context menu ID 181 private static final int MENU_VIEW_CONTACT = 12; 182 private static final int MENU_ADD_TO_CONTACTS = 13; 183 184 private static final int MENU_EDIT_MESSAGE = 14; 185 private static final int MENU_VIEW_SLIDESHOW = 16; 186 private static final int MENU_VIEW_MESSAGE_DETAILS = 17; 187 private static final int MENU_DELETE_MESSAGE = 18; 188 private static final int MENU_SEARCH = 19; 189 private static final int MENU_DELIVERY_REPORT = 20; 190 private static final int MENU_FORWARD_MESSAGE = 21; 191 private static final int MENU_CALL_BACK = 22; 192 private static final int MENU_SEND_EMAIL = 23; 193 private static final int MENU_COPY_MESSAGE_TEXT = 24; 194 private static final int MENU_COPY_TO_SDCARD = 25; 195 private static final int MENU_INSERT_SMILEY = 26; 196 private static final int MENU_ADD_ADDRESS_TO_CONTACTS = 27; 197 198 private static final int SUBJECT_MAX_LENGTH = 40; 199 private static final int RECIPIENTS_MAX_LENGTH = 312; 200 201 private static final int MESSAGE_LIST_QUERY_TOKEN = 9527; 202 203 private static final int DELETE_MESSAGE_TOKEN = 9700; 204 private static final int DELETE_CONVERSATION_TOKEN = 9701; 205 206 private static final int CALLER_ID_QUERY_TOKEN = 9800; 207 private static final int EMAIL_CONTACT_QUERY_TOKEN = 9801; 208 209 private static final int MARK_AS_READ_TOKEN = 9900; 210 211 private static final int MMS_THRESHOLD = 4; 212 213 private static final int CHARS_REMAINING_BEFORE_COUNTER_SHOWN = 10; 214 215 private static final long NO_DATE_FOR_DIALOG = -1L; 216 217 private static final int REFRESH_PRESENCE = 45236; 218 219 220 // caller id query params 221 private static final String[] CALLER_ID_PROJECTION = new String[] { 222 People.PRESENCE_STATUS, // 0 223 }; 224 private static final int PRESENCE_STATUS_COLUMN = 0; 225 226 private static final String NUMBER_LOOKUP = "PHONE_NUMBERS_EQUAL(" 227 + Contacts.Phones.NUMBER + ",?)"; 228 private static final Uri PHONES_WITH_PRESENCE_URI 229 = Uri.parse(Contacts.Phones.CONTENT_URI + "_with_presence"); 230 231 // email contact query params 232 private static final String[] EMAIL_QUERY_PROJECTION = new String[] { 233 Contacts.People.PRESENCE_STATUS, // 0 234 }; 235 236 private static final String METHOD_LOOKUP = Contacts.ContactMethods.DATA + "=?"; 237 private static final Uri METHOD_WITH_PRESENCE_URI = 238 Uri.withAppendedPath(Contacts.ContactMethods.CONTENT_URI, "with_presence"); 239 240 241 242 private ContentResolver mContentResolver; 243 244 // The parameters/states of the activity. 245 private long mThreadId; // Database key for the current conversation 246 private String mExternalAddress; // Serialized recipients in the current conversation 247 private boolean mExitOnSent; // Should we finish() after sending a message? 248 249 private View mTopPanel; // View containing the recipient and subject editors 250 private View mBottomPanel; // View containing the text editor, send button, ec. 251 private EditText mTextEditor; // Text editor to type your message into 252 private TextView mTextCounter; // Shows the number of characters used in text editor 253 private Button mSendButton; // Press to detonate 254 255 private CharSequence mMsgText; // Text of message 256 257 private BackgroundQueryHandler mBackgroundQueryHandler; 258 259 private MessageListView mMsgListView; // ListView for messages in this conversation 260 private MessageListAdapter mMsgListAdapter; // and its corresponding ListAdapter 261 262 private RecipientList mRecipientList; // List of recipients for this conversation 263 private RecipientsEditor mRecipientsEditor; // UI control for editing recipients 264 265 private boolean mIsKeyboardOpen; // Whether the hardware keyboard is visible 266 private boolean mIsLandscape; // Whether we're in landscape mode 267 268 private boolean mPossiblePendingNotification; // If the message list has changed, we may have 269 // a pending notification to deal with. 270 271 private boolean mToastForDraftSave; // Whether to notify the user that a draft is 272 // being saved. 273 274 private static final int RECIPIENTS_REQUIRE_MMS = (1 << 0); // 1 275 private static final int HAS_SUBJECT = (1 << 1); // 2 276 private static final int HAS_ATTACHMENT = (1 << 2); // 4 277 private static final int LENGTH_REQUIRES_MMS = (1 << 3); // 8 278 279 private int mMessageState; // A bitmap of the above indicating different 280 // properties of the message -- any bit set 281 // will require conversion to MMS. 282 283 // These fields are only used in MMS compose mode (requiresMms() == true) and should 284 // otherwise be null. 285 private SlideshowModel mSlideshow; 286 private Uri mMessageUri; 287 private EditText mSubjectTextEditor; // Text editor for MMS subject 288 private String mSubject; // MMS subject 289 private AttachmentEditor mAttachmentEditor; 290 private PduPersister mPersister; 291 292 private AlertDialog mSmileyDialog; 293 294 // Everything needed to deal with presence 295 private Cursor mContactInfoCursor; 296 private int mPresenceStatus; 297 private String[] mContactInfoSelectionArgs = new String[1]; 298 299 private boolean mWaitingForSubActivity; 300 301 302 //========================================================== 303 // Inner classes 304 //========================================================== 305 306 private final Handler mAttachmentEditorHandler = new Handler() { 307 @Override 308 public void handleMessage(Message msg) { 309 switch (msg.what) { 310 case AttachmentEditor.MSG_EDIT_SLIDESHOW: { 311 Intent intent = new Intent(ComposeMessageActivity.this, 312 SlideshowEditActivity.class); 313 // Need this to make sure mMessageUri is set up. 314 convertMessageIfNeeded(HAS_ATTACHMENT, true); 315 intent.setData(mMessageUri); 316 startActivityForResult(intent, REQUEST_CODE_CREATE_SLIDESHOW); 317 break; 318 } 319 case AttachmentEditor.MSG_SEND_SLIDESHOW: { 320 if (isPreparedForSending()) { 321 ComposeMessageActivity.this.confirmSendMessageIfNeeded(); 322 } 323 break; 324 } 325 case AttachmentEditor.MSG_VIEW_IMAGE: 326 case AttachmentEditor.MSG_PLAY_VIDEO: 327 case AttachmentEditor.MSG_PLAY_AUDIO: 328 case AttachmentEditor.MSG_PLAY_SLIDESHOW: 329 MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this, 330 mMessageUri, mSlideshow, mPersister); 331 break; 332 333 case AttachmentEditor.MSG_REPLACE_IMAGE: 334 case AttachmentEditor.MSG_REPLACE_VIDEO: 335 case AttachmentEditor.MSG_REPLACE_AUDIO: 336 showAddAttachmentDialog(); 337 break; 338 339 default: 340 break; 341 } 342 } 343 }; 344 345 private final Handler mMessageListItemHandler = new Handler() { 346 @Override 347 public void handleMessage(Message msg) { 348 String type; 349 switch (msg.what) { 350 case MessageListItem.MSG_LIST_EDIT_MMS: 351 type = "mms"; 352 break; 353 case MessageListItem.MSG_LIST_EDIT_SMS: 354 type = "sms"; 355 break; 356 default: 357 Log.w(TAG, "Unknown message: " + msg.what); 358 return; 359 } 360 361 MessageItem msgItem = getMessageItem(type, (Long) msg.obj); 362 if (msgItem != null) { 363 editMessageItem(msgItem); 364 int attachmentType = requiresMms() 365 ? MessageUtils.getAttachmentType(mSlideshow) 366 : AttachmentEditor.TEXT_ONLY; 367 drawBottomPanel(attachmentType); 368 } 369 } 370 }; 371 372 private final Handler mPresencePollingHandler = new Handler() { 373 @Override 374 public void handleMessage(Message msg) { 375 if (msg.what == REFRESH_PRESENCE) { 376 startQueryForContactInfo(); 377 } 378 } 379 }; 380 381 private final OnKeyListener mSubjectKeyListener = new OnKeyListener() { 382 public boolean onKey(View v, int keyCode, KeyEvent event) { 383 if (event.getAction() != KeyEvent.ACTION_DOWN) { 384 return false; 385 } 386 387 // When the subject editor is empty, press "DEL" to hide the input field. 388 if ((keyCode == KeyEvent.KEYCODE_DEL) && (mSubjectTextEditor.length() == 0)) { 389 mSubjectTextEditor.setVisibility(View.GONE); 390 ComposeMessageActivity.this.hideTopPanelIfNecessary(); 391 convertMessageIfNeeded(HAS_SUBJECT, false); 392 return true; 393 } 394 395 return false; 396 } 397 }; 398 399 private MessageItem getMessageItem(String type, long msgId) { 400 // Check whether the cursor is valid or not. 401 Cursor cursor = mMsgListAdapter.getCursor(); 402 if (cursor.isClosed() || cursor.isBeforeFirst() || cursor.isAfterLast()) { 403 Log.e(TAG, "Bad cursor.", new RuntimeException()); 404 return null; 405 } 406 407 return mMsgListAdapter.getCachedMessageItem(type, msgId, cursor); 408 } 409 410 private void resetCounter() { 411 mTextCounter.setText(""); 412 mTextCounter.setVisibility(View.GONE); 413 } 414 415 private void updateCounter(CharSequence text, int start, int before, int count) { 416 // The worst case before we begin showing the text counter would be 417 // a UCS-2 message, providing space for 70 characters, minus 418 // CHARS_REMAINING_BEFORE_COUNTER_SHOWN. Don't bother calling 419 // the relatively expensive SmsMessage.calculateLength() until that 420 // point is reached. 421 if (text.length() < (70-CHARS_REMAINING_BEFORE_COUNTER_SHOWN)) { 422 mTextCounter.setVisibility(View.GONE); 423 return; 424 } 425 426 // If we're not removing text (i.e. no chance of converting back to SMS 427 // because of this change) and we're in MMS mode, just bail out. 428 final boolean textAdded = (before < count); 429 if (textAdded && requiresMms()) { 430 mTextCounter.setVisibility(View.GONE); 431 return; 432 } 433 434 int[] params = SmsMessage.calculateLength(text, false); 435 /* SmsMessage.calculateLength returns an int[4] with: 436 * int[0] being the number of SMS's required, 437 * int[1] the number of code units used, 438 * int[2] is the number of code units remaining until the next message. 439 * int[3] is the encoding type that should be used for the message. 440 */ 441 int msgCount = params[0]; 442 int remainingInCurrentMessage = params[2]; 443 444 // Convert to MMS if this message has gotten too long for SMS. 445 convertMessageIfNeeded(LENGTH_REQUIRES_MMS, msgCount >= MMS_THRESHOLD); 446 447 // Show the counter only if: 448 // - We are not in MMS mode 449 // - We are going to send more than one message OR we are getting close 450 boolean showCounter = false; 451 if (!requiresMms() && 452 (msgCount > 1 || remainingInCurrentMessage <= CHARS_REMAINING_BEFORE_COUNTER_SHOWN)) { 453 showCounter = true; 454 } 455 456 if (showCounter) { 457 // Update the remaining characters and number of messages required. 458 mTextCounter.setText(remainingInCurrentMessage + " / " + msgCount); 459 mTextCounter.setVisibility(View.VISIBLE); 460 } else { 461 mTextCounter.setVisibility(View.GONE); 462 } 463 } 464 465 private void initMmsComponents() { 466 // Initialize subject editor. 467 mSubjectTextEditor = (EditText) findViewById(R.id.subject); 468 mSubjectTextEditor.setOnKeyListener(mSubjectKeyListener); 469 mSubjectTextEditor.setFilters(new InputFilter[] { 470 new InputFilter.LengthFilter(SUBJECT_MAX_LENGTH) }); 471 if (!TextUtils.isEmpty(mSubject)) { 472 updateState(HAS_SUBJECT, true); 473 mSubjectTextEditor.setText(mSubject); 474 showSubjectEditor(); 475 } 476 477 try { 478 if (mMessageUri != null) { 479 // Move the message into Draft before editing it. 480 mMessageUri = mPersister.move(mMessageUri, Mms.Draft.CONTENT_URI); 481 mSlideshow = SlideshowModel.createFromMessageUri(this, mMessageUri); 482 } else { 483 mSlideshow = createNewSlideshow(this); 484 if (mMsgText != null) { 485 mSlideshow.get(0).getText().setText(mMsgText); 486 } 487 mMessageUri = createTemporaryMmsMessage(); 488 } 489 } catch (MmsException e) { 490 Log.e(TAG, e.getMessage(), e); 491 finish(); 492 return; 493 } 494 495 // Set up the attachment editor. 496 mAttachmentEditor = new AttachmentEditor(this, mAttachmentEditorHandler, 497 findViewById(R.id.attachment_editor)); 498 mAttachmentEditor.setOnAttachmentChangedListener(this); 499 500 int attachmentType = MessageUtils.getAttachmentType(mSlideshow); 501 fixEmptySlideshow(mSlideshow); 502 if (attachmentType == AttachmentEditor.EMPTY) { 503 attachmentType = AttachmentEditor.TEXT_ONLY; 504 } 505 mAttachmentEditor.setAttachment(mSlideshow, attachmentType); 506 507 if (attachmentType > AttachmentEditor.TEXT_ONLY) { 508 updateState(HAS_ATTACHMENT, true); 509 } 510 } 511 512 @Override 513 public void startActivityForResult(Intent intent, int requestCode) 514 { 515 // requestCode >= 0 means the activity in question is a sub-activity. 516 if (requestCode >= 0) { 517 mWaitingForSubActivity = true; 518 } 519 520 super.startActivityForResult(intent, requestCode); 521 } 522 523 synchronized private void uninitMmsComponents() { 524 // Get text from slideshow if needed. 525 if (mAttachmentEditor != null && mSlideshow != null) { 526 int attachmentType = mAttachmentEditor.getAttachmentType(); 527 if (AttachmentEditor.TEXT_ONLY == attachmentType && mSlideshow != null) { 528 SlideModel model = mSlideshow.get(0); 529 if (model != null) { 530 TextModel textModel = model.getText(); 531 if (textModel != null) { 532 mMsgText = textModel.getText(); 533 } 534 } 535 } 536 } 537 538 mMessageState = 0; 539 mSlideshow = null; 540 if (mMessageUri != null) { 541 // Not sure if this is the best way to do this.. 542 if (mMessageUri.toString().startsWith(Mms.Draft.CONTENT_URI.toString())) { 543 asyncDelete(mMessageUri, null, null); 544 mMessageUri = null; 545 } 546 } 547 if (mSubjectTextEditor != null) { 548 mSubjectTextEditor.setText(""); 549 mSubjectTextEditor.setVisibility(View.GONE); 550 hideTopPanelIfNecessary(); 551 mSubjectTextEditor = null; 552 } 553 mSubject = null; 554 mAttachmentEditor = null; 555 } 556 557 private void resetMmsComponents() { 558 mMessageState = RECIPIENTS_REQUIRE_MMS; 559 if (mSubjectTextEditor != null) { 560 mSubjectTextEditor.setText(""); 561 mSubjectTextEditor.setVisibility(View.GONE); 562 } 563 mSubject = null; 564 565 try { 566 mSlideshow = createNewSlideshow(this); 567 if (mMsgText != null) { 568 mSlideshow.get(0).getText().setText(mMsgText); 569 } 570 mMessageUri = createTemporaryMmsMessage(); 571 } catch (MmsException e) { 572 Log.e(TAG, e.getMessage(), e); 573 finish(); 574 return; 575 } 576 577 int attachmentType = MessageUtils.getAttachmentType(mSlideshow); 578 if (attachmentType == AttachmentEditor.EMPTY) { 579 fixEmptySlideshow(mSlideshow); 580 attachmentType = AttachmentEditor.TEXT_ONLY; 581 } 582 mAttachmentEditor.setAttachment(mSlideshow, attachmentType); 583 } 584 585 private boolean requiresMms() { 586 return (mMessageState > 0); 587 } 588 589 private boolean recipientsRequireMms() { 590 return mRecipientList.containsBcc() || mRecipientList.containsEmail(); 591 } 592 593 private boolean hasAttachment() { 594 return ((mAttachmentEditor != null) 595 && (mAttachmentEditor.getAttachmentType() > AttachmentEditor.TEXT_ONLY)); 596 } 597 598 private void updateState(int whichState, boolean set) { 599 if (set) { 600 mMessageState |= whichState; 601 } else { 602 mMessageState &= ~whichState; 603 } 604 } 605 606 private void convertMessage(boolean toMms) { 607 if (LOCAL_LOGV) { 608 Log.v(TAG, "Message type: " + (requiresMms() ? "MMS" : "SMS") 609 + " -> " + (toMms ? "MMS" : "SMS")); 610 } 611 if (toMms) { 612 // Hide the counter 613 if (mTextCounter != null) { 614 mTextCounter.setVisibility(View.GONE); 615 } 616 initMmsComponents(); 617 CharSequence mmsText = mSlideshow.get(0).getText().getText(); 618 // Show or hide the counter as necessary 619 updateCounter(mmsText, 0, 0, mmsText.length()); 620 } else { 621 uninitMmsComponents(); 622 // Show or hide the counter as necessary 623 updateCounter(mMsgText, 0, 0, mMsgText.length()); 624 } 625 626 updateSendButtonState(); 627 } 628 629 private void toastConvertInfo(boolean toMms) { 630 // If we didn't know whether to convert (e.g. resetting after message 631 // send, we need to notify the user. 632 int resId = toMms ? R.string.converting_to_picture_message 633 : R.string.converting_to_text_message; 634 Toast.makeText(this, resId, Toast.LENGTH_SHORT).show(); 635 } 636 637 private void convertMessageIfNeeded(int whichState, boolean set) { 638 convertMessageIfNeeded(whichState, set, true); 639 } 640 641 private void convertMessageIfNeeded(int whichState, boolean set, boolean toast) { 642 int oldState = mMessageState; 643 updateState(whichState, set); 644 645 // With MMS disabled, LENGTH_REQUIRES_MMS is a no-op. 646 if (MmsConfig.DISABLE_MMS) { 647 whichState &= ~LENGTH_REQUIRES_MMS; 648 } 649 650 boolean toMms; 651 // If any bits are set in the new state and none were set in the 652 // old state, we need to convert to MMS. 653 if ((oldState == 0) && (mMessageState != 0)) { 654 toMms = true; 655 } else if ((oldState != 0) && (mMessageState == 0)) { 656 // Vice versa, to SMS. 657 toMms = false; 658 } else { 659 // If we changed state but didn't change SMS vs. MMS status, 660 // there is nothing to do. 661 return; 662 } 663 664 if (MmsConfig.DISABLE_MMS && toMms) { 665 throw new IllegalStateException( 666 "Message converted to MMS with DISABLE_MMS set"); 667 } 668 669 if (toast) { 670 toastConvertInfo(toMms); 671 } 672 convertMessage(toMms); 673 } 674 675 private class DeleteMessageListener implements OnClickListener { 676 private final Uri mDeleteUri; 677 private final boolean mDeleteAll; 678 679 public DeleteMessageListener(Uri uri, boolean all) { 680 mDeleteUri = uri; 681 mDeleteAll = all; 682 } 683 684 public DeleteMessageListener(long msgId, String type) { 685 if ("mms".equals(type)) { 686 mDeleteUri = ContentUris.withAppendedId( 687 Mms.CONTENT_URI, msgId); 688 } else { 689 mDeleteUri = ContentUris.withAppendedId( 690 Sms.CONTENT_URI, msgId); 691 } 692 mDeleteAll = false; 693 } 694 695 public void onClick(DialogInterface dialog, int whichButton) { 696 int token = mDeleteAll ? DELETE_CONVERSATION_TOKEN 697 : DELETE_MESSAGE_TOKEN; 698 mBackgroundQueryHandler.startDelete(token, 699 null, mDeleteUri, null, null); 700 } 701 } 702 703 private void discardTemporaryMessage() { 704 if (requiresMms()) { 705 if (mMessageUri != null) { 706 if (LOCAL_LOGV) Log.v(TAG, "discardTemporaryMessage " + mMessageUri); 707 asyncDelete(mMessageUri, null, null); 708 // Prevent the message from being re-saved in onStop(). 709 mMessageUri = null; 710 } 711 } else if (mThreadId > 0) { 712 asyncDeleteTemporarySmsMessage(mThreadId); 713 } 714 715 // Don't save this message as a draft, even if it is only an SMS. 716 mMsgText = ""; 717 } 718 719 private class DiscardDraftListener implements OnClickListener { 720 public void onClick(DialogInterface dialog, int whichButton) { 721 discardTemporaryMessage(); 722 goToConversationList(); 723 } 724 } 725 726 private class SendIgnoreInvalidRecipientListener implements OnClickListener { 727 public void onClick(DialogInterface dialog, int whichButton) { 728 sendMessage(); 729 } 730 } 731 732 private class CancelSendingListener implements OnClickListener { 733 public void onClick(DialogInterface dialog, int whichButton) { 734 if (isRecipientsEditorVisible()) { 735 mRecipientsEditor.requestFocus(); 736 } 737 } 738 } 739 740 private void confirmSendMessageIfNeeded() { 741 if (mRecipientList.hasInvalidRecipient()) { 742 if (mRecipientList.hasValidRecipient()) { 743 String title = getResourcesString(R.string.has_invalid_recipient, 744 mRecipientList.getInvalidRecipientString()); 745 new AlertDialog.Builder(this) 746 .setIcon(android.R.drawable.ic_dialog_alert) 747 .setTitle(title) 748 .setMessage(R.string.invalid_recipient_message) 749 .setPositiveButton(R.string.try_to_send, 750 new SendIgnoreInvalidRecipientListener()) 751 .setNegativeButton(R.string.no, new CancelSendingListener()) 752 .show(); 753 } else { 754 new AlertDialog.Builder(this) 755 .setIcon(android.R.drawable.ic_dialog_alert) 756 .setTitle(R.string.cannot_send_message) 757 .setMessage(R.string.cannot_send_message_reason) 758 .setPositiveButton(R.string.yes, new CancelSendingListener()) 759 .show(); 760 } 761 } else { 762 sendMessage(); 763 } 764 } 765 766 private final OnFocusChangeListener mRecipientsFocusListener = new OnFocusChangeListener() { 767 public void onFocusChange(View v, boolean hasFocus) { 768 if (!hasFocus) { 769 convertMessageIfNeeded(RECIPIENTS_REQUIRE_MMS, recipientsRequireMms()); 770 updateWindowTitle(); 771 startQueryForContactInfo(); 772 } 773 } 774 }; 775 776 private final TextWatcher mRecipientsWatcher = new TextWatcher() { 777 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 778 } 779 780 public void onTextChanged(CharSequence s, int start, int before, int count) { 781 // This is a workaround for bug 1609057. Since onUserInteraction() is 782 // not called when the user touches the soft keyboard, we pretend it was 783 // called when textfields changes. This should be removed when the bug 784 // is fixed. 785 onUserInteraction(); 786 } 787 788 public void afterTextChanged(Editable s) { 789 int oldValidCount = mRecipientList.size(); 790 int oldTotal = mRecipientList.countInvalidRecipients() + oldValidCount; 791 792 // Bug 1474782 describes a situation in which we send to 793 // the wrong recipient. We have been unable to reproduce this, 794 // but the best theory we have so far is that the contents of 795 // mRecipientList somehow become stale when entering 796 // ComposeMessageActivity via onNewIntent(). This assertion is 797 // meant to catch one possible path to that, of a non-visible 798 // mRecipientsEditor having its TextWatcher fire and refreshing 799 // mRecipientList with its stale contents. 800 if (!isRecipientsEditorVisible()) { 801 IllegalStateException e = new IllegalStateException( 802 "afterTextChanged called with invisible mRecipientsEditor"); 803 // Make sure the crash is uploaded to the service so we 804 // can see if this is happening in the field. 805 Log.e(TAG, "RecipientsWatcher called incorrectly", e); 806 throw e; 807 } 808 809 // Refresh our local copy of the recipient list. 810 mRecipientList = mRecipientsEditor.getRecipientList(); 811 // If we have gone to zero recipients, disable send button. 812 updateSendButtonState(); 813 814 // If a recipient has been added or deleted (or an invalid one has become valid), 815 // convert the message if necessary. This causes us to "drop" conversions when 816 // a recipient becomes invalid, but we check again upon losing focus to ensure our 817 // state doesn't get too stale. This keeps us from thrashing around between 818 // valid and invalid when typing in an email address. 819 int newValidCount = mRecipientList.size(); 820 int newTotal = mRecipientList.countInvalidRecipients() + newValidCount; 821 if ((oldTotal != newTotal) || (newValidCount > oldValidCount)) { 822 convertMessageIfNeeded(RECIPIENTS_REQUIRE_MMS, recipientsRequireMms()); 823 } 824 825 String recipients = s.toString(); 826 if (recipients.endsWith(",") || recipients.endsWith(", ")) { 827 updateWindowTitle(); 828 startQueryForContactInfo(); 829 } 830 } 831 }; 832 833 private final OnCreateContextMenuListener mRecipientsMenuCreateListener = 834 new OnCreateContextMenuListener() { 835 public void onCreateContextMenu(ContextMenu menu, View v, 836 ContextMenuInfo menuInfo) { 837 if (menuInfo != null) { 838 Recipient r = ((RecipientContextMenuInfo) menuInfo).recipient; 839 RecipientsMenuClickListener l = new RecipientsMenuClickListener(r); 840 841 String title = !TextUtils.isEmpty(r.name) ? r.name : r.number; 842 menu.setHeaderTitle(title); 843 844 long personId = getPersonId(r); 845 if (personId > 0) { 846 r.person_id = personId; // make sure it's updated with the latest. 847 menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact) 848 .setOnMenuItemClickListener(l); 849 } else { 850 menu.add(0, MENU_ADD_TO_CONTACTS, 0, R.string.menu_add_to_contacts) 851 .setOnMenuItemClickListener(l); 852 } 853 } 854 } 855 }; 856 857 private final class RecipientsMenuClickListener implements MenuItem.OnMenuItemClickListener { 858 private final Recipient mRecipient; 859 860 RecipientsMenuClickListener(Recipient recipient) { 861 mRecipient = recipient; 862 } 863 864 public boolean onMenuItemClick(MenuItem item) { 865 switch (item.getItemId()) { 866 // Context menu handlers for the recipients editor. 867 case MENU_VIEW_CONTACT: { 868 viewContact(mRecipient.person_id); 869 return true; 870 } 871 case MENU_ADD_TO_CONTACTS: { 872 Intent intent = ConversationList.createAddContactIntent(mRecipient.number); 873 ComposeMessageActivity.this.startActivity(intent); 874 return true; 875 } 876 } 877 return false; 878 } 879 } 880 881 private void viewContact(long personId) { 882 Uri uri = ContentUris.withAppendedId(People.CONTENT_URI, personId); 883 Intent intent = new Intent(Intent.ACTION_VIEW, uri); 884 startActivity(intent); 885 } 886 887 private void addPositionBasedMenuItems(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 888 AdapterView.AdapterContextMenuInfo info; 889 890 try { 891 info = (AdapterView.AdapterContextMenuInfo) menuInfo; 892 } catch (ClassCastException e) { 893 Log.e(TAG, "bad menuInfo"); 894 return; 895 } 896 final int position = info.position; 897 898 addUriSpecificMenuItems(menu, v, position); 899 } 900 901 private Uri getSelectedUriFromMessageList(ListView listView, int position) { 902 // If the context menu was opened over a uri, get that uri. 903 MessageListItem msglistItem = (MessageListItem) listView.getChildAt(position); 904 if (msglistItem == null) { 905 // FIXME: Should get the correct view. No such interface in ListView currently 906 // to get the view by position. The ListView.getChildAt(position) cannot 907 // get correct view since the list doesn't create one child for each item. 908 // And if setSelection(position) then getSelectedView(), 909 // cannot get corrent view when in touch mode. 910 return null; 911 } 912 913 TextView textView; 914 CharSequence text = null; 915 int selStart = -1; 916 int selEnd = -1; 917 918 //check if message sender is selected 919 textView = (TextView) msglistItem.findViewById(R.id.text_view); 920 if (textView != null) { 921 text = textView.getText(); 922 selStart = textView.getSelectionStart(); 923 selEnd = textView.getSelectionEnd(); 924 } 925 926 if (selStart == -1) { 927 //sender is not being selected, it may be within the message body 928 textView = (TextView) msglistItem.findViewById(R.id.body_text_view); 929 if (textView != null) { 930 text = textView.getText(); 931 selStart = textView.getSelectionStart(); 932 selEnd = textView.getSelectionEnd(); 933 } 934 } 935 936 // Check that some text is actually selected, rather than the cursor 937 // just being placed within the TextView. 938 if (selStart != selEnd) { 939 int min = Math.min(selStart, selEnd); 940 int max = Math.max(selStart, selEnd); 941 942 URLSpan[] urls = ((Spanned) text).getSpans(min, max, 943 URLSpan.class); 944 945 if (urls.length == 1) { 946 return Uri.parse(urls[0].getURL()); 947 } 948 } 949 950 //no uri was selected 951 return null; 952 } 953 954 private void addUriSpecificMenuItems(ContextMenu menu, View v, int position) { 955 Uri uri = getSelectedUriFromMessageList((ListView) v, position); 956 957 if (uri != null) { 958 Intent intent = new Intent(null, uri); 959 intent.addCategory(Intent.CATEGORY_SELECTED_ALTERNATIVE); 960 menu.addIntentOptions(0, 0, 0, 961 new android.content.ComponentName(this, ComposeMessageActivity.class), 962 null, intent, 0, null); 963 } 964 } 965 966 private final void addCallAndContactMenuItems( 967 ContextMenu menu, MsgListMenuClickListener l, MessageItem msgItem) { 968 // Add all possible links in the address & message 969 StringBuilder textToSpannify = new StringBuilder(); 970 if (msgItem.mBoxId == Mms.MESSAGE_BOX_INBOX) { 971 textToSpannify.append(msgItem.mAddress + ": "); 972 } 973 textToSpannify.append(msgItem.mBody); 974 975 SpannableString msg = new SpannableString(textToSpannify.toString()); 976 Linkify.addLinks(msg, Linkify.ALL); 977 ArrayList<String> uris = 978 MessageUtils.extractUris(msg.getSpans(0, msg.length(), URLSpan.class)); 979 980 while (uris.size() > 0) { 981 String uriString = uris.remove(0); 982 // Remove any dupes so they don't get added to the menu multiple times 983 while (uris.contains(uriString)) { 984 uris.remove(uriString); 985 } 986 987 int sep = uriString.indexOf(":"); 988 String prefix = null; 989 if (sep >= 0) { 990 prefix = uriString.substring(0, sep); 991 uriString = uriString.substring(sep + 1); 992 } 993 boolean addToContacts = false; 994 if ("mailto".equalsIgnoreCase(prefix)) { 995 String sendEmailString = getString( 996 R.string.menu_send_email).replace("%s", uriString); 997 menu.add(0, MENU_SEND_EMAIL, 0, sendEmailString) 998 .setOnMenuItemClickListener(l) 999 .setIntent(new Intent( 1000 Intent.ACTION_VIEW, 1001 Uri.parse("mailto:" + uriString))); 1002 addToContacts = !haveEmailContact(uriString); 1003 } else if ("tel".equalsIgnoreCase(prefix)) { 1004 String callBackString = getString( 1005 R.string.menu_call_back).replace("%s", uriString); 1006 menu.add(0, MENU_CALL_BACK, 0, callBackString) 1007 .setOnMenuItemClickListener(l) 1008 .setIntent(new Intent( 1009 Intent.ACTION_DIAL, 1010 Uri.parse("tel:" + uriString))); 1011 addToContacts = !isNumberInContacts(uriString); 1012 } 1013 if (addToContacts) { 1014 Intent intent = ConversationList.createAddContactIntent(uriString); 1015 String addContactString = getString( 1016 R.string.menu_add_address_to_contacts).replace("%s", uriString); 1017 menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, addContactString) 1018 .setOnMenuItemClickListener(l) 1019 .setIntent(intent); 1020 } 1021 } 1022 } 1023 1024 private boolean haveEmailContact(String emailAddress) { 1025 Cursor cursor = SqliteWrapper.query(this, getContentResolver(), 1026 Contacts.ContactMethods.CONTENT_EMAIL_URI, 1027 new String[] { Contacts.ContactMethods.NAME }, 1028 Contacts.ContactMethods.DATA + " = " + DatabaseUtils.sqlEscapeString(emailAddress), 1029 null, null); 1030 1031 if (cursor != null) { 1032 try { 1033 while (cursor.moveToNext()) { 1034 String name = cursor.getString(0); 1035 if (!TextUtils.isEmpty(name)) { 1036 return true; 1037 } 1038 } 1039 } finally { 1040 cursor.close(); 1041 } 1042 } 1043 return false; 1044 } 1045 1046 private boolean isNumberInContacts(String phoneNumber) { 1047 ContactInfoCache.CacheEntry entry = 1048 ContactInfoCache.getInstance().getContactInfo(this, phoneNumber); 1049 return !TextUtils.isEmpty(entry.name); 1050 } 1051 1052 private final OnCreateContextMenuListener mMsgListMenuCreateListener = 1053 new OnCreateContextMenuListener() { 1054 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 1055 Cursor cursor = mMsgListAdapter.getCursor(); 1056 String type = cursor.getString(COLUMN_MSG_TYPE); 1057 long msgId = cursor.getLong(COLUMN_ID); 1058 1059 addPositionBasedMenuItems(menu, v, menuInfo); 1060 1061 MessageItem msgItem = mMsgListAdapter.getCachedMessageItem(type, msgId, cursor); 1062 if (msgItem == null) { 1063 Log.e(TAG, "Cannot load message item for type = " + type 1064 + ", msgId = " + msgId); 1065 return; 1066 } 1067 1068 menu.setHeaderTitle(R.string.message_options); 1069 1070 MsgListMenuClickListener l = new MsgListMenuClickListener(); 1071 if (msgItem.isMms()) { 1072 switch (msgItem.mBoxId) { 1073 case Mms.MESSAGE_BOX_INBOX: 1074 break; 1075 case Mms.MESSAGE_BOX_OUTBOX: 1076 if (mRecipientList.size() == 1) { 1077 menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit) 1078 .setOnMenuItemClickListener(l); 1079 } 1080 break; 1081 } 1082 switch (msgItem.mAttachmentType) { 1083 case AttachmentEditor.TEXT_ONLY: 1084 break; 1085 case AttachmentEditor.VIDEO_ATTACHMENT: 1086 case AttachmentEditor.IMAGE_ATTACHMENT: 1087 if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) { 1088 menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard) 1089 .setOnMenuItemClickListener(l); 1090 } 1091 break; 1092 case AttachmentEditor.SLIDESHOW_ATTACHMENT: 1093 default: 1094 menu.add(0, MENU_VIEW_SLIDESHOW, 0, R.string.view_slideshow) 1095 .setOnMenuItemClickListener(l); 1096 if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) { 1097 menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard) 1098 .setOnMenuItemClickListener(l); 1099 } 1100 break; 1101 } 1102 } else { 1103 // Message type is sms. Only allow "edit" if the message has a single recipient 1104 if (mRecipientList.size() == 1 && 1105 (msgItem.mBoxId == Sms.MESSAGE_TYPE_OUTBOX || 1106 msgItem.mBoxId == Sms.MESSAGE_TYPE_FAILED)) { 1107 menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit) 1108 .setOnMenuItemClickListener(l); 1109 } 1110 } 1111 1112 addCallAndContactMenuItems(menu, l, msgItem); 1113 1114 // Forward is not available for undownloaded messages. 1115 if (msgItem.isDownloaded()) { 1116 menu.add(0, MENU_FORWARD_MESSAGE, 0, R.string.menu_forward) 1117 .setOnMenuItemClickListener(l); 1118 } 1119 1120 // It is unclear what would make most sense for copying an MMS message 1121 // to the clipboard, so we currently do SMS only. 1122 if (msgItem.isSms()) { 1123 menu.add(0, MENU_COPY_MESSAGE_TEXT, 0, R.string.copy_message_text) 1124 .setOnMenuItemClickListener(l); 1125 } 1126 1127 menu.add(0, MENU_VIEW_MESSAGE_DETAILS, 0, R.string.view_message_details) 1128 .setOnMenuItemClickListener(l); 1129 menu.add(0, MENU_DELETE_MESSAGE, 0, R.string.delete_message) 1130 .setOnMenuItemClickListener(l); 1131 if (msgItem.mDeliveryReport || msgItem.mReadReport) { 1132 menu.add(0, MENU_DELIVERY_REPORT, 0, R.string.view_delivery_report) 1133 .setOnMenuItemClickListener(l); 1134 } 1135 } 1136 }; 1137 1138 private void editMessageItem(MessageItem msgItem) { 1139 if ("sms".equals(msgItem.mType)) { 1140 editSmsMessageItem(msgItem); 1141 } else { 1142 editMmsMessageItem(msgItem); 1143 } 1144 if (MessageListItem.isFailedMessage(msgItem) && mMsgListAdapter.getCount() <= 1) { 1145 // For messages with bad addresses, let the user re-edit the recipients. 1146 initRecipientsEditor(); 1147 } 1148 } 1149 1150 private void editSmsMessageItem(MessageItem msgItem) { 1151 // Delete the old undelivered SMS and load its content. 1152 Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgItem.mMsgId); 1153 SqliteWrapper.delete(ComposeMessageActivity.this, 1154 mContentResolver, uri, null, null); 1155 mMsgText = msgItem.mBody; 1156 } 1157 1158 private void editMmsMessageItem(MessageItem msgItem) { 1159 if (mMessageUri != null) { 1160 // Delete the former draft. 1161 SqliteWrapper.delete(ComposeMessageActivity.this, 1162 mContentResolver, mMessageUri, null, null); 1163 } 1164 mMessageUri = msgItem.mMessageUri; 1165 ContentValues values = new ContentValues(1); 1166 values.put(Mms.MESSAGE_BOX, Mms.MESSAGE_BOX_DRAFTS); 1167 SqliteWrapper.update(ComposeMessageActivity.this, 1168 mContentResolver, mMessageUri, values, null, null); 1169 1170 updateState(RECIPIENTS_REQUIRE_MMS, recipientsRequireMms()); 1171 if (!TextUtils.isEmpty(msgItem.mSubject)) { 1172 mSubject = msgItem.mSubject; 1173 updateState(HAS_SUBJECT, true); 1174 } 1175 1176 if (msgItem.mAttachmentType > AttachmentEditor.TEXT_ONLY) { 1177 updateState(HAS_ATTACHMENT, true); 1178 } 1179 1180 convertMessage(true); 1181 if (!TextUtils.isEmpty(mSubject)) { 1182 showSubjectEditor(); 1183 } else { 1184 mSubjectTextEditor.setVisibility(View.GONE); 1185 hideTopPanelIfNecessary(); 1186 } 1187 } 1188 1189 private void copyToClipboard(String str) { 1190 ClipboardManager clip = 1191 (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); 1192 clip.setText(str); 1193 } 1194 1195 /** 1196 * Context menu handlers for the message list view. 1197 */ 1198 private final class MsgListMenuClickListener implements MenuItem.OnMenuItemClickListener { 1199 public boolean onMenuItemClick(MenuItem item) { 1200 Cursor cursor = mMsgListAdapter.getCursor(); 1201 String type = cursor.getString(COLUMN_MSG_TYPE); 1202 long msgId = cursor.getLong(COLUMN_ID); 1203 MessageItem msgItem = getMessageItem(type, msgId); 1204 1205 if (msgItem == null) { 1206 return false; 1207 } 1208 1209 switch (item.getItemId()) { 1210 case MENU_EDIT_MESSAGE: { 1211 editMessageItem(msgItem); 1212 int attachmentType = requiresMms() 1213 ? MessageUtils.getAttachmentType(mSlideshow) 1214 : AttachmentEditor.TEXT_ONLY; 1215 drawBottomPanel(attachmentType); 1216 return true; 1217 } 1218 case MENU_COPY_MESSAGE_TEXT: { 1219 copyToClipboard(msgItem.mBody); 1220 return true; 1221 } 1222 case MENU_FORWARD_MESSAGE: { 1223 Intent intent = new Intent(ComposeMessageActivity.this, 1224 ComposeMessageActivity.class); 1225 1226 intent.putExtra("exit_on_sent", true); 1227 intent.putExtra("forwarded_message", true); 1228 if (type.equals("sms")) { 1229 intent.putExtra("sms_body", msgItem.mBody); 1230 } else { 1231 SendReq sendReq = new SendReq(); 1232 String subject = getString(R.string.forward_prefix); 1233 if (msgItem.mSubject != null) { 1234 subject += msgItem.mSubject; 1235 } 1236 sendReq.setSubject(new EncodedStringValue(subject)); 1237 sendReq.setBody(msgItem.mSlideshow.makeCopy( 1238 ComposeMessageActivity.this)); 1239 1240 Uri uri = null; 1241 try { 1242 // Implicitly copy the parts of the message here. 1243 uri = mPersister.persist(sendReq, Mms.Draft.CONTENT_URI); 1244 } catch (MmsException e) { 1245 Log.e(TAG, "Failed to copy message: " + msgItem.mMessageUri, e); 1246 Toast.makeText(ComposeMessageActivity.this, 1247 R.string.cannot_save_message, Toast.LENGTH_SHORT).show(); 1248 return true; 1249 } 1250 1251 intent.putExtra("msg_uri", uri); 1252 intent.putExtra("subject", subject); 1253 } 1254 startActivityIfNeeded(intent, -1); 1255 return true; 1256 } 1257 case MENU_VIEW_SLIDESHOW: { 1258 MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this, 1259 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId), 1260 null /* slideshow */, null /* persister */); 1261 return true; 1262 } 1263 case MENU_VIEW_MESSAGE_DETAILS: { 1264 String messageDetails = MessageUtils.getMessageDetails( 1265 ComposeMessageActivity.this, cursor, msgItem.mMessageSize); 1266 new AlertDialog.Builder(ComposeMessageActivity.this) 1267 .setTitle(R.string.message_details_title) 1268 .setMessage(messageDetails) 1269 .setPositiveButton(android.R.string.ok, null) 1270 .setCancelable(true) 1271 .show(); 1272 return true; 1273 } 1274 case MENU_DELETE_MESSAGE: { 1275 DeleteMessageListener l = new DeleteMessageListener( 1276 msgItem.mMessageUri, false); 1277 confirmDeleteDialog(l, false); 1278 return true; 1279 } 1280 case MENU_DELIVERY_REPORT: 1281 showDeliveryReport(msgId, type); 1282 return true; 1283 1284 case MENU_COPY_TO_SDCARD: { 1285 int resId = copyMedia(msgId) ? R.string.copy_to_sdcard_success : 1286 R.string.copy_to_sdcard_fail; 1287 Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show(); 1288 return true; 1289 } 1290 1291 default: 1292 return false; 1293 } 1294 } 1295 } 1296 1297 /** 1298 * Looks to see if there are any valid parts of the attachment that can be copied to a SD card. 1299 * @param msgId 1300 */ 1301 private boolean haveSomethingToCopyToSDCard(long msgId) { 1302 PduBody body; 1303 try { 1304 body = SlideshowModel.getPduBody(this, 1305 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId)); 1306 } catch (MmsException e) { 1307 Log.e(TAG, e.getMessage(), e); 1308 return false; 1309 } 1310 1311 boolean result = false; 1312 int partNum = body.getPartsNum(); 1313 for(int i = 0; i < partNum; i++) { 1314 PduPart part = body.getPart(i); 1315 String type = new String(part.getContentType()); 1316 1317 if ((ContentType.isImageType(type) || ContentType.isVideoType(type) || 1318 ContentType.isAudioType(type))) { 1319 result = true; 1320 break; 1321 } 1322 } 1323 return result; 1324 } 1325 1326 /** 1327 * Copies media from an Mms to the "download" directory on the SD card 1328 * @param msgId 1329 */ 1330 private boolean copyMedia(long msgId) { 1331 PduBody body; 1332 boolean result = true; 1333 try { 1334 body = SlideshowModel.getPduBody(this, ContentUris.withAppendedId(Mms.CONTENT_URI, msgId)); 1335 } catch (MmsException e) { 1336 Log.e(TAG, e.getMessage(), e); 1337 return false; 1338 } 1339 1340 int partNum = body.getPartsNum(); 1341 for(int i = 0; i < partNum; i++) { 1342 PduPart part = body.getPart(i); 1343 String type = new String(part.getContentType()); 1344 1345 if ((ContentType.isImageType(type) || ContentType.isVideoType(type) || 1346 ContentType.isAudioType(type))) { 1347 result &= copyPart(part); // all parts have to be successful for a valid result. 1348 } 1349 } 1350 return result; 1351 } 1352 1353 private boolean copyPart(PduPart part) { 1354 Uri uri = part.getDataUri(); 1355 1356 InputStream input = null; 1357 FileOutputStream fout = null; 1358 try { 1359 input = mContentResolver.openInputStream(uri); 1360 if (input instanceof FileInputStream) { 1361 FileInputStream fin = (FileInputStream) input; 1362 1363 byte[] location = part.getName(); 1364 if (location == null) { 1365 location = part.getFilename(); 1366 } 1367 if (location == null) { 1368 location = part.getContentLocation(); 1369 } 1370 1371 // Depending on the location, there may be an 1372 // extension already on the name or not 1373 String fileName = new String(location); 1374 String dir = "/sdcard/download/"; 1375 String extension; 1376 int index; 1377 if ((index = fileName.indexOf(".")) == -1) { 1378 String type = new String(part.getContentType()); 1379 extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type); 1380 } else { 1381 extension = fileName.substring(index + 1, fileName.length()); 1382 fileName = fileName.substring(0, index); 1383 } 1384 1385 File file = getUniqueDestination(dir + fileName, extension); 1386 1387 // make sure the path is valid and directories created for this file. 1388 File parentFile = file.getParentFile(); 1389 if (!parentFile.exists() && !parentFile.mkdirs()) { 1390 Log.e(TAG, "[MMS] copyPart: mkdirs for " + parentFile.getPath() + " failed!"); 1391 return false; 1392 } 1393 1394 fout = new FileOutputStream(file); 1395 1396 byte[] buffer = new byte[8000]; 1397 while(fin.read(buffer) != -1) { 1398 fout.write(buffer); 1399 } 1400 1401 // Notify other applications listening to scanner events 1402 // that a media file has been added to the sd card 1403 sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, 1404 Uri.fromFile(file))); 1405 } 1406 } catch (IOException e) { 1407 // Ignore 1408 Log.e(TAG, "IOException caught while opening or reading stream", e); 1409 return false; 1410 } finally { 1411 if (null != input) { 1412 try { 1413 input.close(); 1414 } catch (IOException e) { 1415 // Ignore 1416 Log.e(TAG, "IOException caught while closing stream", e); 1417 return false; 1418 } 1419 } 1420 if (null != fout) { 1421 try { 1422 fout.close(); 1423 } catch (IOException e) { 1424 // Ignore 1425 Log.e(TAG, "IOException caught while closing stream", e); 1426 return false; 1427 } 1428 } 1429 } 1430 return true; 1431 } 1432 1433 private File getUniqueDestination(String base, String extension) { 1434 File file = new File(base + "." + extension); 1435 1436 for (int i = 2; file.exists(); i++) { 1437 file = new File(base + "_" + i + "." + extension); 1438 } 1439 return file; 1440 } 1441 1442 private void showDeliveryReport(long messageId, String type) { 1443 Intent intent = new Intent(this, DeliveryReportActivity.class); 1444 intent.putExtra("message_id", messageId); 1445 intent.putExtra("message_type", type); 1446 1447 startActivity(intent); 1448 } 1449 1450 private final IntentFilter mHttpProgressFilter = new IntentFilter(PROGRESS_STATUS_ACTION); 1451 1452 private final BroadcastReceiver mHttpProgressReceiver = new BroadcastReceiver() { 1453 @Override 1454 public void onReceive(Context context, Intent intent) { 1455 if (PROGRESS_STATUS_ACTION.equals(intent.getAction())) { 1456 long token = intent.getLongExtra("token", 1457 SendingProgressTokenManager.NO_TOKEN); 1458 if (token != mThreadId) { 1459 return; 1460 } 1461 1462 int progress = intent.getIntExtra("progress", 0); 1463 switch (progress) { 1464 case PROGRESS_START: 1465 setProgressBarVisibility(true); 1466 break; 1467 case PROGRESS_ABORT: 1468 case PROGRESS_COMPLETE: 1469 setProgressBarVisibility(false); 1470 break; 1471 default: 1472 setProgress(100 * progress); 1473 } 1474 } 1475 } 1476 }; 1477 1478 //========================================================== 1479 // Static methods 1480 //========================================================== 1481 1482 private static SlideshowModel createNewSlideshow(Context context) { 1483 SlideshowModel slideshow = SlideshowModel.createNew(context); 1484 SlideModel slide = new SlideModel(slideshow); 1485 1486 TextModel text = new TextModel( 1487 context, ContentType.TEXT_PLAIN, "text_0.txt", 1488 slideshow.getLayout().getTextRegion()); 1489 slide.add(text); 1490 1491 slideshow.add(slide); 1492 return slideshow; 1493 } 1494 1495 private static EncodedStringValue[] encodeStrings(String[] array) { 1496 int count = array.length; 1497 if (count > 0) { 1498 EncodedStringValue[] encodedArray = new EncodedStringValue[count]; 1499 for (int i = 0; i < count; i++) { 1500 encodedArray[i] = new EncodedStringValue(array[i]); 1501 } 1502 return encodedArray; 1503 } 1504 return null; 1505 } 1506 1507 // Get the recipients editor ready to be displayed onscreen. 1508 private void initRecipientsEditor() { 1509 if (isRecipientsEditorVisible()) { 1510 return; 1511 } 1512 ViewStub stub = (ViewStub)findViewById(R.id.recipients_editor_stub); 1513 if (stub != null) { 1514 mRecipientsEditor = (RecipientsEditor) stub.inflate(); 1515 } else { 1516 mRecipientsEditor = (RecipientsEditor)findViewById(R.id.recipients_editor); 1517 mRecipientsEditor.setVisibility(View.VISIBLE); 1518 } 1519 1520 mRecipientsEditor.setAdapter(new RecipientsAdapter(this)); 1521 mRecipientsEditor.populate(mRecipientList); 1522 mRecipientsEditor.setOnCreateContextMenuListener(mRecipientsMenuCreateListener); 1523 mRecipientsEditor.addTextChangedListener(mRecipientsWatcher); 1524 mRecipientsEditor.setOnFocusChangeListener(mRecipientsFocusListener); 1525 mRecipientsEditor.setFilters(new InputFilter[] { 1526 new InputFilter.LengthFilter(RECIPIENTS_MAX_LENGTH) }); 1527 mRecipientsEditor.setOnItemClickListener(new AdapterView.OnItemClickListener() { 1528 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1529 // After the user selects an item in the pop-up contacts list, move the 1530 // focus to the text editor if there is only one recipient. This helps 1531 // the common case of selecting one recipient and then typing a message, 1532 // but avoids annoying a user who is trying to add five recipients and 1533 // keeps having focus stolen away. 1534 if (mRecipientList.size() == 1) { 1535 // if we're in extract mode then don't request focus 1536 final InputMethodManager inputManager = (InputMethodManager) 1537 getSystemService(Context.INPUT_METHOD_SERVICE); 1538 if (inputManager == null || !inputManager.isFullscreenMode()) { 1539 mTextEditor.requestFocus(); 1540 } 1541 } 1542 } 1543 }); 1544 1545 mTopPanel.setVisibility(View.VISIBLE); 1546 } 1547 1548 //========================================================== 1549 // Activity methods 1550 //========================================================== 1551 1552 private void setPresenceIcon(int iconId) { 1553 Drawable icon = iconId == 0 ? null : this.getResources().getDrawable(iconId); 1554 getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, icon); 1555 } 1556 1557 static public boolean cancelFailedToDeliverNotification(Intent intent, Context context) { 1558 if (ConversationList.isFailedToDeliver(intent)) { 1559 // Cancel any failed message notifications 1560 MessagingNotification.cancelNotification(context, 1561 MessagingNotification.MESSAGE_FAILED_NOTIFICATION_ID); 1562 return true; 1563 } 1564 return false; 1565 } 1566 1567 @Override 1568 protected void onCreate(Bundle savedInstanceState) { 1569 super.onCreate(savedInstanceState); 1570 requestWindowFeature(Window.FEATURE_PROGRESS); 1571 requestWindowFeature(Window.FEATURE_LEFT_ICON); 1572 1573 setContentView(R.layout.compose_message_activity); 1574 setProgressBarVisibility(false); 1575 1576 setTitle(""); 1577 1578 // Initialize members for UI elements. 1579 initResourceRefs(); 1580 1581 mContentResolver = getContentResolver(); 1582 mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver); 1583 mPersister = PduPersister.getPduPersister(this); 1584 1585 // Read parameters or previously saved state of this activity. 1586 initActivityState(savedInstanceState, getIntent()); 1587 1588 if (LOCAL_LOGV) { 1589 Log.v(TAG, "onCreate(): savedInstanceState = " + savedInstanceState); 1590 Log.v(TAG, "onCreate(): intent = " + getIntent()); 1591 Log.v(TAG, "onCreate(): mThreadId = " + mThreadId); 1592 Log.v(TAG, "onCreate(): mMessageUri = " + mMessageUri); 1593 } 1594 1595 // Parse the recipient list. 1596 mRecipientList = RecipientList.from(mExternalAddress, this); 1597 1598 if (cancelFailedToDeliverNotification(getIntent(), getApplicationContext())) { 1599 // Show a pop-up dialog to inform user the message was 1600 // failed to deliver. 1601 undeliveredMessageDialog(getMessageDate(mMessageUri)); 1602 } 1603 1604 // Set up the message history ListAdapter 1605 initMessageList(); 1606 1607 // Mark the current thread as read. 1608 markAsRead(mThreadId); 1609 1610 // Load the draft for this thread, if we aren't already handling 1611 // existing data, such as a shared picture or forwarded message. 1612 if (!handleSendIntent(getIntent()) && !handleForwardedMessage()) { 1613 loadDraft(); 1614 } 1615 1616 // If we are still not in MMS mode, check to see if we need to convert 1617 // because of e-mail recipients. 1618 convertMessageIfNeeded(RECIPIENTS_REQUIRE_MMS, recipientsRequireMms(), false); 1619 1620 // Show the recipients editor if we don't have a valid thread. 1621 if (mThreadId <= 0) { 1622 initRecipientsEditor(); 1623 } 1624 1625 int attachmentType = requiresMms() 1626 ? MessageUtils.getAttachmentType(mSlideshow) 1627 : AttachmentEditor.TEXT_ONLY; 1628 1629 updateSendButtonState(); 1630 1631 drawBottomPanel(attachmentType); 1632 1633 mTopPanel.setFocusable(false); 1634 1635 Configuration config = getResources().getConfiguration(); 1636 mIsKeyboardOpen = config.keyboardHidden == KEYBOARDHIDDEN_NO; 1637 mIsLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE; 1638 onKeyboardStateChanged(mIsKeyboardOpen); 1639 1640 if (TRACE) { 1641 android.os.Debug.startMethodTracing("compose"); 1642 } 1643 } 1644 1645 private void showSubjectEditor() { 1646 mSubjectTextEditor.setVisibility(View.VISIBLE); 1647 mTopPanel.setVisibility(View.VISIBLE); 1648 } 1649 1650 private void hideTopPanelIfNecessary() { 1651 if (!isSubjectEditorVisible() && !isRecipientsEditorVisible()) { 1652 mTopPanel.setVisibility(View.GONE); 1653 } 1654 } 1655 1656 @Override 1657 protected void onRestart() { 1658 super.onRestart(); 1659 1660 markAsRead(mThreadId); 1661 1662 // If the user added a contact from a recipient, we've got to make sure we invalidate 1663 // our local contact cache so we'll go out and refresh that particular contact and 1664 // get the real person_id and other info. 1665 invalidateRecipientsInCache(); 1666 } 1667 1668 @Override 1669 protected void onStart() { 1670 super.onStart(); 1671 1672 updateWindowTitle(); 1673 initFocus(); 1674 1675 // Register a BroadcastReceiver to listen on HTTP I/O process. 1676 registerReceiver(mHttpProgressReceiver, mHttpProgressFilter); 1677 1678 startMsgListQuery(); 1679 startQueryForContactInfo(); 1680 updateSendFailedNotification(); 1681 } 1682 1683 @Override 1684 protected void onResume() { 1685 super.onResume(); 1686 startPresencePollingRequest(); 1687 } 1688 1689 private void updateSendFailedNotification() { 1690 // updateSendFailedNotificationForThread makes a database call, so do the work off 1691 // of the ui thread. 1692 new Thread(new Runnable() { 1693 public void run() { 1694 MessagingNotification.updateSendFailedNotificationForThread( 1695 ComposeMessageActivity.this, mThreadId); 1696 } 1697 }).run(); 1698 } 1699 1700 @Override 1701 public void onSaveInstanceState(Bundle outState) { 1702 super.onSaveInstanceState(outState); 1703 1704 if (mThreadId > 0L) { 1705 if (LOCAL_LOGV) { 1706 Log.v(TAG, "ONFREEZE: thread_id: " + mThreadId); 1707 } 1708 outState.putLong("thread_id", mThreadId); 1709 } 1710 1711 if (LOCAL_LOGV) { 1712 Log.v(TAG, "ONFREEZE: address: " + mRecipientList.serialize()); 1713 } 1714 outState.putString("address", mRecipientList.serialize()); 1715 1716 if (needSaveAsMms()) { 1717 if (isSubjectEditorVisible()) { 1718 outState.putString("subject", mSubjectTextEditor.getText().toString()); 1719 } 1720 1721 if (mMessageUri != null) { 1722 if (LOCAL_LOGV) { 1723 Log.v(TAG, "ONFREEZE: mMessageUri: " + mMessageUri); 1724 } 1725 outState.putParcelable("msg_uri", mMessageUri); 1726 } 1727 } else { 1728 outState.putString("sms_body", mMsgText.toString()); 1729 } 1730 1731 if (mExitOnSent) { 1732 outState.putBoolean("exit_on_sent", mExitOnSent); 1733 } 1734 } 1735 1736 private boolean isEmptyMessage() { 1737 if (requiresMms()) { 1738 return isEmptyMms(); 1739 } 1740 return isEmptySms(); 1741 } 1742 1743 private boolean isEmptySms() { 1744 return TextUtils.isEmpty(mMsgText); 1745 } 1746 1747 private boolean isEmptyMms() { 1748 return !(hasText() || hasSubject() || hasAttachment()); 1749 } 1750 1751 private boolean needSaveAsMms() { 1752 // subject editor is visible without any contents. 1753 if ( (mMessageState == HAS_SUBJECT) && !hasSubject()) { 1754 convertMessage(false); 1755 return false; 1756 } 1757 return requiresMms(); 1758 } 1759 1760 @Override 1761 protected void onPause() { 1762 super.onPause(); 1763 cancelPresencePollingRequests(); 1764 } 1765 1766 @Override 1767 protected void onStop() { 1768 super.onStop(); 1769 1770 if (mMsgListAdapter != null) { 1771 mMsgListAdapter.changeCursor(null); 1772 } 1773 1774 saveDraft(); 1775 1776 // Cleanup the BroadcastReceiver. 1777 unregisterReceiver(mHttpProgressReceiver); 1778 1779 cleanupContactInfoCursor(); 1780 } 1781 1782 @Override 1783 protected void onDestroy() { 1784 if (TRACE) { 1785 android.os.Debug.stopMethodTracing(); 1786 } 1787 1788 super.onDestroy(); 1789 } 1790 1791 @Override 1792 public void onConfigurationChanged(Configuration newConfig) { 1793 super.onConfigurationChanged(newConfig); 1794 if (LOCAL_LOGV) { 1795 Log.v(TAG, "onConfigurationChanged: " + newConfig); 1796 } 1797 1798 mIsKeyboardOpen = newConfig.keyboardHidden == KEYBOARDHIDDEN_NO; 1799 mIsLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE; 1800 onKeyboardStateChanged(mIsKeyboardOpen); 1801 } 1802 1803 private void onKeyboardStateChanged(boolean isKeyboardOpen) { 1804 // If the keyboard is hidden, don't show focus highlights for 1805 // things that cannot receive input. 1806 if (isKeyboardOpen) { 1807 if (mRecipientsEditor != null) { 1808 mRecipientsEditor.setFocusableInTouchMode(true); 1809 } 1810 if (mSubjectTextEditor != null) { 1811 mSubjectTextEditor.setFocusableInTouchMode(true); 1812 } 1813 mTextEditor.setFocusableInTouchMode(true); 1814 mTextEditor.setHint(R.string.type_to_compose_text_enter_to_send); 1815 initFocus(); 1816 } else { 1817 if (mRecipientsEditor != null) { 1818 mRecipientsEditor.setFocusable(false); 1819 } 1820 if (mSubjectTextEditor != null) { 1821 mSubjectTextEditor.setFocusable(false); 1822 } 1823 mTextEditor.setFocusable(false); 1824 mTextEditor.setHint(R.string.open_keyboard_to_compose_message); 1825 } 1826 } 1827 1828 @Override 1829 public void onUserInteraction() { 1830 checkPendingNotification(); 1831 } 1832 1833 @Override 1834 public void onWindowFocusChanged(boolean hasFocus) { 1835 if (hasFocus) { 1836 checkPendingNotification(); 1837 } 1838 } 1839 1840 @Override 1841 public boolean onKeyDown(int keyCode, KeyEvent event) { 1842 switch (keyCode) { 1843 case KeyEvent.KEYCODE_DEL: 1844 if ((mMsgListAdapter != null) && mMsgListView.isFocused()) { 1845 Cursor cursor; 1846 try { 1847 cursor = (Cursor) mMsgListView.getSelectedItem(); 1848 } catch (ClassCastException e) { 1849 Log.e(TAG, "Unexpected ClassCastException.", e); 1850 return super.onKeyDown(keyCode, event); 1851 } 1852 1853 if (cursor != null) { 1854 DeleteMessageListener l = new DeleteMessageListener( 1855 cursor.getLong(COLUMN_ID), 1856 cursor.getString(COLUMN_MSG_TYPE)); 1857 confirmDeleteDialog(l, false); 1858 return true; 1859 } 1860 } 1861 break; 1862 case KeyEvent.KEYCODE_DPAD_CENTER: 1863 case KeyEvent.KEYCODE_ENTER: 1864 if (isPreparedForSending()) { 1865 confirmSendMessageIfNeeded(); 1866 return true; 1867 } 1868 break; 1869 case KeyEvent.KEYCODE_BACK: 1870 exitComposeMessageActivity(new Runnable() { 1871 public void run() { 1872 finish(); 1873 } 1874 }); 1875 return true; 1876 } 1877 1878 return super.onKeyDown(keyCode, event); 1879 } 1880 1881 private void exitComposeMessageActivity(final Runnable exit) { 1882 // If the message is empty, just quit -- finishing the 1883 // activity will cause an empty draft to be deleted. 1884 if (isEmptyMessage()) { 1885 exit.run(); 1886 return; 1887 } 1888 1889 if (!hasValidRecipient()) { 1890 MessageUtils.showDiscardDraftConfirmDialog(this, 1891 new DiscardDraftListener()); 1892 return; 1893 } 1894 1895 mToastForDraftSave = true; 1896 exit.run(); 1897 } 1898 1899 private void goToConversationList() { 1900 finish(); 1901 startActivity(new Intent(this, ConversationList.class)); 1902 } 1903 1904 private boolean isRecipientsEditorVisible() { 1905 return (null != mRecipientsEditor) 1906 && (View.VISIBLE == mRecipientsEditor.getVisibility()); 1907 } 1908 1909 private boolean isSubjectEditorVisible() { 1910 return (null != mSubjectTextEditor) 1911 && (View.VISIBLE == mSubjectTextEditor.getVisibility()); 1912 } 1913 1914 public void onAttachmentChanged(int newType, int oldType) { 1915 drawBottomPanel(newType); 1916 if (newType > AttachmentEditor.TEXT_ONLY) { 1917 updateState(HAS_ATTACHMENT, true); 1918 } else { 1919 convertMessageIfNeeded(HAS_ATTACHMENT, false); 1920 } 1921 updateSendButtonState(); 1922 } 1923 1924 // We don't want to show the "call" option unless there is only one 1925 // recipient and it's a phone number. 1926 private boolean isRecipientCallable() { 1927 return (mRecipientList.size() == 1 && !mRecipientList.containsEmail()); 1928 } 1929 1930 private void dialRecipient() { 1931 String number = mRecipientList.getSingleRecipientNumber(); 1932 Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + number)); 1933 startActivity(dialIntent); 1934 } 1935 1936 @Override 1937 public boolean onPrepareOptionsMenu(Menu menu) { 1938 menu.clear(); 1939 1940 if (isRecipientCallable()) { 1941 menu.add(0, MENU_CALL_RECIPIENT, 0, R.string.menu_call).setIcon( 1942 com.android.internal.R.drawable.ic_menu_call); 1943 } 1944 1945 // Only add the "View contact" menu item when there's a single recipient and that 1946 // recipient is someone in contacts. 1947 long personId = getPersonId(mRecipientList.getSingleRecipient()); 1948 if (personId > 0) { 1949 menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact).setIcon( 1950 R.drawable.ic_menu_contact); 1951 } 1952 1953 if (!MmsConfig.DISABLE_MMS) { 1954 if (!isSubjectEditorVisible()) { 1955 menu.add(0, MENU_ADD_SUBJECT, 0, R.string.add_subject).setIcon( 1956 com.android.internal.R.drawable.ic_menu_edit); 1957 } 1958 1959 if ((mAttachmentEditor == null) || (mAttachmentEditor.getAttachmentType() == AttachmentEditor.TEXT_ONLY)) { 1960 menu.add(0, MENU_ADD_ATTACHMENT, 0, R.string.add_attachment).setIcon( 1961 R.drawable.ic_menu_attachment); 1962 } 1963 } 1964 1965 if (isPreparedForSending()) { 1966 menu.add(0, MENU_SEND, 0, R.string.send).setIcon(android.R.drawable.ic_menu_send); 1967 } 1968 1969 menu.add(0, MENU_INSERT_SMILEY, 0, R.string.menu_insert_smiley).setIcon( 1970 com.android.internal.R.drawable.ic_menu_emoticons); 1971 1972 if (mMsgListAdapter.getCount() > 0) { 1973 // Removed search as part of b/1205708 1974 //menu.add(0, MENU_SEARCH, 0, R.string.menu_search).setIcon( 1975 // R.drawable.ic_menu_search); 1976 Cursor cursor = mMsgListAdapter.getCursor(); 1977 if ((null != cursor) && (cursor.getCount() > 0)) { 1978 menu.add(0, MENU_DELETE_THREAD, 0, R.string.delete_thread).setIcon( 1979 android.R.drawable.ic_menu_delete); 1980 } 1981 } else { 1982 menu.add(0, MENU_DISCARD, 0, R.string.discard).setIcon(android.R.drawable.ic_menu_delete); 1983 } 1984 1985 menu.add(0, MENU_CONVERSATION_LIST, 0, R.string.all_threads).setIcon( 1986 com.android.internal.R.drawable.ic_menu_friendslist); 1987 1988 buildAddAddressToContactMenuItem(menu); 1989 return true; 1990 } 1991 1992 private void buildAddAddressToContactMenuItem(Menu menu) { 1993 if (mRecipientList.hasValidRecipient()) { 1994 // Look for the first recipient we don't have a contact for and create a menu item to 1995 // add the number to contacts. 1996 Iterator<Recipient> recipientIterator = mRecipientList.iterator(); 1997 while (recipientIterator.hasNext()) { 1998 Recipient r = recipientIterator.next(); 1999 long personId = getPersonId(r); 2000 2001 if (personId <= 0) { 2002 Intent intent = ConversationList.createAddContactIntent(r.number); 2003 menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, R.string.menu_add_to_contacts) 2004 .setIcon(android.R.drawable.ic_menu_add) 2005 .setIntent(intent); 2006 break; 2007 } 2008 } 2009 } 2010 } 2011 2012 private void invalidateRecipientsInCache() { 2013 ContactInfoCache cache = ContactInfoCache.getInstance(); 2014 Iterator<Recipient> recipientIterator = mRecipientList.iterator(); 2015 while (recipientIterator.hasNext()) { 2016 Recipient r = recipientIterator.next(); 2017 cache.invalidateContact(r.number); 2018 } 2019 } 2020 2021 private long getPersonId(Recipient r) { 2022 // The recipient doesn't always have a person_id. This can happen when a user adds 2023 // a contact in the middle of an activity after the recipient has already been loaded. 2024 if (r == null) { 2025 return -1; 2026 } 2027 if (r.person_id > 0) { 2028 return r.person_id; 2029 } 2030 ContactInfoCache.CacheEntry entry = ContactInfoCache.getInstance() 2031 .getContactInfo(this, r.number); 2032 if (entry.person_id > 0) { 2033 return entry.person_id; 2034 } 2035 return -1; 2036 } 2037 2038 @Override 2039 public boolean onOptionsItemSelected(MenuItem item) { 2040 switch (item.getItemId()) { 2041 case MENU_ADD_SUBJECT: 2042 convertMessageIfNeeded(HAS_SUBJECT, true); 2043 showSubjectEditor(); 2044 mSubjectTextEditor.requestFocus(); 2045 break; 2046 case MENU_ADD_ATTACHMENT: 2047 // Launch the add-attachment list dialog 2048 showAddAttachmentDialog(); 2049 break; 2050 case MENU_DISCARD: 2051 discardTemporaryMessage(); 2052 finish(); 2053 break; 2054 case MENU_SEND: 2055 if (isPreparedForSending()) { 2056 confirmSendMessageIfNeeded(); 2057 } 2058 break; 2059 case MENU_SEARCH: 2060 onSearchRequested(); 2061 break; 2062 case MENU_DELETE_THREAD: 2063 DeleteMessageListener l = new DeleteMessageListener( 2064 getThreadUri(), true); 2065 confirmDeleteDialog(l, true); 2066 break; 2067 case MENU_CONVERSATION_LIST: 2068 exitComposeMessageActivity(new Runnable() { 2069 public void run() { 2070 goToConversationList(); 2071 } 2072 }); 2073 break; 2074 case MENU_CALL_RECIPIENT: 2075 dialRecipient(); 2076 break; 2077 case MENU_INSERT_SMILEY: 2078 showSmileyDialog(); 2079 break; 2080 case MENU_VIEW_CONTACT: 2081 // View the contact for the first (and only) recipient. 2082 long personId = getPersonId(mRecipientList.getSingleRecipient()); 2083 if (personId > 0) { 2084 viewContact(personId); 2085 } 2086 break; 2087 case MENU_ADD_ADDRESS_TO_CONTACTS: 2088 return false; // so the intent attached to the menu item will get launched. 2089 } 2090 2091 return true; 2092 } 2093 2094 private void addAttachment(int type) { 2095 switch (type) { 2096 case AttachmentTypeSelectorAdapter.ADD_IMAGE: 2097 MessageUtils.selectImage(this, REQUEST_CODE_ATTACH_IMAGE); 2098 break; 2099 2100 case AttachmentTypeSelectorAdapter.TAKE_PICTURE: { 2101 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 2102 startActivityForResult(intent, REQUEST_CODE_TAKE_PICTURE); 2103 } 2104 break; 2105 2106 case AttachmentTypeSelectorAdapter.ADD_VIDEO: 2107 MessageUtils.selectVideo(this, REQUEST_CODE_ATTACH_VIDEO); 2108 break; 2109 2110 case AttachmentTypeSelectorAdapter.RECORD_VIDEO: { 2111 Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); 2112 intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); 2113 startActivityForResult(intent, REQUEST_CODE_TAKE_VIDEO); 2114 } 2115 break; 2116 2117 case AttachmentTypeSelectorAdapter.ADD_SOUND: 2118 MessageUtils.selectAudio(this, REQUEST_CODE_ATTACH_SOUND); 2119 break; 2120 2121 case AttachmentTypeSelectorAdapter.RECORD_SOUND: 2122 MessageUtils.recordSound(this, REQUEST_CODE_RECORD_SOUND); 2123 break; 2124 2125 case AttachmentTypeSelectorAdapter.ADD_SLIDESHOW: { 2126 boolean wasSms = !requiresMms(); 2127 2128 // SlideshowEditActivity needs mMessageUri to work with. 2129 convertMessageIfNeeded(HAS_ATTACHMENT, true); 2130 2131 if (wasSms) { 2132 // If we are converting from SMS, make sure the SMS 2133 // text message gets imported into the first slide. 2134 TextModel text = mSlideshow.get(0).getText(); 2135 if (text != null) { 2136 text.setText(mMsgText); 2137 } 2138 } 2139 2140 Intent intent = new Intent(this, SlideshowEditActivity.class); 2141 intent.setData(mMessageUri); 2142 startActivityForResult(intent, REQUEST_CODE_CREATE_SLIDESHOW); 2143 } 2144 break; 2145 2146 default: 2147 break; 2148 } 2149 } 2150 2151 private void showAddAttachmentDialog() { 2152 AlertDialog.Builder builder = new AlertDialog.Builder(this); 2153 builder.setIcon(R.drawable.ic_dialog_attach); 2154 builder.setTitle(R.string.add_attachment); 2155 2156 AttachmentTypeSelectorAdapter adapter = new AttachmentTypeSelectorAdapter( 2157 this, AttachmentTypeSelectorAdapter.MODE_WITH_SLIDESHOW); 2158 2159 builder.setAdapter(adapter, new DialogInterface.OnClickListener() { 2160 public void onClick(DialogInterface dialog, int which) { 2161 addAttachment(which); 2162 } 2163 }); 2164 2165 builder.show(); 2166 } 2167 2168 @Override 2169 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 2170 if (LOCAL_LOGV) { 2171 Log.v(TAG, "onActivityResult: requestCode=" + requestCode 2172 + ", resultCode=" + resultCode + ", data=" + data); 2173 } 2174 mWaitingForSubActivity = false; // We're back! 2175 2176 if (resultCode != RESULT_OK) { 2177 // Make sure if there was an error that our message 2178 // type remains correct. 2179 convertMessageIfNeeded(HAS_ATTACHMENT, hasAttachment()); 2180 return; 2181 } 2182 2183 if (!requiresMms()) { 2184 convertMessage(true); 2185 } 2186 2187 switch(requestCode) { 2188 case REQUEST_CODE_CREATE_SLIDESHOW: 2189 try { 2190 // Refresh the slideshow model since it may be changed 2191 // by the slideshow editor. 2192 mSlideshow = SlideshowModel.createFromMessageUri(this, mMessageUri); 2193 } catch (MmsException e) { 2194 Log.e(TAG, "Failed to load slideshow from " + mMessageUri); 2195 Toast.makeText(this, getString(R.string.cannot_load_message), 2196 Toast.LENGTH_SHORT).show(); 2197 return; 2198 } 2199 2200 // Find the most suitable type for the attachment. 2201 int attachmentType = MessageUtils.getAttachmentType(mSlideshow); 2202 switch (attachmentType) { 2203 case AttachmentEditor.EMPTY: 2204 fixEmptySlideshow(mSlideshow); 2205 attachmentType = AttachmentEditor.TEXT_ONLY; 2206 // fall-through 2207 case AttachmentEditor.TEXT_ONLY: 2208 mAttachmentEditor.setAttachment(mSlideshow, attachmentType); 2209 convertMessageIfNeeded(HAS_ATTACHMENT, false); 2210 drawBottomPanel(attachmentType); 2211 return; 2212 default: 2213 mAttachmentEditor.setAttachment(mSlideshow, attachmentType); 2214 break; 2215 } 2216 2217 drawBottomPanel(attachmentType); 2218 break; 2219 2220 case REQUEST_CODE_TAKE_PICTURE: 2221 Bitmap bitmap = (Bitmap) data.getParcelableExtra("data"); 2222 2223 if (bitmap == null) { 2224 Toast.makeText(this, 2225 getResourcesString(R.string.failed_to_add_media, getPictureString()), 2226 Toast.LENGTH_SHORT).show(); 2227 return; 2228 } 2229 addImage(bitmap); 2230 break; 2231 2232 case REQUEST_CODE_ATTACH_IMAGE: 2233 addImage(data.getData()); 2234 break; 2235 2236 case REQUEST_CODE_TAKE_VIDEO: 2237 case REQUEST_CODE_ATTACH_VIDEO: 2238 addVideo(data.getData()); 2239 break; 2240 2241 case REQUEST_CODE_ATTACH_SOUND: 2242 case REQUEST_CODE_RECORD_SOUND: 2243 Uri uri; 2244 if (requestCode == REQUEST_CODE_ATTACH_SOUND) { 2245 uri = (Uri) data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); 2246 if (Settings.System.DEFAULT_RINGTONE_URI.equals(uri)) { 2247 uri = null; 2248 } 2249 } else { 2250 uri = data.getData(); 2251 } 2252 2253 if (uri == null) { 2254 convertMessageIfNeeded(HAS_ATTACHMENT, hasAttachment()); 2255 return; 2256 } 2257 2258 try { 2259 mAttachmentEditor.changeAudio(uri); 2260 mAttachmentEditor.setAttachment( 2261 mSlideshow, AttachmentEditor.AUDIO_ATTACHMENT); 2262 } catch (MmsException e) { 2263 Log.e(TAG, "add audio failed", e); 2264 Toast.makeText(this, 2265 getResourcesString(R.string.failed_to_add_media, getAudioString()), 2266 Toast.LENGTH_SHORT).show(); 2267 } catch (UnsupportContentTypeException e) { 2268 MessageUtils.showErrorDialog(ComposeMessageActivity.this, 2269 getResourcesString(R.string.unsupported_media_format, getAudioString()), 2270 getResourcesString(R.string.select_different_media, getAudioString())); 2271 } catch (ExceedMessageSizeException e) { 2272 MessageUtils.showErrorDialog(ComposeMessageActivity.this, 2273 getResourcesString(R.string.exceed_message_size_limitation), 2274 getResourcesString(R.string.failed_to_add_media, getAudioString())); 2275 } 2276 break; 2277 2278 default: 2279 // TODO 2280 break; 2281 } 2282 2283 // Make sure if there was an error that our message 2284 // type remains correct. Excludes add image because it may be in async 2285 // resize process. 2286 if (!requiresMms() && (REQUEST_CODE_ATTACH_IMAGE != requestCode)) { 2287 convertMessage(false); 2288 } 2289 } 2290 2291 private void addImage(Bitmap bitmap) { 2292 try { 2293 addImage(MessageUtils.saveBitmapAsPart(this, mMessageUri, bitmap)); 2294 } catch (MmsException e) { 2295 handleAddImageFailure(e); 2296 } 2297 } 2298 2299 private final ResizeImageResultCallback mResizeImageCallback = new ResizeImageResultCallback() { 2300 public void onResizeResult(PduPart part) { 2301 Context context = ComposeMessageActivity.this; 2302 Resources r = context.getResources(); 2303 2304 if (part == null) { 2305 Toast.makeText(context, 2306 r.getString(R.string.failed_to_add_media, getPictureString()), 2307 Toast.LENGTH_SHORT).show(); 2308 return; 2309 } 2310 2311 convertMessageIfNeeded(HAS_ATTACHMENT, true); 2312 try { 2313 long messageId = ContentUris.parseId(mMessageUri); 2314 Uri newUri = mPersister.persistPart(part, messageId); 2315 mAttachmentEditor.changeImage(newUri); 2316 mAttachmentEditor.setAttachment( 2317 mSlideshow, AttachmentEditor.IMAGE_ATTACHMENT); 2318 } catch (MmsException e) { 2319 Toast.makeText(context, 2320 r.getString(R.string.failed_to_add_media, getPictureString()), 2321 Toast.LENGTH_SHORT).show(); 2322 } catch (UnsupportContentTypeException e) { 2323 MessageUtils.showErrorDialog(context, 2324 r.getString(R.string.unsupported_media_format, getPictureString()), 2325 r.getString(R.string.select_different_media, getPictureString())); 2326 } catch (ResolutionException e) { 2327 MessageUtils.showErrorDialog(context, 2328 r.getString(R.string.failed_to_resize_image), 2329 r.getString(R.string.resize_image_error_information)); 2330 } catch (ExceedMessageSizeException e) { 2331 MessageUtils.showErrorDialog(context, 2332 r.getString(R.string.exceed_message_size_limitation), 2333 r.getString(R.string.failed_to_add_media, getPictureString())); 2334 } 2335 } 2336 }; 2337 2338 private void addImage(Uri uri) { 2339 try { 2340 mAttachmentEditor.changeImage(uri); 2341 mAttachmentEditor.setAttachment( 2342 mSlideshow, AttachmentEditor.IMAGE_ATTACHMENT); 2343 } catch (MmsException e) { 2344 handleAddImageFailure(e); 2345 } catch (UnsupportContentTypeException e) { 2346 MessageUtils.showErrorDialog( 2347 ComposeMessageActivity.this, 2348 getResourcesString(R.string.unsupported_media_format, getPictureString()), 2349 getResourcesString(R.string.select_different_media, getPictureString())); 2350 } catch (ResolutionException e) { 2351 MessageUtils.resizeImageAsync(ComposeMessageActivity.this, 2352 uri, mAttachmentEditorHandler, mResizeImageCallback); 2353 } catch (ExceedMessageSizeException e) { 2354 MessageUtils.showErrorDialog( 2355 ComposeMessageActivity.this, 2356 getResourcesString(R.string.exceed_message_size_limitation), 2357 getResourcesString(R.string.failed_to_add_media, getPictureString())); 2358 } 2359 } 2360 2361 private void handleAddImageFailure(MmsException exception) { 2362 Log.e(TAG, "add image failed", exception); 2363 Toast.makeText( 2364 this, 2365 getResourcesString(R.string.failed_to_add_media, getPictureString()), 2366 Toast.LENGTH_SHORT).show(); 2367 } 2368 2369 private void addVideo(Uri uri) { 2370 try { 2371 mAttachmentEditor.changeVideo(uri); 2372 mAttachmentEditor.setAttachment( 2373 mSlideshow, AttachmentEditor.VIDEO_ATTACHMENT); 2374 } catch (MmsException e) { 2375 Log.e(TAG, "add video failed", e); 2376 Toast.makeText(this, 2377 getResourcesString(R.string.failed_to_add_media, getVideoString()), 2378 Toast.LENGTH_SHORT).show(); 2379 } catch (UnsupportContentTypeException e) { 2380 MessageUtils.showErrorDialog(ComposeMessageActivity.this, 2381 getResourcesString(R.string.unsupported_media_format, getVideoString()), 2382 getResourcesString(R.string.select_different_media, getVideoString())); 2383 } catch (ExceedMessageSizeException e) { 2384 MessageUtils.showErrorDialog(ComposeMessageActivity.this, 2385 getResourcesString(R.string.exceed_message_size_limitation), 2386 getResourcesString(R.string.failed_to_add_media, getVideoString())); 2387 } 2388 } 2389 2390 private boolean handleForwardedMessage() { 2391 // If this is a forwarded message, it will have an Intent extra 2392 // indicating so. If not, bail out. 2393 if (getIntent().getBooleanExtra("forwarded_message", false) == false) { 2394 return false; 2395 } 2396 2397 // If we are forwarding an MMS, mMessageUri will already be set. 2398 if (mMessageUri != null) { 2399 convertMessage(true); 2400 } 2401 2402 return true; 2403 } 2404 2405 private boolean handleSendIntent(Intent intent) { 2406 Bundle extras = intent.getExtras(); 2407 2408 if (!Intent.ACTION_SEND.equals(intent.getAction()) || (extras == null)) { 2409 return false; 2410 } 2411 2412 if (extras.containsKey(Intent.EXTRA_STREAM)) { 2413 Uri uri = (Uri)extras.getParcelable(Intent.EXTRA_STREAM); 2414 if (uri != null) { 2415 convertMessage(true); 2416 if (intent.getType().startsWith("image/")) { 2417 addImage(uri); 2418 } else if (intent.getType().startsWith("video/")) { 2419 addVideo(uri); 2420 } 2421 } 2422 return true; 2423 } else if (extras.containsKey(Intent.EXTRA_TEXT)) { 2424 mMsgText = extras.getString(Intent.EXTRA_TEXT); 2425 return true; 2426 } 2427 2428 return false; 2429 } 2430 2431 private String getAudioString() { 2432 return getResourcesString(R.string.type_audio); 2433 } 2434 2435 private String getPictureString() { 2436 return getResourcesString(R.string.type_picture); 2437 } 2438 2439 private String getVideoString() { 2440 return getResourcesString(R.string.type_video); 2441 } 2442 2443 private String getResourcesString(int id, String mediaName) { 2444 Resources r = getResources(); 2445 return r.getString(id, mediaName); 2446 } 2447 2448 private String getResourcesString(int id) { 2449 Resources r = getResources(); 2450 return r.getString(id); 2451 } 2452 2453 private void fixEmptySlideshow(SlideshowModel slideshow) { 2454 if (slideshow.size() == 0) { 2455 SlideModel slide = new SlideModel(slideshow); 2456 slideshow.add(slide); 2457 } 2458 2459 if (!slideshow.get(0).hasText()) { 2460 TextModel text = new TextModel( 2461 this, ContentType.TEXT_PLAIN, "text_0.txt", 2462 slideshow.getLayout().getTextRegion()); 2463 slideshow.get(0).add(text); 2464 } 2465 } 2466 2467 private void drawBottomPanel(int attachmentType) { 2468 // Reset the counter for text editor. 2469 resetCounter(); 2470 2471 switch (attachmentType) { 2472 case AttachmentEditor.EMPTY: 2473 throw new IllegalArgumentException( 2474 "Type of the attachment may not be EMPTY."); 2475 case AttachmentEditor.SLIDESHOW_ATTACHMENT: 2476 mBottomPanel.setVisibility(View.GONE); 2477 findViewById(R.id.attachment_editor).requestFocus(); 2478 return; 2479 default: 2480 mBottomPanel.setVisibility(View.VISIBLE); 2481 CharSequence text = null; 2482 if (requiresMms()) { 2483 TextModel tm = mSlideshow.get(0).getText(); 2484 if (tm != null) { 2485 text = tm.getText(); 2486 } 2487 } else { 2488 text = mMsgText; 2489 } 2490 2491 if ((text != null) && !text.equals(mTextEditor.getText().toString())) { 2492 mTextEditor.setText(text); 2493 } 2494 } 2495 } 2496 2497 //========================================================== 2498 // Interface methods 2499 //========================================================== 2500 2501 public void onClick(View v) { 2502 if ((v == mSendButton) && isPreparedForSending()) { 2503 confirmSendMessageIfNeeded(); 2504 } 2505 } 2506 2507 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 2508 if (event != null) { 2509 if (!event.isShiftPressed()) { 2510 if (isPreparedForSending()) { 2511 sendMessage(); 2512 } 2513 return true; 2514 } 2515 return false; 2516 } 2517 2518 if (isPreparedForSending()) { 2519 confirmSendMessageIfNeeded(); 2520 } 2521 return true; 2522 } 2523 2524 private final TextWatcher mTextEditorWatcher = new TextWatcher() { 2525 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 2526 } 2527 2528 public void onTextChanged(CharSequence s, int start, int before, int count) { 2529 // This is a workaround for bug 1609057. Since onUserInteraction() is 2530 // not called when the user touches the soft keyboard, we pretend it was 2531 // called when textfields changes. This should be removed when the bug 2532 // is fixed. 2533 onUserInteraction(); 2534 2535 if (requiresMms()) { 2536 // Update the content of the text model. 2537 TextModel text = mSlideshow.get(0).getText(); 2538 if (text == null) { 2539 text = new TextModel( 2540 ComposeMessageActivity.this, 2541 ContentType.TEXT_PLAIN, 2542 "text_0.txt", 2543 mSlideshow.getLayout().getTextRegion() 2544 ); 2545 mSlideshow.get(0).add(text); 2546 } 2547 2548 text.setText(s); 2549 } else { 2550 mMsgText = s; 2551 } 2552 2553 updateSendButtonState(); 2554 2555 updateCounter(s, start, before, count); 2556 } 2557 2558 public void afterTextChanged(Editable s) { 2559 } 2560 }; 2561 2562 //========================================================== 2563 // Private methods 2564 //========================================================== 2565 2566 /** 2567 * Initialize all UI elements from resources. 2568 */ 2569 private void initResourceRefs() { 2570 mMsgListView = (MessageListView) findViewById(R.id.history); 2571 mMsgListView.setDivider(null); // no divider so we look like IM conversation. 2572 mBottomPanel = findViewById(R.id.bottom_panel); 2573 mTextEditor = (EditText) findViewById(R.id.embedded_text_editor); 2574 mTextEditor.setOnEditorActionListener(this); 2575 mTextEditor.addTextChangedListener(mTextEditorWatcher); 2576 mTextCounter = (TextView) findViewById(R.id.text_counter); 2577 mSendButton = (Button) findViewById(R.id.send_button); 2578 mSendButton.setOnClickListener(this); 2579 mTopPanel = findViewById(R.id.recipients_subject_linear); 2580 } 2581 2582 private void confirmDeleteDialog(OnClickListener listener, boolean allMessages) { 2583 AlertDialog.Builder builder = new AlertDialog.Builder(this); 2584 builder.setTitle(R.string.confirm_dialog_title); 2585 builder.setIcon(android.R.drawable.ic_dialog_alert); 2586 builder.setCancelable(true); 2587 builder.setMessage(allMessages 2588 ? R.string.confirm_delete_conversation 2589 : R.string.confirm_delete_message); 2590 builder.setPositiveButton(R.string.yes, listener); 2591 builder.setNegativeButton(R.string.no, null); 2592 builder.show(); 2593 } 2594 2595 void undeliveredMessageDialog(long date) { 2596 String body; 2597 LinearLayout dialog = (LinearLayout) LayoutInflater.from(this).inflate( 2598 R.layout.retry_sending_dialog, null); 2599 2600 if (date >= 0) { 2601 body = getString(R.string.undelivered_msg_dialog_body, 2602 MessageUtils.formatTimeStampString(this, date)); 2603 } else { 2604 // FIXME: we can not get sms retry time. 2605 body = getString(R.string.undelivered_sms_dialog_body); 2606 } 2607 2608 ((TextView) dialog.findViewById(R.id.body_text_view)).setText(body); 2609 2610 Toast undeliveredDialog = new Toast(this); 2611 undeliveredDialog.setView(dialog); 2612 undeliveredDialog.setDuration(Toast.LENGTH_LONG); 2613 undeliveredDialog.show(); 2614 } 2615 2616 private String deriveAddress(Intent intent) { 2617 Uri recipientUri = intent.getData(); 2618 return (recipientUri == null) 2619 ? null : recipientUri.getSchemeSpecificPart(); 2620 } 2621 2622 private Uri getThreadUri() { 2623 return ContentUris.withAppendedId(Threads.CONTENT_URI, mThreadId); 2624 } 2625 2626 private void startMsgListQuery() { 2627 if (mThreadId <= 0) { 2628 return; 2629 } 2630 2631 // Cancel any pending queries 2632 mBackgroundQueryHandler.cancelOperation(MESSAGE_LIST_QUERY_TOKEN); 2633 try { 2634 // Kick off the new query 2635 mBackgroundQueryHandler.startQuery( 2636 MESSAGE_LIST_QUERY_TOKEN, null, getThreadUri(), 2637 PROJECTION, null, null, null); 2638 } catch (SQLiteException e) { 2639 SqliteWrapper.checkSQLiteException(this, e); 2640 } 2641 } 2642 2643 private void initMessageList() { 2644 if (mMsgListAdapter != null) { 2645 return; 2646 } 2647 2648 // Initialize the list adapter with a null cursor. 2649 mMsgListAdapter = new MessageListAdapter( 2650 this, null, mMsgListView, true, getThreadType()); 2651 mMsgListAdapter.setOnDataSetChangedListener(mDataSetChangedListener); 2652 mMsgListAdapter.setMsgListItemHandler(mMessageListItemHandler); 2653 mMsgListView.setAdapter(mMsgListAdapter); 2654 mMsgListView.setItemsCanFocus(false); 2655 mMsgListView.setVisibility(View.VISIBLE); 2656 mMsgListView.setOnCreateContextMenuListener(mMsgListMenuCreateListener); 2657 mMsgListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 2658 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 2659 ((MessageListItem) view).onMessageListItemClick(); 2660 } 2661 }); 2662 } 2663 2664 private void loadDraft() { 2665 // If we have no associated thread ID, there can't be a draft. 2666 if (mThreadId <= 0) { 2667 return; 2668 } 2669 2670 // If we already have text, don't stomp on it with the draft. 2671 if (!TextUtils.isEmpty(mMsgText)) { 2672 return; 2673 } 2674 2675 // If we know there is no draft, don't bother to look for one. 2676 if (!DraftCache.getInstance().hasDraft(mThreadId)) { 2677 return; 2678 } 2679 2680 // Try to load an SMS draft; if one does not exist, 2681 // load an MMS draft. 2682 mMsgText = readTemporarySmsMessage(mThreadId); 2683 if (TextUtils.isEmpty(mMsgText)) { 2684 if (readTemporaryMmsMessage(mThreadId)) { 2685 convertMessage(true); 2686 } else { 2687 Log.e(TAG, "no SMS or MMS drafts in thread " + mThreadId); 2688 return; 2689 } 2690 } 2691 } 2692 2693 private void asyncDelete(Uri uri, String selection, String[] selectionArgs) { 2694 if (LOCAL_LOGV) Log.v(TAG, "asyncDelete " + uri); 2695 2696 mBackgroundQueryHandler.startDelete(0, null, uri, selection, selectionArgs); 2697 } 2698 2699 private void saveDraft() { 2700 // If we're in the middle of creating a slideshow or some other subactivity, 2701 // don't bother to discard the draft, because that's likely to delete the draft 2702 // behind the slideshow's back. 2703 if (!hasValidRecipient() && !mWaitingForSubActivity) { 2704 discardTemporaryMessage(); 2705 DraftCache.getInstance().setDraftState(mThreadId, false); 2706 return; 2707 } 2708 2709 boolean savedAsDraft = false; 2710 if (needSaveAsMms()) { 2711 if (mMessageUri == null) { 2712 // no draft to be saved 2713 return; 2714 } 2715 2716 if (isEmptyMms() && !mWaitingForSubActivity) { 2717 asyncDelete(mMessageUri, null, null); 2718 } else { 2719 asyncUpdateTemporaryMmsMessage(mRecipientList.getToNumbers()); 2720 savedAsDraft = true; 2721 } 2722 } else { 2723 if (isEmptySms()) { 2724 if (mThreadId > 0) { 2725 asyncDeleteTemporarySmsMessage(mThreadId); 2726 } 2727 } else { 2728 asyncUpdateTemporarySmsMessage(mRecipientList.getToNumbers(), 2729 mMsgText.toString()); 2730 savedAsDraft = true; 2731 } 2732 } 2733 2734 DraftCache.getInstance().setDraftState(mThreadId, savedAsDraft); 2735 2736 if (mToastForDraftSave && savedAsDraft) { 2737 Toast.makeText(this, R.string.message_saved_as_draft, 2738 Toast.LENGTH_SHORT).show(); 2739 } 2740 } 2741 2742 private static final String[] MMS_DRAFT_PROJECTION = { 2743 Mms._ID, // 0 2744 Mms.SUBJECT // 1 2745 }; 2746 2747 private static final int MMS_ID_INDEX = 0; 2748 private static final int MMS_SUBJECT_INDEX = 1; 2749 2750 private boolean readTemporaryMmsMessage(long threadId) { 2751 Cursor cursor; 2752 2753 if (mMessageUri != null) { 2754 if (LOCAL_LOGV) { 2755 Log.v(TAG, "readTemporaryMmsMessage: already has message url " + mMessageUri); 2756 } 2757 return true; 2758 } 2759 2760 final String selection = Mms.THREAD_ID + " = " + threadId; 2761 cursor = SqliteWrapper.query(this, mContentResolver, 2762 Mms.Draft.CONTENT_URI, MMS_DRAFT_PROJECTION, 2763 selection, null, null); 2764 2765 try { 2766 if ((cursor.getCount() == 1) && cursor.moveToFirst()) { 2767 mMessageUri = ContentUris.withAppendedId(Mms.Draft.CONTENT_URI, 2768 cursor.getLong(MMS_ID_INDEX)); 2769 2770 mSubject = cursor.getString(MMS_SUBJECT_INDEX); 2771 if (!TextUtils.isEmpty(mSubject)) { 2772 updateState(HAS_SUBJECT, true); 2773 } 2774 return true; 2775 } 2776 } finally { 2777 cursor.close(); 2778 } 2779 2780 return false; 2781 } 2782 2783 2784 private Uri createTemporaryMmsMessage() throws MmsException { 2785 SendReq sendReq = new SendReq(); 2786 fillMessageHeaders(sendReq); 2787 PduBody pb = mSlideshow.toPduBody(); 2788 sendReq.setBody(pb); 2789 Uri res = mPersister.persist(sendReq, Mms.Draft.CONTENT_URI); 2790 mSlideshow.sync(pb); 2791 return res; 2792 } 2793 2794 private void asyncUpdateTemporaryMmsMessage(final String[] dests) { 2795 // PduPersister makes database calls and is known to ANR. Do the work on a 2796 // background thread. 2797 final SendReq sendReq = new SendReq(); 2798 fillMessageHeaders(sendReq); 2799 2800 new Thread(new Runnable() { 2801 public void run() { 2802 setThreadId(getOrCreateThreadId(dests)); 2803 updateTemporaryMmsMessage(mMessageUri, mPersister, 2804 mSlideshow, sendReq); 2805 } 2806 }).start(); 2807 2808 // Be paranoid and delete any SMS drafts that might be lying around. 2809 asyncDeleteTemporarySmsMessage(mThreadId); 2810 } 2811 2812 public static void updateTemporaryMmsMessage(Uri uri, PduPersister persister, 2813 SlideshowModel slideshow, SendReq sendReq) { 2814 persister.updateHeaders(uri, sendReq); 2815 final PduBody pb = slideshow.toPduBody(); 2816 2817 try { 2818 persister.updateParts(uri, pb); 2819 } catch (MmsException e) { 2820 Log.e(TAG, "updateTemporaryMmsMessage: cannot update message " + uri); 2821 } 2822 2823 slideshow.sync(pb); 2824 } 2825 2826 private static final String[] SMS_BODY_PROJECTION = { Sms._ID, Sms.BODY }; 2827 private static final String SMS_DRAFT_WHERE = Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT; 2828 2829 /** 2830 * Reads a draft message for the given thread ID from the database, 2831 * if there is one, deletes it from the database, and returns it. 2832 * @return The draft message or an empty string. 2833 */ 2834 private String readTemporarySmsMessage(long thread_id) { 2835 // If it's an invalid thread, don't bother. 2836 if (thread_id <= 0) { 2837 return ""; 2838 } 2839 2840 Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id); 2841 String body = ""; 2842 2843 Cursor c = SqliteWrapper.query(this, mContentResolver, 2844 thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null); 2845 2846 if (c != null) { 2847 try { 2848 if (c.moveToFirst()) { 2849 body = c.getString(1); 2850 } 2851 } finally { 2852 c.close(); 2853 } 2854 } 2855 2856 // Clean out drafts for this thread -- if the recipient set changes, 2857 // we will lose track of the original draft and be unable to delete 2858 // it later. The message will be re-saved if necessary upon exit of 2859 // the activity. 2860 SqliteWrapper.delete(this, mContentResolver, thread_uri, SMS_DRAFT_WHERE, null); 2861 2862 return body; 2863 } 2864 2865 private void asyncUpdateTemporarySmsMessage(final String[] dests, final String contents) { 2866 new Thread(new Runnable() { 2867 public void run() { 2868 setThreadId(getOrCreateThreadId(dests)); 2869 updateTemporarySmsMessage(mThreadId, contents); 2870 } 2871 }).start(); 2872 } 2873 2874 private void updateTemporarySmsMessage(long thread_id, String contents) { 2875 // If we don't have a valid thread, there's nothing to do. 2876 if (thread_id <= 0) { 2877 return; 2878 } 2879 2880 // Don't bother saving an empty message. 2881 if (TextUtils.isEmpty(contents)) { 2882 // But delete the old temporary message if it's there. 2883 deleteTemporarySmsMessage(thread_id); 2884 return; 2885 } 2886 2887 Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id); 2888 Cursor c = SqliteWrapper.query(this, mContentResolver, 2889 thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null); 2890 2891 try { 2892 if (c.moveToFirst()) { 2893 ContentValues values = new ContentValues(1); 2894 values.put(Sms.BODY, contents); 2895 SqliteWrapper.update(this, mContentResolver, thread_uri, values, 2896 SMS_DRAFT_WHERE, null); 2897 } else { 2898 ContentValues values = new ContentValues(3); 2899 values.put(Sms.THREAD_ID, thread_id); 2900 values.put(Sms.BODY, contents); 2901 values.put(Sms.TYPE, Sms.MESSAGE_TYPE_DRAFT); 2902 SqliteWrapper.insert(this, mContentResolver, Sms.CONTENT_URI, values); 2903 asyncDeleteTemporaryMmsMessage(thread_id); 2904 } 2905 } finally { 2906 c.close(); 2907 } 2908 } 2909 2910 private void asyncDeleteTemporarySmsMessage(long threadId) { 2911 asyncDelete(ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId), 2912 SMS_DRAFT_WHERE, null); 2913 } 2914 2915 private void deleteTemporarySmsMessage(long threadId) { 2916 SqliteWrapper.delete(this, mContentResolver, 2917 ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId), 2918 SMS_DRAFT_WHERE, null); 2919 } 2920 2921 private void asyncDeleteTemporaryMmsMessage(long threadId) { 2922 final String where = Mms.THREAD_ID + " = " + threadId; 2923 asyncDelete(Mms.Draft.CONTENT_URI, where, null); 2924 } 2925 2926 private void abandonDraftsAndFinish() { 2927 // If we are in MMS mode, first convert the message to SMS, 2928 // which will cause the MMS draft to be deleted. 2929 if (mMessageUri != null) { 2930 convertMessage(false); 2931 } 2932 // Now get rid of the SMS text to inhibit saving of a draft. 2933 mMsgText = ""; 2934 finish(); 2935 } 2936 2937 private String[] fillMessageHeaders(SendReq sendReq) { 2938 // Set the numbers in the 'TO' field. 2939 String[] dests = mRecipientList.getToNumbers(); 2940 EncodedStringValue[] encodedNumbers = encodeStrings(dests); 2941 if (encodedNumbers != null) { 2942 sendReq.setTo(encodedNumbers); 2943 } 2944 2945 // Set the numbers in the 'BCC' field. 2946 encodedNumbers = encodeStrings(mRecipientList.getBccNumbers()); 2947 if (encodedNumbers != null) { 2948 sendReq.setBcc(encodedNumbers); 2949 } 2950 2951 // Set the subject of the message. 2952 String subject = (mSubjectTextEditor == null) 2953 ? "" : mSubjectTextEditor.getText().toString(); 2954 sendReq.setSubject(new EncodedStringValue(subject)); 2955 2956 // Update the 'date' field of the message before sending it. 2957 sendReq.setDate(System.currentTimeMillis() / 1000L); 2958 2959 return dests; 2960 } 2961 2962 private boolean hasRecipient() { 2963 return hasValidRecipient() || hasInvalidRecipient(); 2964 } 2965 2966 private boolean hasValidRecipient() { 2967 // If someone is in the recipient list, or if a valid recipient is 2968 // currently in the recipients editor, we have recipients. 2969 return (mRecipientList.hasValidRecipient()) 2970 || ((mRecipientsEditor != null) 2971 && Recipient.isValid(mRecipientsEditor.getText().toString())); 2972 } 2973 2974 private boolean hasInvalidRecipient() { 2975 return (mRecipientList.hasInvalidRecipient()) 2976 || ((mRecipientsEditor != null) 2977 && !TextUtils.isEmpty(mRecipientsEditor.getText().toString()) 2978 && !Recipient.isValid(mRecipientsEditor.getText().toString())); 2979 } 2980 2981 private boolean hasText() { 2982 return mTextEditor.length() > 0; 2983 } 2984 2985 private boolean hasSubject() { 2986 return (null != mSubjectTextEditor) 2987 && !TextUtils.isEmpty(mSubjectTextEditor.getText().toString()); 2988 } 2989 2990 private boolean isPreparedForSending() { 2991 return hasRecipient() && (hasAttachment() || hasText()); 2992 } 2993 2994 private long getOrCreateThreadId(String[] numbers) { 2995 HashSet<String> recipients = new HashSet<String>(); 2996 recipients.addAll(Arrays.asList(numbers)); 2997 return Threads.getOrCreateThreadId(this, recipients); 2998 } 2999 3000 3001 private void sendMessage() { 3002 // Need this for both SMS and MMS. 3003 final String[] dests = mRecipientList.getToNumbers(); 3004 3005 // needSaveAsMms will convert a message that is solely a Mms message because it has 3006 // an empty subject back into an Sms message. Doesn't notify the user of the conversion. 3007 if (needSaveAsMms()) { 3008 // Make local copies of the bits we need for sending a message, 3009 // because we will be doing it off of the main thread, which will 3010 // immediately continue on to resetting some of this state. 3011 final Uri mmsUri = mMessageUri; 3012 final PduPersister persister = mPersister; 3013 final SlideshowModel slideshow = mSlideshow; 3014 final SendReq sendReq = new SendReq(); 3015 fillMessageHeaders(sendReq); 3016 3017 // Make sure the text in slide 0 is no longer holding onto a reference to the text 3018 // in the message text box. 3019 slideshow.prepareForSend(); 3020 3021 // Do the dirty work of sending the message off of the main UI thread. 3022 new Thread(new Runnable() { 3023 public void run() { 3024 sendMmsWorker(dests, mmsUri, persister, slideshow, sendReq); 3025 } 3026 }).start(); 3027 } else { 3028 // Same rules apply as above. 3029 final String msgText = mMsgText.toString(); 3030 new Thread(new Runnable() { 3031 public void run() { 3032 sendSmsWorker(dests, msgText); 3033 } 3034 }).start(); 3035 } 3036 3037 if (mExitOnSent) { 3038 // If we are supposed to exit after a message is sent, 3039 // clear out the text and URIs to inhibit saving of any 3040 // drafts and call finish(). 3041 mMsgText = ""; 3042 mMessageUri = null; 3043 finish(); 3044 } else { 3045 // Otherwise, reset the UI to be ready for the next message. 3046 resetMessage(); 3047 } 3048 } 3049 3050 /** 3051 * Do the actual work of sending a message. Runs outside of the main thread. 3052 */ 3053 private void sendSmsWorker(String[] dests, String msgText) { 3054 // Make sure we are still using the correct thread ID for our 3055 // recipient set. 3056 long threadId = getOrCreateThreadId(dests); 3057 3058 MessageSender sender = new SmsMessageSender(this, dests, msgText, threadId); 3059 try { 3060 sender.sendMessage(threadId); 3061 setThreadId(threadId); 3062 startMsgListQuery(); 3063 } catch (Exception e) { 3064 Log.e(TAG, "Failed to send SMS message, threadId=" + threadId, e); 3065 } 3066 } 3067 3068 private void sendMmsWorker(String[] dests, Uri mmsUri, PduPersister persister, 3069 SlideshowModel slideshow, SendReq sendReq) { 3070 // Make sure we are still using the correct thread ID for our 3071 // recipient set. 3072 long threadId = getOrCreateThreadId(dests); 3073 3074 if (LOCAL_LOGV) Log.v(TAG, "sendMmsWorker: update temporary MMS message " + mmsUri); 3075 3076 // Sync the MMS message in progress to disk. 3077 updateTemporaryMmsMessage(mmsUri, persister, slideshow, sendReq); 3078 // Be paranoid and clean any draft SMS up. 3079 deleteTemporarySmsMessage(threadId); 3080 3081 MessageSender sender = new MmsMessageSender(this, mmsUri); 3082 try { 3083 if (!sender.sendMessage(threadId)) { 3084 // The message was sent through SMS protocol, we should 3085 // delete the copy which was previously saved in MMS drafts. 3086 SqliteWrapper.delete(this, mContentResolver, mmsUri, null, null); 3087 } 3088 3089 setThreadId(threadId); 3090 startMsgListQuery(); 3091 } catch (Exception e) { 3092 Log.e(TAG, "Failed to send message: " + mmsUri + ", threadId=" + threadId, e); 3093 } 3094 } 3095 3096 private void resetMessage() { 3097 // Make the attachment editor hide its view before we destroy it. 3098 if (mAttachmentEditor != null) { 3099 mAttachmentEditor.hideView(); 3100 } 3101 3102 // Focus to the text editor. 3103 mTextEditor.requestFocus(); 3104 3105 // We have to remove the text change listener while the text editor gets cleared and 3106 // we subsequently turn the message back into SMS. When the listener is listening while 3107 // doing the clearing, it's fighting to update its counts and itself try and turn 3108 // the message one way or the other. 3109 mTextEditor.removeTextChangedListener(mTextEditorWatcher); 3110 3111 // RECIPIENTS_REQUIRE_MMS is the only state flag that is valid 3112 // when starting a new message, so preserve only that. 3113 mMessageState &= RECIPIENTS_REQUIRE_MMS; 3114 3115 // Clear the text box. 3116 TextKeyListener.clear(mTextEditor.getText()); 3117 3118 // Clear out the slideshow and message URI. New ones will be 3119 // created if we are starting a new message as MMS. 3120 mSlideshow = null; 3121 mMessageUri = null; 3122 3123 // Empty out text. 3124 mMsgText = ""; 3125 3126 // Convert back to SMS if necessary, or if we still need to 3127 // be in MMS mode, reset the MMS components. 3128 if (!requiresMms()) { 3129 // Start a new message as an SMS. 3130 convertMessage(false); 3131 } else { 3132 // Start a new message as an MMS. 3133 resetMmsComponents(); 3134 } 3135 3136 drawBottomPanel(AttachmentEditor.TEXT_ONLY); 3137 3138 // "Or not", in this case. 3139 updateSendButtonState(); 3140 3141 // Hide the recipients editor. 3142 if (mRecipientsEditor != null) { 3143 mRecipientsEditor.setVisibility(View.GONE); 3144 hideTopPanelIfNecessary(); 3145 } 3146 3147 // Our changes are done. Let the listener respond to text changes once again. 3148 mTextEditor.addTextChangedListener(mTextEditorWatcher); 3149 3150 // Close the soft on-screen keyboard if we're in landscape mode so the user can see the 3151 // conversation. 3152 if (mIsLandscape) { 3153 InputMethodManager inputMethodManager = 3154 (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); 3155 3156 inputMethodManager.hideSoftInputFromWindow(mTextEditor.getWindowToken(), 0); 3157 } 3158 } 3159 3160 private void updateSendButtonState() { 3161 boolean enable = false; 3162 if (isPreparedForSending()) { 3163 // When the type of attachment is slideshow, we should 3164 // also hide the 'Send' button since the slideshow view 3165 // already has a 'Send' button embedded. 3166 if ((mAttachmentEditor == null) || 3167 (mAttachmentEditor.getAttachmentType() != AttachmentEditor.SLIDESHOW_ATTACHMENT)) { 3168 enable = true; 3169 } else { 3170 mAttachmentEditor.setCanSend(true); 3171 } 3172 } else if (null != mAttachmentEditor){ 3173 mAttachmentEditor.setCanSend(false); 3174 } 3175 3176 mSendButton.setEnabled(enable); 3177 mSendButton.setFocusable(enable); 3178 } 3179 3180 private long getMessageDate(Uri uri) { 3181 if (uri != null) { 3182 Cursor cursor = SqliteWrapper.query(this, mContentResolver, 3183 uri, new String[] { Mms.DATE }, null, null, null); 3184 if (cursor != null) { 3185 try { 3186 if ((cursor.getCount() == 1) && cursor.moveToFirst()) { 3187 return cursor.getLong(0) * 1000L; 3188 } 3189 } finally { 3190 cursor.close(); 3191 } 3192 } 3193 } 3194 return NO_DATE_FOR_DIALOG; 3195 } 3196 3197 private void setSubjectFromIntent(Intent intent) { 3198 String subject = intent.getStringExtra("subject"); 3199 if ( !TextUtils.isEmpty(subject) ) { 3200 mSubject = subject; 3201 } 3202 } 3203 3204 private void setThreadId(long threadId) { 3205 mThreadId = threadId; 3206 } 3207 3208 private void initActivityState(Bundle savedInstanceState, Intent intent) { 3209 if (savedInstanceState != null) { 3210 setThreadId(savedInstanceState.getLong("thread_id", 0)); 3211 mMessageUri = (Uri) savedInstanceState.getParcelable("msg_uri"); 3212 mExternalAddress = savedInstanceState.getString("address"); 3213 mExitOnSent = savedInstanceState.getBoolean("exit_on_sent", false); 3214 mSubject = savedInstanceState.getString("subject"); 3215 mMsgText = savedInstanceState.getString("sms_body"); 3216 } else { 3217 setThreadId(intent.getLongExtra("thread_id", 0)); 3218 mMessageUri = (Uri) intent.getParcelableExtra("msg_uri"); 3219 if ((mMessageUri == null) && (mThreadId == 0)) { 3220 // If we haven't been given a thread id or a URI in the extras, 3221 // get it out of the intent. 3222 Uri uri = intent.getData(); 3223 if ((uri != null) && (uri.getPathSegments().size() >= 2)) { 3224 try { 3225 setThreadId(Long.parseLong(uri.getPathSegments().get(1))); 3226 } catch (NumberFormatException exception) { 3227 Log.e(TAG, "Thread ID must be a Long."); 3228 } 3229 } 3230 } 3231 mExternalAddress = intent.getStringExtra("address"); 3232 mExitOnSent = intent.getBooleanExtra("exit_on_sent", false); 3233 mMsgText = intent.getStringExtra("sms_body"); 3234 3235 setSubjectFromIntent(intent); 3236 } 3237 3238 if (!TextUtils.isEmpty(mSubject)) { 3239 updateState(HAS_SUBJECT, true); 3240 } 3241 3242 // If there was not a body already, start with a blank one. 3243 if (mMsgText == null) { 3244 mMsgText = ""; 3245 } 3246 3247 if (mExternalAddress == null) { 3248 if (mThreadId > 0L) { 3249 mExternalAddress = MessageUtils.getAddressByThreadId(this, mThreadId); 3250 } else { 3251 mExternalAddress = deriveAddress(intent); 3252 // Even if we end up creating a thread here and the user 3253 // discards the message, we will clean it up later when we 3254 // delete obsolete threads. 3255 if (!TextUtils.isEmpty(mExternalAddress)) { 3256 setThreadId(getOrCreateThreadId(new String[] { mExternalAddress })); 3257 } 3258 } 3259 } 3260 } 3261 3262 private int getThreadType() { 3263 boolean isMms = (mMessageUri != null) || requiresMms(); 3264 3265 return (!isMms 3266 && (mRecipientList != null) 3267 && (mRecipientList.size() > 1)) 3268 ? Threads.BROADCAST_THREAD 3269 : Threads.COMMON_THREAD; 3270 } 3271 3272 private void updateWindowTitle() { 3273 StringBuilder sb = new StringBuilder(); 3274 Iterator<Recipient> iter = mRecipientList.iterator(); 3275 while (iter.hasNext()) { 3276 Recipient r = iter.next(); 3277 sb.append(r.nameAndNumber).append(", "); 3278 } 3279 3280 ContactInfoCache cache = ContactInfoCache.getInstance(); 3281 String[] values = mRecipientList.getBccNumbers(); 3282 if (values.length > 0) { 3283 sb.append("Bcc: "); 3284 for (String v : values) { 3285 sb.append(cache.getContactName(this, v)).append(", "); 3286 } 3287 } 3288 3289 if (sb.length() > 0) { 3290 // Delete the trailing ", " characters. 3291 int tail = sb.length() - 2; 3292 setTitle(sb.delete(tail, tail + 2).toString()); 3293 } else { 3294 setTitle(getString(R.string.compose_title)); 3295 } 3296 } 3297 3298 private void initFocus() { 3299 if (!mIsKeyboardOpen) { 3300 return; 3301 } 3302 3303 // If the recipients editor is visible, there is nothing in it, 3304 // and the text editor is not already focused, focus the 3305 // recipients editor. 3306 if (isRecipientsEditorVisible() && TextUtils.isEmpty(mRecipientsEditor.getText()) 3307 && !mTextEditor.isFocused()) { 3308 mRecipientsEditor.requestFocus(); 3309 return; 3310 } 3311 3312 // If we decided not to focus the recipients editor, focus the text editor. 3313 mTextEditor.requestFocus(); 3314 } 3315 3316 private final MessageListAdapter.OnDataSetChangedListener 3317 mDataSetChangedListener = new MessageListAdapter.OnDataSetChangedListener() { 3318 public void onDataSetChanged(MessageListAdapter adapter) { 3319 mPossiblePendingNotification = true; 3320 } 3321 }; 3322 3323 private void checkPendingNotification() { 3324 if (mPossiblePendingNotification && hasWindowFocus()) { 3325 markAsRead(mThreadId); 3326 mPossiblePendingNotification = false; 3327 } 3328 } 3329 3330 private void markAsRead(long threadId) { 3331 if (threadId <= 0) { 3332 return; 3333 } 3334 3335 Uri threadUri = ContentUris.withAppendedId(Threads.CONTENT_URI, threadId); 3336 ContentValues values = new ContentValues(1); 3337 values.put("read", 1); 3338 String where = "read = 0"; 3339 3340 mBackgroundQueryHandler.startUpdate(MARK_AS_READ_TOKEN, null, 3341 threadUri, values, where, null); 3342 } 3343 3344 private final class BackgroundQueryHandler extends AsyncQueryHandler { 3345 public BackgroundQueryHandler(ContentResolver contentResolver) { 3346 super(contentResolver); 3347 } 3348 3349 @Override 3350 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 3351 switch(token) { 3352 case MESSAGE_LIST_QUERY_TOKEN: 3353 mMsgListAdapter.changeCursor(cursor); 3354 3355 // Once we have completed the query for the message history, if 3356 // there is nothing in the cursor and we are not composing a new 3357 // message, we must be editing a draft in a new conversation. 3358 // Show the recipients editor to give the user a chance to add 3359 // more people before the conversation begins. 3360 if (cursor.getCount() == 0 && !isRecipientsEditorVisible()) { 3361 initRecipientsEditor(); 3362 } 3363 3364 // FIXME: freshing layout changes the focused view to an unexpected 3365 // one, set it back to TextEditor forcely. 3366 mTextEditor.requestFocus(); 3367 3368 return; 3369 3370 case CALLER_ID_QUERY_TOKEN: 3371 case EMAIL_CONTACT_QUERY_TOKEN: 3372 cleanupContactInfoCursor(); 3373 mContactInfoCursor = cursor; 3374 updateContactInfo(); 3375 startPresencePollingRequest(); 3376 return; 3377 3378 } 3379 } 3380 3381 @Override 3382 protected void onDeleteComplete(int token, Object cookie, int result) { 3383 switch(token) { 3384 case DELETE_MESSAGE_TOKEN: 3385 case DELETE_CONVERSATION_TOKEN: 3386 // Update the notification for new messages since they 3387 // may be deleted. 3388 MessagingNotification.updateNewMessageIndicator( 3389 ComposeMessageActivity.this); 3390 // Update the notification for failed messages since they 3391 // may be deleted. 3392 updateSendFailedNotification(); 3393 break; 3394 } 3395 3396 if (token == DELETE_CONVERSATION_TOKEN) { 3397 ComposeMessageActivity.this.abandonDraftsAndFinish(); 3398 } 3399 } 3400 3401 @Override 3402 protected void onUpdateComplete(int token, Object cookie, int result) { 3403 switch(token) { 3404 case MARK_AS_READ_TOKEN: 3405 MessagingNotification.updateAllNotifications(ComposeMessageActivity.this); 3406 break; 3407 } 3408 } 3409 } 3410 3411 private void showSmileyDialog() { 3412 if (mSmileyDialog == null) { 3413 int[] icons = SmileyParser.DEFAULT_SMILEY_RES_IDS; 3414 String[] names = getResources().getStringArray( 3415 SmileyParser.DEFAULT_SMILEY_NAMES); 3416 final String[] texts = getResources().getStringArray( 3417 SmileyParser.DEFAULT_SMILEY_TEXTS); 3418 3419 final int N = names.length; 3420 3421 List<Map<String, ?>> entries = new ArrayList<Map<String, ?>>(); 3422 for (int i = 0; i < N; i++) { 3423 // We might have different ASCII for the same icon, skip it if 3424 // the icon is already added. 3425 boolean added = false; 3426 for (int j = 0; j < i; j++) { 3427 if (icons[i] == icons[j]) { 3428 added = true; 3429 break; 3430 } 3431 } 3432 if (!added) { 3433 HashMap<String, Object> entry = new HashMap<String, Object>(); 3434 3435 entry. put("icon", icons[i]); 3436 entry. put("name", names[i]); 3437 entry.put("text", texts[i]); 3438 3439 entries.add(entry); 3440 } 3441 } 3442 3443 final SimpleAdapter a = new SimpleAdapter( 3444 this, 3445 entries, 3446 R.layout.smiley_menu_item, 3447 new String[] {"icon", "name", "text"}, 3448 new int[] {R.id.smiley_icon, R.id.smiley_name, R.id.smiley_text}); 3449 SimpleAdapter.ViewBinder viewBinder = new SimpleAdapter.ViewBinder() { 3450 public boolean setViewValue(View view, Object data, String textRepresentation) { 3451 if (view instanceof ImageView) { 3452 Drawable img = getResources().getDrawable((Integer)data); 3453 ((ImageView)view).setImageDrawable(img); 3454 return true; 3455 } 3456 return false; 3457 } 3458 }; 3459 a.setViewBinder(viewBinder); 3460 3461 AlertDialog.Builder b = new AlertDialog.Builder(this); 3462 3463 b.setTitle(getString(R.string.menu_insert_smiley)); 3464 3465 b.setCancelable(true); 3466 b.setAdapter(a, new DialogInterface.OnClickListener() { 3467 public final void onClick(DialogInterface dialog, int which) { 3468 HashMap<String, Object> item = (HashMap<String, Object>) a.getItem(which); 3469 mTextEditor.append((String)item.get("text")); 3470 } 3471 }); 3472 3473 mSmileyDialog = b.create(); 3474 } 3475 3476 mSmileyDialog.show(); 3477 } 3478 3479 private void cleanupContactInfoCursor() { 3480 if (mContactInfoCursor != null) { 3481 mContactInfoCursor.close(); 3482 } 3483 } 3484 3485 private void cancelPresencePollingRequests() { 3486 mPresencePollingHandler.removeMessages(REFRESH_PRESENCE); 3487 } 3488 3489 private void startPresencePollingRequest() { 3490 mPresencePollingHandler.sendEmptyMessageDelayed(REFRESH_PRESENCE, 3491 60 * 1000); // refresh every minute 3492 } 3493 3494 private void startQueryForContactInfo() { 3495 String number = mRecipientList.getSingleRecipientNumber(); 3496 cancelPresencePollingRequests(); // make sure there are no outstanding polling requests 3497 if (TextUtils.isEmpty(number)) { 3498 setPresenceIcon(0); 3499 startPresencePollingRequest(); 3500 return; 3501 } 3502 3503 mContactInfoSelectionArgs[0] = number; 3504 3505 if (Mms.isEmailAddress(number)) { 3506 // Cancel any pending queries 3507 mBackgroundQueryHandler.cancelOperation(EMAIL_CONTACT_QUERY_TOKEN); 3508 3509 mBackgroundQueryHandler.startQuery(EMAIL_CONTACT_QUERY_TOKEN, null, 3510 METHOD_WITH_PRESENCE_URI, 3511 EMAIL_QUERY_PROJECTION, 3512 METHOD_LOOKUP, 3513 mContactInfoSelectionArgs, 3514 null); 3515 } else { 3516 // Cancel any pending queries 3517 mBackgroundQueryHandler.cancelOperation(CALLER_ID_QUERY_TOKEN); 3518 3519 mBackgroundQueryHandler.startQuery(CALLER_ID_QUERY_TOKEN, null, 3520 PHONES_WITH_PRESENCE_URI, 3521 CALLER_ID_PROJECTION, 3522 NUMBER_LOOKUP, 3523 mContactInfoSelectionArgs, 3524 null); 3525 } 3526 } 3527 3528 private void updateContactInfo() { 3529 boolean updated = false; 3530 if (mContactInfoCursor != null && mContactInfoCursor.moveToFirst()) { 3531 mPresenceStatus = mContactInfoCursor.getInt(PRESENCE_STATUS_COLUMN); 3532 if (mPresenceStatus != Contacts.People.OFFLINE) { 3533 int presenceIcon = Presence.getPresenceIconResourceId(mPresenceStatus); 3534 setPresenceIcon(presenceIcon); 3535 updated = true; 3536 } 3537 } 3538 if (!updated) { 3539 setPresenceIcon(0); 3540 } 3541 } 3542 3543} 3544