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