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