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