ComposeMessageActivity.java revision 227c061247fb53150330a1c11bc163881def7ef6
1/* 2 * Copyright (C) 2008 Esmertec AG. 3 * Copyright (C) 2008 The Android Open Source Project 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.mms.ui; 19 20import static android.content.res.Configuration.KEYBOARDHIDDEN_NO; 21import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_ABORT; 22import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_COMPLETE; 23import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_START; 24import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_STATUS_ACTION; 25import static com.android.mms.ui.MessageListAdapter.COLUMN_ID; 26import static com.android.mms.ui.MessageListAdapter.COLUMN_MSG_TYPE; 27import static com.android.mms.ui.MessageListAdapter.PROJECTION; 28 29import com.android.mms.ExceedMessageSizeException; 30import com.android.mms.MmsConfig; 31import com.android.mms.R; 32import com.android.mms.ResolutionException; 33import com.android.mms.UnsupportContentTypeException; 34import com.android.mms.model.SlideModel; 35import com.android.mms.model.SlideshowModel; 36import com.android.mms.model.TextModel; 37import com.android.mms.transaction.MessageSender; 38import com.android.mms.transaction.MessagingNotification; 39import com.android.mms.transaction.MmsMessageSender; 40import com.android.mms.transaction.SmsMessageSender; 41import com.android.mms.ui.AttachmentEditor.OnAttachmentChangedListener; 42import com.android.mms.ui.MessageUtils.ResizeImageResultCallback; 43import com.android.mms.ui.RecipientList.Recipient; 44import com.android.mms.ui.RecipientsEditor.RecipientContextMenuInfo; 45import com.android.mms.util.ContactInfoCache; 46import com.android.mms.util.DraftCache; 47import com.android.mms.util.SendingProgressTokenManager; 48import com.android.mms.util.SmileyParser; 49 50import com.google.android.mms.ContentType; 51import com.google.android.mms.MmsException; 52import com.google.android.mms.pdu.EncodedStringValue; 53import com.google.android.mms.pdu.PduBody; 54import com.google.android.mms.pdu.PduPart; 55import com.google.android.mms.pdu.PduPersister; 56import com.google.android.mms.pdu.SendReq; 57import com.google.android.mms.util.SqliteWrapper; 58 59import android.app.Activity; 60import android.app.AlertDialog; 61import android.content.AsyncQueryHandler; 62import android.content.BroadcastReceiver; 63import android.content.ContentResolver; 64import android.content.ContentUris; 65import android.content.ContentValues; 66import android.content.Context; 67import android.content.DialogInterface; 68import android.content.Intent; 69import android.content.IntentFilter; 70import android.content.DialogInterface.OnClickListener; 71import android.content.res.Configuration; 72import android.content.res.Resources; 73import android.database.Cursor; 74import android.database.DatabaseUtils; 75import android.database.sqlite.SQLiteException; 76import android.graphics.Bitmap; 77import android.graphics.drawable.Drawable; 78import android.media.RingtoneManager; 79import android.net.Uri; 80import android.os.Bundle; 81import android.os.Handler; 82import android.os.Message; 83import android.provider.Contacts; 84import android.provider.Contacts.People; 85import android.provider.Contacts.Presence; 86import android.provider.MediaStore; 87import android.provider.Settings; 88import android.provider.Telephony.Mms; 89import android.provider.Telephony.Sms; 90import android.provider.Telephony.Threads; 91import android.telephony.gsm.SmsMessage; 92import android.text.ClipboardManager; 93import android.text.Editable; 94import android.text.InputFilter; 95import android.text.SpannableString; 96import android.text.Spanned; 97import android.text.TextUtils; 98import android.text.TextWatcher; 99import android.text.method.TextKeyListener; 100import android.text.style.URLSpan; 101import android.text.util.Linkify; 102import android.util.Config; 103import android.util.Log; 104import android.view.ContextMenu; 105import android.view.KeyEvent; 106import android.view.LayoutInflater; 107import android.view.Menu; 108import android.view.MenuItem; 109import android.view.View; 110import android.view.ViewStub; 111import android.view.Window; 112import android.view.ContextMenu.ContextMenuInfo; 113import android.view.View.OnCreateContextMenuListener; 114import android.view.View.OnFocusChangeListener; 115import android.view.View.OnKeyListener; 116import android.view.inputmethod.InputMethodManager; 117import android.widget.AdapterView; 118import android.widget.Button; 119import android.widget.EditText; 120import android.widget.ImageView; 121import android.widget.LinearLayout; 122import android.widget.ListView; 123import android.widget.SimpleAdapter; 124import android.widget.TextView; 125import android.widget.Toast; 126 127import java.io.InputStream; 128import java.io.IOException; 129import java.io.File; 130import java.io.FileInputStream; 131import java.io.FileOutputStream; 132import java.util.ArrayList; 133import java.util.Arrays; 134import java.util.HashMap; 135import java.util.HashSet; 136import java.util.Iterator; 137import java.util.List; 138import java.util.Map; 139 140import android.webkit.MimeTypeMap; 141 142/** 143 * This is the main UI for: 144 * 1. Composing a new message; 145 * 2. Viewing/managing message history of a conversation. 146 * 147 * This activity can handle following parameters from the intent 148 * by which it's launched. 149 * thread_id long Identify the conversation to be viewed. When creating a 150 * new message, this parameter shouldn't be present. 151 * msg_uri Uri The message which should be opened for editing in the editor. 152 * address String The addresses of the recipients in current conversation. 153 * exit_on_sent boolean Exit this activity after the message is sent. 154 */ 155public class ComposeMessageActivity extends Activity 156 implements View.OnClickListener, TextView.OnEditorActionListener, 157 OnAttachmentChangedListener { 158 public static final int REQUEST_CODE_ATTACH_IMAGE = 10; 159 public static final int REQUEST_CODE_TAKE_PICTURE = 11; 160 public static final int REQUEST_CODE_ATTACH_VIDEO = 12; 161 public static final int REQUEST_CODE_TAKE_VIDEO = 13; 162 public static final int REQUEST_CODE_ATTACH_SOUND = 14; 163 public static final int REQUEST_CODE_RECORD_SOUND = 15; 164 public static final int REQUEST_CODE_CREATE_SLIDESHOW = 16; 165 166 private static final String TAG = "ComposeMessageActivity"; 167 private static final boolean DEBUG = false; 168 private static final boolean TRACE = false; 169 private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; 170 171 // Menu ID 172 private static final int MENU_ADD_SUBJECT = 0; 173 private static final int MENU_DELETE_THREAD = 1; 174 private static final int MENU_ADD_ATTACHMENT = 2; 175 private static final int MENU_DISCARD = 3; 176 private static final int MENU_SEND = 4; 177 private static final int MENU_CALL_RECIPIENT = 5; 178 private static final int MENU_CONVERSATION_LIST = 6; 179 180 // Context menu ID 181 private static final int MENU_VIEW_CONTACT = 12; 182 private static final int MENU_ADD_TO_CONTACTS = 13; 183 184 private static final int MENU_EDIT_MESSAGE = 14; 185 private static final int MENU_VIEW_SLIDESHOW = 16; 186 private static final int MENU_VIEW_MESSAGE_DETAILS = 17; 187 private static final int MENU_DELETE_MESSAGE = 18; 188 private static final int MENU_SEARCH = 19; 189 private static final int MENU_DELIVERY_REPORT = 20; 190 private static final int MENU_FORWARD_MESSAGE = 21; 191 private static final int MENU_CALL_BACK = 22; 192 private static final int MENU_SEND_EMAIL = 23; 193 private static final int MENU_COPY_MESSAGE_TEXT = 24; 194 private static final int MENU_COPY_TO_SDCARD = 25; 195 private static final int MENU_INSERT_SMILEY = 26; 196 private static final int MENU_ADD_ADDRESS_TO_CONTACTS = 27; 197 198 private static final int SUBJECT_MAX_LENGTH = 40; 199 private static final int RECIPIENTS_MAX_LENGTH = 312; 200 201 private static final int MESSAGE_LIST_QUERY_TOKEN = 9527; 202 203 private static final int DELETE_MESSAGE_TOKEN = 9700; 204 private static final int DELETE_CONVERSATION_TOKEN = 9701; 205 206 private static final int CALLER_ID_QUERY_TOKEN = 9800; 207 private static final int EMAIL_CONTACT_QUERY_TOKEN = 9801; 208 209 private static final int MARK_AS_READ_TOKEN = 9900; 210 211 private static final int MMS_THRESHOLD = 4; 212 213 private static final int CHARS_REMAINING_BEFORE_COUNTER_SHOWN = 10; 214 215 private static final long NO_DATE_FOR_DIALOG = -1L; 216 217 private static final int REFRESH_PRESENCE = 45236; 218 219 220 // caller id query params 221 private static final String[] CALLER_ID_PROJECTION = new String[] { 222 People.PRESENCE_STATUS, // 0 223 }; 224 private static final int PRESENCE_STATUS_COLUMN = 0; 225 226 private static final String NUMBER_LOOKUP = "PHONE_NUMBERS_EQUAL(" 227 + Contacts.Phones.NUMBER + ",?)"; 228 private static final Uri PHONES_WITH_PRESENCE_URI 229 = Uri.parse(Contacts.Phones.CONTENT_URI + "_with_presence"); 230 231 // email contact query params 232 private static final String[] EMAIL_QUERY_PROJECTION = new String[] { 233 Contacts.People.PRESENCE_STATUS, // 0 234 }; 235 236 private static final String METHOD_LOOKUP = Contacts.ContactMethods.DATA + "=?"; 237 private static final Uri METHOD_WITH_PRESENCE_URI = 238 Uri.withAppendedPath(Contacts.ContactMethods.CONTENT_URI, "with_presence"); 239 240 241 242 private ContentResolver mContentResolver; 243 244 // The parameters/states of the activity. 245 private long mThreadId; // Database key for the current conversation 246 private String mExternalAddress; // Serialized recipients in the current conversation 247 private boolean mExitOnSent; // Should we finish() after sending a message? 248 249 private View mTopPanel; // View containing the recipient and subject editors 250 private View mBottomPanel; // View containing the text editor, send button, ec. 251 private EditText mTextEditor; // Text editor to type your message into 252 private TextView mTextCounter; // Shows the number of characters used in text editor 253 private Button mSendButton; // Press to detonate 254 255 private CharSequence mMsgText; // Text of message 256 257 private BackgroundQueryHandler mBackgroundQueryHandler; 258 259 private MessageListView mMsgListView; // ListView for messages in this conversation 260 private MessageListAdapter mMsgListAdapter; // and its corresponding ListAdapter 261 262 private RecipientList mRecipientList; // List of recipients for this conversation 263 private RecipientsEditor mRecipientsEditor; // UI control for editing recipients 264 265 private boolean mIsKeyboardOpen; // Whether the hardware keyboard is visible 266 private boolean mIsLandscape; // Whether we're in landscape mode 267 268 private boolean mPossiblePendingNotification; // If the message list has changed, we may have 269 // a pending notification to deal with. 270 271 private boolean mToastForDraftSave; // Whether to notify the user that a draft is 272 // being saved. 273 274 private static final int RECIPIENTS_REQUIRE_MMS = (1 << 0); // 1 275 private static final int HAS_SUBJECT = (1 << 1); // 2 276 private static final int HAS_ATTACHMENT = (1 << 2); // 4 277 private static final int LENGTH_REQUIRES_MMS = (1 << 3); // 8 278 279 private int mMessageState; // A bitmap of the above indicating different 280 // properties of the message -- any bit set 281 // will require conversion to MMS. 282 283 // These fields are only used in MMS compose mode (requiresMms() == true) and should 284 // otherwise be null. 285 private SlideshowModel mSlideshow; 286 private Uri mMessageUri; 287 private EditText mSubjectTextEditor; // Text editor for MMS subject 288 private String mSubject; // MMS subject 289 private AttachmentEditor mAttachmentEditor; 290 private PduPersister mPersister; 291 292 private AlertDialog mSmileyDialog; 293 294 // Everything needed to deal with presence 295 private Cursor mContactInfoCursor; 296 private int mPresenceStatus; 297 private String[] mContactInfoSelectionArgs = new String[1]; 298 299 private boolean mWaitingForSubActivity; 300 301 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.DISABLE_MMS) { 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.DISABLE_MMS && 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.mDeliveryReport || 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 @Override 1694 protected void onResume() { 1695 super.onResume(); 1696 startPresencePollingRequest(); 1697 } 1698 1699 private void updateSendFailedNotification() { 1700 // updateSendFailedNotificationForThread makes a database call, so do the work off 1701 // of the ui thread. 1702 new Thread(new Runnable() { 1703 public void run() { 1704 MessagingNotification.updateSendFailedNotificationForThread( 1705 ComposeMessageActivity.this, mThreadId); 1706 } 1707 }).run(); 1708 } 1709 1710 @Override 1711 public void onSaveInstanceState(Bundle outState) { 1712 super.onSaveInstanceState(outState); 1713 1714 if (mThreadId > 0L) { 1715 if (LOCAL_LOGV) { 1716 Log.v(TAG, "ONFREEZE: thread_id: " + mThreadId); 1717 } 1718 outState.putLong("thread_id", mThreadId); 1719 } 1720 1721 if (LOCAL_LOGV) { 1722 Log.v(TAG, "ONFREEZE: address: " + mRecipientList.serialize()); 1723 } 1724 outState.putString("address", mRecipientList.serialize()); 1725 1726 removeSubjectIfEmpty(); 1727 if (requiresMms()) { 1728 if (isSubjectEditorVisible()) { 1729 outState.putString("subject", mSubjectTextEditor.getText().toString()); 1730 } 1731 1732 if (mMessageUri != null) { 1733 if (LOCAL_LOGV) { 1734 Log.v(TAG, "ONFREEZE: mMessageUri: " + mMessageUri); 1735 } 1736 outState.putParcelable("msg_uri", mMessageUri); 1737 } 1738 } else { 1739 outState.putString("sms_body", mMsgText.toString()); 1740 } 1741 1742 if (mExitOnSent) { 1743 outState.putBoolean("exit_on_sent", mExitOnSent); 1744 } 1745 } 1746 1747 private boolean isEmptyMessage() { 1748 if (requiresMms()) { 1749 return isEmptyMms(); 1750 } 1751 return isEmptySms(); 1752 } 1753 1754 private boolean isEmptySms() { 1755 return TextUtils.isEmpty(mMsgText); 1756 } 1757 1758 private boolean isEmptyMms() { 1759 return !(hasText() || hasSubject() || hasAttachment()); 1760 } 1761 1762 private void removeSubjectIfEmpty() { 1763 // subject editor is visible without any contents. 1764 if ((mMessageState == HAS_SUBJECT) && !hasSubject()) { 1765 convertMessage(false); 1766 } 1767 } 1768 1769 @Override 1770 protected void onPause() { 1771 super.onPause(); 1772 cancelPresencePollingRequests(); 1773 } 1774 1775 @Override 1776 protected void onStop() { 1777 super.onStop(); 1778 1779 if (mMsgListAdapter != null) { 1780 mMsgListAdapter.changeCursor(null); 1781 } 1782 1783 saveDraft(); 1784 1785 // Cleanup the BroadcastReceiver. 1786 unregisterReceiver(mHttpProgressReceiver); 1787 1788 cleanupContactInfoCursor(); 1789 } 1790 1791 @Override 1792 protected void onDestroy() { 1793 if (TRACE) { 1794 android.os.Debug.stopMethodTracing(); 1795 } 1796 1797 super.onDestroy(); 1798 } 1799 1800 @Override 1801 public void onConfigurationChanged(Configuration newConfig) { 1802 super.onConfigurationChanged(newConfig); 1803 if (LOCAL_LOGV) { 1804 Log.v(TAG, "onConfigurationChanged: " + newConfig); 1805 } 1806 1807 mIsKeyboardOpen = newConfig.keyboardHidden == KEYBOARDHIDDEN_NO; 1808 mIsLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE; 1809 onKeyboardStateChanged(mIsKeyboardOpen); 1810 } 1811 1812 private void onKeyboardStateChanged(boolean isKeyboardOpen) { 1813 // If the keyboard is hidden, don't show focus highlights for 1814 // things that cannot receive input. 1815 if (isKeyboardOpen) { 1816 if (mRecipientsEditor != null) { 1817 mRecipientsEditor.setFocusableInTouchMode(true); 1818 } 1819 if (mSubjectTextEditor != null) { 1820 mSubjectTextEditor.setFocusableInTouchMode(true); 1821 } 1822 mTextEditor.setFocusableInTouchMode(true); 1823 mTextEditor.setHint(R.string.type_to_compose_text_enter_to_send); 1824 initFocus(); 1825 } else { 1826 if (mRecipientsEditor != null) { 1827 mRecipientsEditor.setFocusable(false); 1828 } 1829 if (mSubjectTextEditor != null) { 1830 mSubjectTextEditor.setFocusable(false); 1831 } 1832 mTextEditor.setFocusable(false); 1833 mTextEditor.setHint(R.string.open_keyboard_to_compose_message); 1834 } 1835 } 1836 1837 @Override 1838 public void onUserInteraction() { 1839 checkPendingNotification(); 1840 } 1841 1842 @Override 1843 public void onWindowFocusChanged(boolean hasFocus) { 1844 if (hasFocus) { 1845 checkPendingNotification(); 1846 } 1847 } 1848 1849 @Override 1850 public boolean onKeyDown(int keyCode, KeyEvent event) { 1851 switch (keyCode) { 1852 case KeyEvent.KEYCODE_DEL: 1853 if ((mMsgListAdapter != null) && mMsgListView.isFocused()) { 1854 Cursor cursor; 1855 try { 1856 cursor = (Cursor) mMsgListView.getSelectedItem(); 1857 } catch (ClassCastException e) { 1858 Log.e(TAG, "Unexpected ClassCastException.", e); 1859 return super.onKeyDown(keyCode, event); 1860 } 1861 1862 if (cursor != null) { 1863 DeleteMessageListener l = new DeleteMessageListener( 1864 cursor.getLong(COLUMN_ID), 1865 cursor.getString(COLUMN_MSG_TYPE)); 1866 confirmDeleteDialog(l, false); 1867 return true; 1868 } 1869 } 1870 break; 1871 case KeyEvent.KEYCODE_DPAD_CENTER: 1872 case KeyEvent.KEYCODE_ENTER: 1873 if (isPreparedForSending()) { 1874 confirmSendMessageIfNeeded(); 1875 return true; 1876 } 1877 break; 1878 case KeyEvent.KEYCODE_BACK: 1879 exitComposeMessageActivity(new Runnable() { 1880 public void run() { 1881 finish(); 1882 } 1883 }); 1884 return true; 1885 } 1886 1887 return super.onKeyDown(keyCode, event); 1888 } 1889 1890 private void exitComposeMessageActivity(final Runnable exit) { 1891 // If the message is empty, just quit -- finishing the 1892 // activity will cause an empty draft to be deleted. 1893 if (isEmptyMessage()) { 1894 exit.run(); 1895 return; 1896 } 1897 1898 if (!hasValidRecipient()) { 1899 MessageUtils.showDiscardDraftConfirmDialog(this, 1900 new DiscardDraftListener()); 1901 return; 1902 } 1903 1904 mToastForDraftSave = true; 1905 exit.run(); 1906 } 1907 1908 private void goToConversationList() { 1909 finish(); 1910 startActivity(new Intent(this, ConversationList.class)); 1911 } 1912 1913 private boolean isRecipientsEditorVisible() { 1914 return (null != mRecipientsEditor) 1915 && (View.VISIBLE == mRecipientsEditor.getVisibility()); 1916 } 1917 1918 private boolean isSubjectEditorVisible() { 1919 return (null != mSubjectTextEditor) 1920 && (View.VISIBLE == mSubjectTextEditor.getVisibility()); 1921 } 1922 1923 public void onAttachmentChanged(int newType, int oldType) { 1924 drawBottomPanel(newType); 1925 if (newType > AttachmentEditor.TEXT_ONLY) { 1926 updateState(HAS_ATTACHMENT, true); 1927 } else { 1928 convertMessageIfNeeded(HAS_ATTACHMENT, false); 1929 } 1930 updateSendButtonState(); 1931 } 1932 1933 // We don't want to show the "call" option unless there is only one 1934 // recipient and it's a phone number. 1935 private boolean isRecipientCallable() { 1936 return (mRecipientList.size() == 1 && !mRecipientList.containsEmail()); 1937 } 1938 1939 private void dialRecipient() { 1940 String number = mRecipientList.getSingleRecipientNumber(); 1941 Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + number)); 1942 startActivity(dialIntent); 1943 } 1944 1945 @Override 1946 public boolean onPrepareOptionsMenu(Menu menu) { 1947 menu.clear(); 1948 1949 if (isRecipientCallable()) { 1950 menu.add(0, MENU_CALL_RECIPIENT, 0, R.string.menu_call).setIcon( 1951 com.android.internal.R.drawable.ic_menu_call); 1952 } 1953 1954 // Only add the "View contact" menu item when there's a single recipient and that 1955 // recipient is someone in contacts. 1956 long personId = getPersonId(mRecipientList.getSingleRecipient()); 1957 if (personId > 0) { 1958 menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact).setIcon( 1959 R.drawable.ic_menu_contact); 1960 } 1961 1962 if (!MmsConfig.DISABLE_MMS) { 1963 if (!isSubjectEditorVisible()) { 1964 menu.add(0, MENU_ADD_SUBJECT, 0, R.string.add_subject).setIcon( 1965 com.android.internal.R.drawable.ic_menu_edit); 1966 } 1967 1968 if ((mAttachmentEditor == null) || (mAttachmentEditor.getAttachmentType() == AttachmentEditor.TEXT_ONLY)) { 1969 menu.add(0, MENU_ADD_ATTACHMENT, 0, R.string.add_attachment).setIcon( 1970 R.drawable.ic_menu_attachment); 1971 } 1972 } 1973 1974 if (isPreparedForSending()) { 1975 menu.add(0, MENU_SEND, 0, R.string.send).setIcon(android.R.drawable.ic_menu_send); 1976 } 1977 1978 menu.add(0, MENU_INSERT_SMILEY, 0, R.string.menu_insert_smiley).setIcon( 1979 com.android.internal.R.drawable.ic_menu_emoticons); 1980 1981 if (mMsgListAdapter.getCount() > 0) { 1982 // Removed search as part of b/1205708 1983 //menu.add(0, MENU_SEARCH, 0, R.string.menu_search).setIcon( 1984 // R.drawable.ic_menu_search); 1985 Cursor cursor = mMsgListAdapter.getCursor(); 1986 if ((null != cursor) && (cursor.getCount() > 0)) { 1987 menu.add(0, MENU_DELETE_THREAD, 0, R.string.delete_thread).setIcon( 1988 android.R.drawable.ic_menu_delete); 1989 } 1990 } else { 1991 menu.add(0, MENU_DISCARD, 0, R.string.discard).setIcon(android.R.drawable.ic_menu_delete); 1992 } 1993 1994 menu.add(0, MENU_CONVERSATION_LIST, 0, R.string.all_threads).setIcon( 1995 com.android.internal.R.drawable.ic_menu_friendslist); 1996 1997 buildAddAddressToContactMenuItem(menu); 1998 return true; 1999 } 2000 2001 private void buildAddAddressToContactMenuItem(Menu menu) { 2002 if (mRecipientList.hasValidRecipient()) { 2003 // Look for the first recipient we don't have a contact for and create a menu item to 2004 // add the number to contacts. 2005 Iterator<Recipient> recipientIterator = mRecipientList.iterator(); 2006 while (recipientIterator.hasNext()) { 2007 Recipient r = recipientIterator.next(); 2008 long personId = getPersonId(r); 2009 2010 if (personId <= 0) { 2011 Intent intent = ConversationList.createAddContactIntent(r.number); 2012 menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, R.string.menu_add_to_contacts) 2013 .setIcon(android.R.drawable.ic_menu_add) 2014 .setIntent(intent); 2015 break; 2016 } 2017 } 2018 } 2019 } 2020 2021 private void invalidateRecipientsInCache() { 2022 ContactInfoCache cache = ContactInfoCache.getInstance(); 2023 Iterator<Recipient> recipientIterator = mRecipientList.iterator(); 2024 while (recipientIterator.hasNext()) { 2025 Recipient r = recipientIterator.next(); 2026 cache.invalidateContact(r.number); 2027 } 2028 } 2029 2030 private long getPersonId(Recipient r) { 2031 // The recipient doesn't always have a person_id. This can happen when a user adds 2032 // a contact in the middle of an activity after the recipient has already been loaded. 2033 if (r == null) { 2034 return -1; 2035 } 2036 if (r.person_id > 0) { 2037 return r.person_id; 2038 } 2039 ContactInfoCache.CacheEntry entry = ContactInfoCache.getInstance() 2040 .getContactInfo(this, r.number); 2041 if (entry.person_id > 0) { 2042 return entry.person_id; 2043 } 2044 return -1; 2045 } 2046 2047 @Override 2048 public boolean onOptionsItemSelected(MenuItem item) { 2049 switch (item.getItemId()) { 2050 case MENU_ADD_SUBJECT: 2051 convertMessageIfNeeded(HAS_SUBJECT, true); 2052 showSubjectEditor(); 2053 mSubjectTextEditor.requestFocus(); 2054 break; 2055 case MENU_ADD_ATTACHMENT: 2056 // Launch the add-attachment list dialog 2057 showAddAttachmentDialog(); 2058 break; 2059 case MENU_DISCARD: 2060 discardTemporaryMessage(); 2061 finish(); 2062 break; 2063 case MENU_SEND: 2064 if (isPreparedForSending()) { 2065 confirmSendMessageIfNeeded(); 2066 } 2067 break; 2068 case MENU_SEARCH: 2069 onSearchRequested(); 2070 break; 2071 case MENU_DELETE_THREAD: 2072 DeleteMessageListener l = new DeleteMessageListener( 2073 getThreadUri(), true); 2074 confirmDeleteDialog(l, true); 2075 break; 2076 case MENU_CONVERSATION_LIST: 2077 exitComposeMessageActivity(new Runnable() { 2078 public void run() { 2079 goToConversationList(); 2080 } 2081 }); 2082 break; 2083 case MENU_CALL_RECIPIENT: 2084 dialRecipient(); 2085 break; 2086 case MENU_INSERT_SMILEY: 2087 showSmileyDialog(); 2088 break; 2089 case MENU_VIEW_CONTACT: 2090 // View the contact for the first (and only) recipient. 2091 long personId = getPersonId(mRecipientList.getSingleRecipient()); 2092 if (personId > 0) { 2093 viewContact(personId); 2094 } 2095 break; 2096 case MENU_ADD_ADDRESS_TO_CONTACTS: 2097 return false; // so the intent attached to the menu item will get launched. 2098 } 2099 2100 return true; 2101 } 2102 2103 private void addAttachment(int type) { 2104 switch (type) { 2105 case AttachmentTypeSelectorAdapter.ADD_IMAGE: 2106 MessageUtils.selectImage(this, REQUEST_CODE_ATTACH_IMAGE); 2107 break; 2108 2109 case AttachmentTypeSelectorAdapter.TAKE_PICTURE: { 2110 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 2111 startActivityForResult(intent, REQUEST_CODE_TAKE_PICTURE); 2112 } 2113 break; 2114 2115 case AttachmentTypeSelectorAdapter.ADD_VIDEO: 2116 MessageUtils.selectVideo(this, REQUEST_CODE_ATTACH_VIDEO); 2117 break; 2118 2119 case AttachmentTypeSelectorAdapter.RECORD_VIDEO: { 2120 Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); 2121 intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); 2122 startActivityForResult(intent, REQUEST_CODE_TAKE_VIDEO); 2123 } 2124 break; 2125 2126 case AttachmentTypeSelectorAdapter.ADD_SOUND: 2127 MessageUtils.selectAudio(this, REQUEST_CODE_ATTACH_SOUND); 2128 break; 2129 2130 case AttachmentTypeSelectorAdapter.RECORD_SOUND: 2131 MessageUtils.recordSound(this, REQUEST_CODE_RECORD_SOUND); 2132 break; 2133 2134 case AttachmentTypeSelectorAdapter.ADD_SLIDESHOW: { 2135 boolean wasSms = !requiresMms(); 2136 2137 // SlideshowEditActivity needs mMessageUri to work with. 2138 convertMessageIfNeeded(HAS_ATTACHMENT, true); 2139 2140 if (wasSms) { 2141 // If we are converting from SMS, make sure the SMS 2142 // text message gets imported into the first slide. 2143 TextModel text = mSlideshow.get(0).getText(); 2144 if (text != null) { 2145 text.setText(mMsgText); 2146 } 2147 } 2148 2149 Intent intent = new Intent(this, SlideshowEditActivity.class); 2150 intent.setData(mMessageUri); 2151 startActivityForResult(intent, REQUEST_CODE_CREATE_SLIDESHOW); 2152 } 2153 break; 2154 2155 default: 2156 break; 2157 } 2158 } 2159 2160 private void showAddAttachmentDialog() { 2161 AlertDialog.Builder builder = new AlertDialog.Builder(this); 2162 builder.setIcon(R.drawable.ic_dialog_attach); 2163 builder.setTitle(R.string.add_attachment); 2164 2165 AttachmentTypeSelectorAdapter adapter = new AttachmentTypeSelectorAdapter( 2166 this, AttachmentTypeSelectorAdapter.MODE_WITH_SLIDESHOW); 2167 2168 builder.setAdapter(adapter, new DialogInterface.OnClickListener() { 2169 public void onClick(DialogInterface dialog, int which) { 2170 addAttachment(which); 2171 } 2172 }); 2173 2174 builder.show(); 2175 } 2176 2177 @Override 2178 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 2179 if (LOCAL_LOGV) { 2180 Log.v(TAG, "onActivityResult: requestCode=" + requestCode 2181 + ", resultCode=" + resultCode + ", data=" + data); 2182 } 2183 mWaitingForSubActivity = false; // We're back! 2184 2185 if (resultCode != RESULT_OK) { 2186 // Make sure if there was an error that our message 2187 // type remains correct. 2188 convertMessageIfNeeded(HAS_ATTACHMENT, hasAttachment()); 2189 return; 2190 } 2191 2192 if (!requiresMms()) { 2193 convertMessage(true); 2194 } 2195 2196 switch(requestCode) { 2197 case REQUEST_CODE_CREATE_SLIDESHOW: 2198 try { 2199 // Refresh the slideshow model since it may be changed 2200 // by the slideshow editor. 2201 mSlideshow = SlideshowModel.createFromMessageUri(this, mMessageUri); 2202 } catch (MmsException e) { 2203 Log.e(TAG, "Failed to load slideshow from " + mMessageUri); 2204 Toast.makeText(this, getString(R.string.cannot_load_message), 2205 Toast.LENGTH_SHORT).show(); 2206 return; 2207 } 2208 2209 // Find the most suitable type for the attachment. 2210 int attachmentType = MessageUtils.getAttachmentType(mSlideshow); 2211 switch (attachmentType) { 2212 case AttachmentEditor.EMPTY: 2213 fixEmptySlideshow(mSlideshow); 2214 attachmentType = AttachmentEditor.TEXT_ONLY; 2215 // fall-through 2216 case AttachmentEditor.TEXT_ONLY: 2217 mAttachmentEditor.setAttachment(mSlideshow, attachmentType); 2218 convertMessageIfNeeded(HAS_ATTACHMENT, false); 2219 drawBottomPanel(attachmentType); 2220 return; 2221 default: 2222 mAttachmentEditor.setAttachment(mSlideshow, attachmentType); 2223 break; 2224 } 2225 2226 drawBottomPanel(attachmentType); 2227 break; 2228 2229 case REQUEST_CODE_TAKE_PICTURE: 2230 Bitmap bitmap = (Bitmap) data.getParcelableExtra("data"); 2231 2232 if (bitmap == null) { 2233 Toast.makeText(this, 2234 getResourcesString(R.string.failed_to_add_media, getPictureString()), 2235 Toast.LENGTH_SHORT).show(); 2236 return; 2237 } 2238 addImage(bitmap); 2239 break; 2240 2241 case REQUEST_CODE_ATTACH_IMAGE: 2242 addImage(data.getData()); 2243 break; 2244 2245 case REQUEST_CODE_TAKE_VIDEO: 2246 case REQUEST_CODE_ATTACH_VIDEO: 2247 addVideo(data.getData()); 2248 break; 2249 2250 case REQUEST_CODE_ATTACH_SOUND: 2251 case REQUEST_CODE_RECORD_SOUND: 2252 Uri uri; 2253 if (requestCode == REQUEST_CODE_ATTACH_SOUND) { 2254 uri = (Uri) data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); 2255 if (Settings.System.DEFAULT_RINGTONE_URI.equals(uri)) { 2256 uri = null; 2257 } 2258 } else { 2259 uri = data.getData(); 2260 } 2261 2262 if (uri == null) { 2263 convertMessageIfNeeded(HAS_ATTACHMENT, hasAttachment()); 2264 return; 2265 } 2266 2267 try { 2268 mAttachmentEditor.changeAudio(uri); 2269 mAttachmentEditor.setAttachment( 2270 mSlideshow, AttachmentEditor.AUDIO_ATTACHMENT); 2271 } catch (MmsException e) { 2272 Log.e(TAG, "add audio failed", e); 2273 Toast.makeText(this, 2274 getResourcesString(R.string.failed_to_add_media, getAudioString()), 2275 Toast.LENGTH_SHORT).show(); 2276 } catch (UnsupportContentTypeException e) { 2277 MessageUtils.showErrorDialog(ComposeMessageActivity.this, 2278 getResourcesString(R.string.unsupported_media_format, getAudioString()), 2279 getResourcesString(R.string.select_different_media, getAudioString())); 2280 } catch (ExceedMessageSizeException e) { 2281 MessageUtils.showErrorDialog(ComposeMessageActivity.this, 2282 getResourcesString(R.string.exceed_message_size_limitation), 2283 getResourcesString(R.string.failed_to_add_media, getAudioString())); 2284 } 2285 break; 2286 2287 default: 2288 // TODO 2289 break; 2290 } 2291 2292 // Make sure if there was an error that our message type remains correct. 2293 if (!requiresMms()) { 2294 convertMessage(false); 2295 } 2296 } 2297 2298 private void addImage(Bitmap bitmap) { 2299 try { 2300 addImage(MessageUtils.saveBitmapAsPart(this, mMessageUri, bitmap)); 2301 } catch (MmsException e) { 2302 handleAddImageFailure(e); 2303 } 2304 } 2305 2306 private final ResizeImageResultCallback mResizeImageCallback = new ResizeImageResultCallback() { 2307 public void onResizeResult(PduPart part) { 2308 Context context = ComposeMessageActivity.this; 2309 Resources r = context.getResources(); 2310 2311 if (part == null) { 2312 Toast.makeText(context, 2313 r.getString(R.string.failed_to_add_media, getPictureString()), 2314 Toast.LENGTH_SHORT).show(); 2315 return; 2316 } 2317 2318 convertMessageIfNeeded(HAS_ATTACHMENT, true); 2319 try { 2320 long messageId = ContentUris.parseId(mMessageUri); 2321 Uri newUri = mPersister.persistPart(part, messageId); 2322 mAttachmentEditor.changeImage(newUri); 2323 mAttachmentEditor.setAttachment( 2324 mSlideshow, AttachmentEditor.IMAGE_ATTACHMENT); 2325 } catch (MmsException e) { 2326 Toast.makeText(context, 2327 r.getString(R.string.failed_to_add_media, getPictureString()), 2328 Toast.LENGTH_SHORT).show(); 2329 } catch (UnsupportContentTypeException e) { 2330 MessageUtils.showErrorDialog(context, 2331 r.getString(R.string.unsupported_media_format, getPictureString()), 2332 r.getString(R.string.select_different_media, getPictureString())); 2333 } catch (ResolutionException e) { 2334 MessageUtils.showErrorDialog(context, 2335 r.getString(R.string.failed_to_resize_image), 2336 r.getString(R.string.resize_image_error_information)); 2337 } catch (ExceedMessageSizeException e) { 2338 MessageUtils.showErrorDialog(context, 2339 r.getString(R.string.exceed_message_size_limitation), 2340 r.getString(R.string.failed_to_add_media, getPictureString())); 2341 } 2342 } 2343 }; 2344 2345 private void addImage(Uri uri) { 2346 try { 2347 mAttachmentEditor.changeImage(uri); 2348 mAttachmentEditor.setAttachment( 2349 mSlideshow, AttachmentEditor.IMAGE_ATTACHMENT); 2350 } catch (MmsException e) { 2351 handleAddImageFailure(e); 2352 } catch (UnsupportContentTypeException e) { 2353 MessageUtils.showErrorDialog( 2354 ComposeMessageActivity.this, 2355 getResourcesString(R.string.unsupported_media_format, getPictureString()), 2356 getResourcesString(R.string.select_different_media, getPictureString())); 2357 } catch (ResolutionException e) { 2358 MessageUtils.resizeImageAsync(ComposeMessageActivity.this, 2359 uri, mAttachmentEditorHandler, mResizeImageCallback); 2360 } catch (ExceedMessageSizeException e) { 2361 MessageUtils.showErrorDialog( 2362 ComposeMessageActivity.this, 2363 getResourcesString(R.string.exceed_message_size_limitation), 2364 getResourcesString(R.string.failed_to_add_media, getPictureString())); 2365 } 2366 } 2367 2368 private void handleAddImageFailure(MmsException exception) { 2369 Log.e(TAG, "add image failed", exception); 2370 Toast.makeText( 2371 this, 2372 getResourcesString(R.string.failed_to_add_media, getPictureString()), 2373 Toast.LENGTH_SHORT).show(); 2374 } 2375 2376 private void addVideo(Uri uri) { 2377 try { 2378 mAttachmentEditor.changeVideo(uri); 2379 mAttachmentEditor.setAttachment( 2380 mSlideshow, AttachmentEditor.VIDEO_ATTACHMENT); 2381 } catch (MmsException e) { 2382 Log.e(TAG, "add video failed", e); 2383 Toast.makeText(this, 2384 getResourcesString(R.string.failed_to_add_media, getVideoString()), 2385 Toast.LENGTH_SHORT).show(); 2386 } catch (UnsupportContentTypeException e) { 2387 MessageUtils.showErrorDialog(ComposeMessageActivity.this, 2388 getResourcesString(R.string.unsupported_media_format, getVideoString()), 2389 getResourcesString(R.string.select_different_media, getVideoString())); 2390 } catch (ExceedMessageSizeException e) { 2391 MessageUtils.showErrorDialog(ComposeMessageActivity.this, 2392 getResourcesString(R.string.exceed_message_size_limitation), 2393 getResourcesString(R.string.failed_to_add_media, getVideoString())); 2394 } 2395 } 2396 2397 private boolean handleForwardedMessage() { 2398 // If this is a forwarded message, it will have an Intent extra 2399 // indicating so. If not, bail out. 2400 if (getIntent().getBooleanExtra("forwarded_message", false) == false) { 2401 return false; 2402 } 2403 2404 // If we are forwarding an MMS, mMessageUri will already be set. 2405 if (mMessageUri != null) { 2406 convertMessage(true); 2407 } 2408 2409 return true; 2410 } 2411 2412 private boolean handleSendIntent(Intent intent) { 2413 Bundle extras = intent.getExtras(); 2414 2415 if (!Intent.ACTION_SEND.equals(intent.getAction()) || (extras == null)) { 2416 return false; 2417 } 2418 2419 if (extras.containsKey(Intent.EXTRA_STREAM)) { 2420 Uri uri = (Uri)extras.getParcelable(Intent.EXTRA_STREAM); 2421 if (uri != null) { 2422 convertMessage(true); 2423 if (intent.getType().startsWith("image/")) { 2424 addImage(uri); 2425 } else if (intent.getType().startsWith("video/")) { 2426 addVideo(uri); 2427 } 2428 } 2429 return true; 2430 } else if (extras.containsKey(Intent.EXTRA_TEXT)) { 2431 mMsgText = extras.getString(Intent.EXTRA_TEXT); 2432 return true; 2433 } 2434 2435 return false; 2436 } 2437 2438 private String getAudioString() { 2439 return getResourcesString(R.string.type_audio); 2440 } 2441 2442 private String getPictureString() { 2443 return getResourcesString(R.string.type_picture); 2444 } 2445 2446 private String getVideoString() { 2447 return getResourcesString(R.string.type_video); 2448 } 2449 2450 private String getResourcesString(int id, String mediaName) { 2451 Resources r = getResources(); 2452 return r.getString(id, mediaName); 2453 } 2454 2455 private String getResourcesString(int id) { 2456 Resources r = getResources(); 2457 return r.getString(id); 2458 } 2459 2460 private void fixEmptySlideshow(SlideshowModel slideshow) { 2461 if (slideshow.size() == 0) { 2462 SlideModel slide = new SlideModel(slideshow); 2463 slideshow.add(slide); 2464 } 2465 2466 if (!slideshow.get(0).hasText()) { 2467 TextModel text = new TextModel( 2468 this, ContentType.TEXT_PLAIN, "text_0.txt", 2469 slideshow.getLayout().getTextRegion()); 2470 slideshow.get(0).add(text); 2471 } 2472 } 2473 2474 private void drawBottomPanel(int attachmentType) { 2475 // Reset the counter for text editor. 2476 resetCounter(); 2477 2478 switch (attachmentType) { 2479 case AttachmentEditor.EMPTY: 2480 throw new IllegalArgumentException( 2481 "Type of the attachment may not be EMPTY."); 2482 case AttachmentEditor.SLIDESHOW_ATTACHMENT: 2483 mBottomPanel.setVisibility(View.GONE); 2484 findViewById(R.id.attachment_editor).requestFocus(); 2485 return; 2486 default: 2487 mBottomPanel.setVisibility(View.VISIBLE); 2488 CharSequence text = null; 2489 if (requiresMms()) { 2490 TextModel tm = mSlideshow.get(0).getText(); 2491 if (tm != null) { 2492 text = tm.getText(); 2493 } 2494 } else { 2495 text = mMsgText; 2496 } 2497 2498 if ((text != null) && !text.equals(mTextEditor.getText().toString())) { 2499 mTextEditor.setText(text); 2500 } 2501 } 2502 } 2503 2504 //========================================================== 2505 // Interface methods 2506 //========================================================== 2507 2508 public void onClick(View v) { 2509 if ((v == mSendButton) && isPreparedForSending()) { 2510 confirmSendMessageIfNeeded(); 2511 } 2512 } 2513 2514 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 2515 if (event != null) { 2516 if (!event.isShiftPressed()) { 2517 if (isPreparedForSending()) { 2518 sendMessage(); 2519 } 2520 return true; 2521 } 2522 return false; 2523 } 2524 2525 if (isPreparedForSending()) { 2526 confirmSendMessageIfNeeded(); 2527 } 2528 return true; 2529 } 2530 2531 private final TextWatcher mTextEditorWatcher = new TextWatcher() { 2532 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 2533 } 2534 2535 public void onTextChanged(CharSequence s, int start, int before, int count) { 2536 // This is a workaround for bug 1609057. Since onUserInteraction() is 2537 // not called when the user touches the soft keyboard, we pretend it was 2538 // called when textfields changes. This should be removed when the bug 2539 // is fixed. 2540 onUserInteraction(); 2541 2542 if (requiresMms()) { 2543 // Update the content of the text model. 2544 TextModel text = mSlideshow.get(0).getText(); 2545 if (text == null) { 2546 text = new TextModel( 2547 ComposeMessageActivity.this, 2548 ContentType.TEXT_PLAIN, 2549 "text_0.txt", 2550 mSlideshow.getLayout().getTextRegion() 2551 ); 2552 mSlideshow.get(0).add(text); 2553 } 2554 2555 text.setText(s); 2556 } else { 2557 mMsgText = s; 2558 } 2559 2560 updateSendButtonState(); 2561 2562 updateCounter(s, start, before, count); 2563 } 2564 2565 public void afterTextChanged(Editable s) { 2566 } 2567 }; 2568 2569 //========================================================== 2570 // Private methods 2571 //========================================================== 2572 2573 /** 2574 * Initialize all UI elements from resources. 2575 */ 2576 private void initResourceRefs() { 2577 mMsgListView = (MessageListView) findViewById(R.id.history); 2578 mMsgListView.setDivider(null); // no divider so we look like IM conversation. 2579 mBottomPanel = findViewById(R.id.bottom_panel); 2580 mTextEditor = (EditText) findViewById(R.id.embedded_text_editor); 2581 mTextEditor.setOnEditorActionListener(this); 2582 mTextEditor.addTextChangedListener(mTextEditorWatcher); 2583 mTextCounter = (TextView) findViewById(R.id.text_counter); 2584 mSendButton = (Button) findViewById(R.id.send_button); 2585 mSendButton.setOnClickListener(this); 2586 mTopPanel = findViewById(R.id.recipients_subject_linear); 2587 } 2588 2589 private void confirmDeleteDialog(OnClickListener listener, boolean allMessages) { 2590 AlertDialog.Builder builder = new AlertDialog.Builder(this); 2591 builder.setTitle(R.string.confirm_dialog_title); 2592 builder.setIcon(android.R.drawable.ic_dialog_alert); 2593 builder.setCancelable(true); 2594 builder.setMessage(allMessages 2595 ? R.string.confirm_delete_conversation 2596 : R.string.confirm_delete_message); 2597 builder.setPositiveButton(R.string.yes, listener); 2598 builder.setNegativeButton(R.string.no, null); 2599 builder.show(); 2600 } 2601 2602 void undeliveredMessageDialog(long date) { 2603 String body; 2604 LinearLayout dialog = (LinearLayout) LayoutInflater.from(this).inflate( 2605 R.layout.retry_sending_dialog, null); 2606 2607 if (date >= 0) { 2608 body = getString(R.string.undelivered_msg_dialog_body, 2609 MessageUtils.formatTimeStampString(this, date)); 2610 } else { 2611 // FIXME: we can not get sms retry time. 2612 body = getString(R.string.undelivered_sms_dialog_body); 2613 } 2614 2615 ((TextView) dialog.findViewById(R.id.body_text_view)).setText(body); 2616 2617 Toast undeliveredDialog = new Toast(this); 2618 undeliveredDialog.setView(dialog); 2619 undeliveredDialog.setDuration(Toast.LENGTH_LONG); 2620 undeliveredDialog.show(); 2621 } 2622 2623 private String deriveAddress(Intent intent) { 2624 Uri recipientUri = intent.getData(); 2625 return (recipientUri == null) 2626 ? null : recipientUri.getSchemeSpecificPart(); 2627 } 2628 2629 private Uri getThreadUri() { 2630 return ContentUris.withAppendedId(Threads.CONTENT_URI, mThreadId); 2631 } 2632 2633 private void startMsgListQuery() { 2634 if (mThreadId <= 0) { 2635 return; 2636 } 2637 2638 // Cancel any pending queries 2639 mBackgroundQueryHandler.cancelOperation(MESSAGE_LIST_QUERY_TOKEN); 2640 try { 2641 // Kick off the new query 2642 mBackgroundQueryHandler.startQuery( 2643 MESSAGE_LIST_QUERY_TOKEN, null, getThreadUri(), 2644 PROJECTION, null, null, null); 2645 } catch (SQLiteException e) { 2646 SqliteWrapper.checkSQLiteException(this, e); 2647 } 2648 } 2649 2650 private void initMessageList() { 2651 if (mMsgListAdapter != null) { 2652 return; 2653 } 2654 2655 // Initialize the list adapter with a null cursor. 2656 mMsgListAdapter = new MessageListAdapter( 2657 this, null, mMsgListView, true, getThreadType()); 2658 mMsgListAdapter.setOnDataSetChangedListener(mDataSetChangedListener); 2659 mMsgListAdapter.setMsgListItemHandler(mMessageListItemHandler); 2660 mMsgListView.setAdapter(mMsgListAdapter); 2661 mMsgListView.setItemsCanFocus(false); 2662 mMsgListView.setVisibility(View.VISIBLE); 2663 mMsgListView.setOnCreateContextMenuListener(mMsgListMenuCreateListener); 2664 mMsgListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 2665 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 2666 ((MessageListItem) view).onMessageListItemClick(); 2667 } 2668 }); 2669 } 2670 2671 private void loadDraft() { 2672 // If we have no associated thread ID, there can't be a draft. 2673 if (mThreadId <= 0) { 2674 return; 2675 } 2676 2677 // If we already have text, don't stomp on it with the draft. 2678 if (!TextUtils.isEmpty(mMsgText)) { 2679 return; 2680 } 2681 2682 // If we know there is no draft, don't bother to look for one. 2683 if (!DraftCache.getInstance().hasDraft(mThreadId)) { 2684 return; 2685 } 2686 2687 // Try to load an SMS draft; if one does not exist, 2688 // load an MMS draft. 2689 mMsgText = readTemporarySmsMessage(mThreadId); 2690 if (TextUtils.isEmpty(mMsgText)) { 2691 if (readTemporaryMmsMessage(mThreadId)) { 2692 convertMessage(true); 2693 } else { 2694 Log.e(TAG, "no SMS or MMS drafts in thread " + mThreadId); 2695 return; 2696 } 2697 } 2698 } 2699 2700 private void asyncDelete(Uri uri, String selection, String[] selectionArgs) { 2701 if (LOCAL_LOGV) Log.v(TAG, "asyncDelete " + uri); 2702 mBackgroundQueryHandler.startDelete(0, null, uri, selection, selectionArgs); 2703 } 2704 2705 private void saveDraft() { 2706 // Convert back to SMS if we were only in MMS mode because there was 2707 // a subject and it is empty. 2708 removeSubjectIfEmpty(); 2709 2710 // If we are in MMS mode but mMessageUri is null, the message has already 2711 // been discarded. Just bail out early. 2712 if (requiresMms() && mMessageUri == null && !mWaitingForSubActivity) { 2713 return; 2714 } 2715 2716 // Throw the message out if it's empty, unless we're in the middle 2717 // of creating a slideshow or some other subactivity -- don't discard 2718 // the draft behind the subactivity's back. 2719 if (isEmptyMessage() && !mWaitingForSubActivity) { 2720 discardTemporaryMessage(); 2721 DraftCache.getInstance().setDraftState(mThreadId, false); 2722 return; 2723 } 2724 2725 // If the user hasn't typed in a recipient and has managed to 2726 // get away from us (e.g. by pressing HOME), we don't have any 2727 // choice but to save an anonymous draft. Fall through to the 2728 // normal case but set up an anonymous thread first. 2729 if (!hasValidRecipient()) { 2730 setThreadId(getOrCreateThreadId(new String[] {})); 2731 } 2732 2733 boolean savedAsDraft = false; 2734 if (requiresMms()) { 2735 if (isEmptyMms() && !mWaitingForSubActivity) { 2736 asyncDelete(mMessageUri, null, null); 2737 } else { 2738 asyncUpdateTemporaryMmsMessage(mRecipientList.getToNumbers()); 2739 savedAsDraft = true; 2740 } 2741 } else { 2742 if (isEmptySms()) { 2743 asyncDeleteTemporarySmsMessage(mThreadId); 2744 } else { 2745 asyncUpdateTemporarySmsMessage(mRecipientList.getToNumbers(), 2746 mMsgText.toString()); 2747 savedAsDraft = true; 2748 } 2749 } 2750 2751 DraftCache.getInstance().setDraftState(mThreadId, savedAsDraft); 2752 2753 if (mToastForDraftSave && savedAsDraft) { 2754 Toast.makeText(this, R.string.message_saved_as_draft, 2755 Toast.LENGTH_SHORT).show(); 2756 } 2757 } 2758 2759 private static final String[] MMS_DRAFT_PROJECTION = { 2760 Mms._ID, // 0 2761 Mms.SUBJECT // 1 2762 }; 2763 2764 private static final int MMS_ID_INDEX = 0; 2765 private static final int MMS_SUBJECT_INDEX = 1; 2766 2767 private boolean readTemporaryMmsMessage(long threadId) { 2768 Cursor cursor; 2769 2770 if (mMessageUri != null) { 2771 if (LOCAL_LOGV) { 2772 Log.v(TAG, "readTemporaryMmsMessage: already has message url " + mMessageUri); 2773 } 2774 return true; 2775 } 2776 2777 final String selection = Mms.THREAD_ID + " = " + threadId; 2778 cursor = SqliteWrapper.query(this, mContentResolver, 2779 Mms.Draft.CONTENT_URI, MMS_DRAFT_PROJECTION, 2780 selection, null, null); 2781 2782 try { 2783 if ((cursor.getCount() == 1) && cursor.moveToFirst()) { 2784 mMessageUri = ContentUris.withAppendedId(Mms.Draft.CONTENT_URI, 2785 cursor.getLong(MMS_ID_INDEX)); 2786 2787 mSubject = cursor.getString(MMS_SUBJECT_INDEX); 2788 if (!TextUtils.isEmpty(mSubject)) { 2789 updateState(HAS_SUBJECT, true); 2790 } 2791 return true; 2792 } 2793 } finally { 2794 cursor.close(); 2795 } 2796 2797 return false; 2798 } 2799 2800 2801 private Uri createTemporaryMmsMessage() throws MmsException { 2802 SendReq sendReq = new SendReq(); 2803 fillMessageHeaders(sendReq); 2804 PduBody pb = mSlideshow.toPduBody(); 2805 sendReq.setBody(pb); 2806 Uri res = mPersister.persist(sendReq, Mms.Draft.CONTENT_URI); 2807 mSlideshow.sync(pb); 2808 return res; 2809 } 2810 2811 private void asyncUpdateTemporaryMmsMessage(final String[] dests) { 2812 // PduPersister makes database calls and is known to ANR. Do the work on a 2813 // background thread. 2814 final SendReq sendReq = new SendReq(); 2815 fillMessageHeaders(sendReq); 2816 2817 new Thread(new Runnable() { 2818 public void run() { 2819 setThreadId(getOrCreateThreadId(dests)); 2820 DraftCache.getInstance().setDraftState(mThreadId, true); 2821 updateTemporaryMmsMessage(mMessageUri, mPersister, 2822 mSlideshow, sendReq); 2823 } 2824 }).start(); 2825 2826 // Be paranoid and delete any SMS drafts that might be lying around. 2827 asyncDeleteTemporarySmsMessage(mThreadId); 2828 } 2829 2830 public static void updateTemporaryMmsMessage(Uri uri, PduPersister persister, 2831 SlideshowModel slideshow, SendReq sendReq) { 2832 persister.updateHeaders(uri, sendReq); 2833 final PduBody pb = slideshow.toPduBody(); 2834 2835 try { 2836 persister.updateParts(uri, pb); 2837 } catch (MmsException e) { 2838 Log.e(TAG, "updateTemporaryMmsMessage: cannot update message " + uri); 2839 } 2840 2841 slideshow.sync(pb); 2842 } 2843 2844 private static final String[] SMS_BODY_PROJECTION = { Sms._ID, Sms.BODY }; 2845 private static final String SMS_DRAFT_WHERE = Sms.TYPE + "=" + Sms.MESSAGE_TYPE_DRAFT; 2846 2847 /** 2848 * Reads a draft message for the given thread ID from the database, 2849 * if there is one, deletes it from the database, and returns it. 2850 * @return The draft message or an empty string. 2851 */ 2852 private String readTemporarySmsMessage(long thread_id) { 2853 // If it's an invalid thread, don't bother. 2854 if (thread_id <= 0) { 2855 return ""; 2856 } 2857 2858 Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id); 2859 String body = ""; 2860 2861 Cursor c = SqliteWrapper.query(this, mContentResolver, 2862 thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null); 2863 2864 if (c != null) { 2865 try { 2866 if (c.moveToFirst()) { 2867 body = c.getString(1); 2868 } 2869 } finally { 2870 c.close(); 2871 } 2872 } 2873 2874 // Clean out drafts for this thread -- if the recipient set changes, 2875 // we will lose track of the original draft and be unable to delete 2876 // it later. The message will be re-saved if necessary upon exit of 2877 // the activity. 2878 SqliteWrapper.delete(this, mContentResolver, thread_uri, SMS_DRAFT_WHERE, null); 2879 2880 return body; 2881 } 2882 2883 private void asyncUpdateTemporarySmsMessage(final String[] dests, final String contents) { 2884 new Thread(new Runnable() { 2885 public void run() { 2886 setThreadId(getOrCreateThreadId(dests)); 2887 DraftCache.getInstance().setDraftState(mThreadId, true); 2888 updateTemporarySmsMessage(mThreadId, contents); 2889 } 2890 }).start(); 2891 } 2892 2893 private void updateTemporarySmsMessage(long thread_id, String contents) { 2894 // If we don't have a valid thread, there's nothing to do. 2895 if (thread_id <= 0) { 2896 return; 2897 } 2898 2899 // Don't bother saving an empty message. 2900 if (TextUtils.isEmpty(contents)) { 2901 // But delete the old temporary message if it's there. 2902 deleteTemporarySmsMessage(thread_id); 2903 return; 2904 } 2905 2906 Uri thread_uri = ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, thread_id); 2907 Cursor c = SqliteWrapper.query(this, mContentResolver, 2908 thread_uri, SMS_BODY_PROJECTION, SMS_DRAFT_WHERE, null, null); 2909 2910 try { 2911 if (c.moveToFirst()) { 2912 ContentValues values = new ContentValues(1); 2913 values.put(Sms.BODY, contents); 2914 SqliteWrapper.update(this, mContentResolver, thread_uri, values, 2915 SMS_DRAFT_WHERE, null); 2916 } else { 2917 ContentValues values = new ContentValues(3); 2918 values.put(Sms.THREAD_ID, thread_id); 2919 values.put(Sms.BODY, contents); 2920 values.put(Sms.TYPE, Sms.MESSAGE_TYPE_DRAFT); 2921 SqliteWrapper.insert(this, mContentResolver, Sms.CONTENT_URI, values); 2922 asyncDeleteTemporaryMmsMessage(thread_id); 2923 } 2924 } finally { 2925 c.close(); 2926 } 2927 } 2928 2929 private void asyncDeleteTemporarySmsMessage(long threadId) { 2930 if (threadId > 0) { 2931 asyncDelete(ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId), 2932 SMS_DRAFT_WHERE, null); 2933 } 2934 } 2935 2936 private void deleteTemporarySmsMessage(long threadId) { 2937 SqliteWrapper.delete(this, mContentResolver, 2938 ContentUris.withAppendedId(Sms.Conversations.CONTENT_URI, threadId), 2939 SMS_DRAFT_WHERE, null); 2940 } 2941 2942 private void asyncDeleteTemporaryMmsMessage(long threadId) { 2943 final String where = Mms.THREAD_ID + " = " + threadId; 2944 asyncDelete(Mms.Draft.CONTENT_URI, where, null); 2945 } 2946 2947 private void abandonDraftsAndFinish() { 2948 // If we are in MMS mode, first convert the message to SMS, 2949 // which will cause the MMS draft to be deleted. 2950 if (mMessageUri != null) { 2951 convertMessage(false); 2952 } 2953 // Now get rid of the SMS text to inhibit saving of a draft. 2954 mMsgText = ""; 2955 finish(); 2956 } 2957 2958 private String[] fillMessageHeaders(SendReq sendReq) { 2959 // Set the numbers in the 'TO' field. 2960 String[] dests = mRecipientList.getToNumbers(); 2961 EncodedStringValue[] encodedNumbers = encodeStrings(dests); 2962 if (encodedNumbers != null) { 2963 sendReq.setTo(encodedNumbers); 2964 } 2965 2966 // Set the numbers in the 'BCC' field. 2967 encodedNumbers = encodeStrings(mRecipientList.getBccNumbers()); 2968 if (encodedNumbers != null) { 2969 sendReq.setBcc(encodedNumbers); 2970 } 2971 2972 // Set the subject of the message. 2973 String subject = (mSubjectTextEditor == null) 2974 ? "" : mSubjectTextEditor.getText().toString(); 2975 sendReq.setSubject(new EncodedStringValue(subject)); 2976 2977 // Update the 'date' field of the message before sending it. 2978 sendReq.setDate(System.currentTimeMillis() / 1000L); 2979 2980 return dests; 2981 } 2982 2983 private boolean hasRecipient() { 2984 return hasValidRecipient() || hasInvalidRecipient(); 2985 } 2986 2987 private boolean hasValidRecipient() { 2988 // If someone is in the recipient list, or if a valid recipient is 2989 // currently in the recipients editor, we have recipients. 2990 return (mRecipientList.hasValidRecipient()) 2991 || ((mRecipientsEditor != null) 2992 && Recipient.isValid(mRecipientsEditor.getText().toString())); 2993 } 2994 2995 private boolean hasInvalidRecipient() { 2996 return (mRecipientList.hasInvalidRecipient()) 2997 || ((mRecipientsEditor != null) 2998 && !TextUtils.isEmpty(mRecipientsEditor.getText().toString()) 2999 && !Recipient.isValid(mRecipientsEditor.getText().toString())); 3000 } 3001 3002 private boolean hasText() { 3003 return mTextEditor.length() > 0; 3004 } 3005 3006 private boolean hasSubject() { 3007 return (null != mSubjectTextEditor) 3008 && !TextUtils.isEmpty(mSubjectTextEditor.getText().toString()); 3009 } 3010 3011 private boolean isPreparedForSending() { 3012 return hasRecipient() && (hasAttachment() || hasText()); 3013 } 3014 3015 private long getOrCreateThreadId(String[] numbers) { 3016 HashSet<String> recipients = new HashSet<String>(); 3017 recipients.addAll(Arrays.asList(numbers)); 3018 return Threads.getOrCreateThreadId(this, recipients); 3019 } 3020 3021 3022 private void sendMessage() { 3023 // Need this for both SMS and MMS. 3024 final String[] dests = mRecipientList.getToNumbers(); 3025 3026 // removeSubjectIfEmpty will convert a message that is solely an MMS 3027 // message because it has an empty subject back into an SMS message. 3028 // It doesn't notify the user of the conversion. 3029 removeSubjectIfEmpty(); 3030 if (requiresMms()) { 3031 // Make local copies of the bits we need for sending a message, 3032 // because we will be doing it off of the main thread, which will 3033 // immediately continue on to resetting some of this state. 3034 final Uri mmsUri = mMessageUri; 3035 final PduPersister persister = mPersister; 3036 final SlideshowModel slideshow = mSlideshow; 3037 final SendReq sendReq = new SendReq(); 3038 fillMessageHeaders(sendReq); 3039 3040 // Make sure the text in slide 0 is no longer holding onto a reference to the text 3041 // in the message text box. 3042 slideshow.prepareForSend(); 3043 3044 // Do the dirty work of sending the message off of the main UI thread. 3045 new Thread(new Runnable() { 3046 public void run() { 3047 sendMmsWorker(dests, mmsUri, persister, slideshow, sendReq); 3048 } 3049 }).start(); 3050 } else { 3051 // Same rules apply as above. 3052 final String msgText = mMsgText.toString(); 3053 new Thread(new Runnable() { 3054 public void run() { 3055 sendSmsWorker(dests, msgText); 3056 } 3057 }).start(); 3058 } 3059 3060 if (mExitOnSent) { 3061 // If we are supposed to exit after a message is sent, 3062 // clear out the text and URIs to inhibit saving of any 3063 // drafts and call finish(). 3064 mMsgText = ""; 3065 mMessageUri = null; 3066 finish(); 3067 } else { 3068 // Otherwise, reset the UI to be ready for the next message. 3069 resetMessage(); 3070 } 3071 } 3072 3073 /** 3074 * Do the actual work of sending a message. Runs outside of the main thread. 3075 */ 3076 private void sendSmsWorker(String[] dests, String msgText) { 3077 // Make sure we are still using the correct thread ID for our 3078 // recipient set. 3079 long threadId = getOrCreateThreadId(dests); 3080 3081 MessageSender sender = new SmsMessageSender(this, dests, msgText, threadId); 3082 try { 3083 sender.sendMessage(threadId); 3084 setThreadId(threadId); 3085 startMsgListQuery(); 3086 } catch (Exception e) { 3087 Log.e(TAG, "Failed to send SMS message, threadId=" + threadId, e); 3088 } 3089 } 3090 3091 private void sendMmsWorker(String[] dests, Uri mmsUri, PduPersister persister, 3092 SlideshowModel slideshow, SendReq sendReq) { 3093 // Make sure we are still using the correct thread ID for our 3094 // recipient set. 3095 long threadId = getOrCreateThreadId(dests); 3096 3097 if (LOCAL_LOGV) Log.v(TAG, "sendMmsWorker: update temporary MMS message " + mmsUri); 3098 3099 // Sync the MMS message in progress to disk. 3100 updateTemporaryMmsMessage(mmsUri, persister, slideshow, sendReq); 3101 // Be paranoid and clean any draft SMS up. 3102 deleteTemporarySmsMessage(threadId); 3103 3104 MessageSender sender = new MmsMessageSender(this, mmsUri); 3105 try { 3106 if (!sender.sendMessage(threadId)) { 3107 // The message was sent through SMS protocol, we should 3108 // delete the copy which was previously saved in MMS drafts. 3109 SqliteWrapper.delete(this, mContentResolver, mmsUri, null, null); 3110 } 3111 3112 setThreadId(threadId); 3113 startMsgListQuery(); 3114 } catch (Exception e) { 3115 Log.e(TAG, "Failed to send message: " + mmsUri + ", threadId=" + threadId, e); 3116 } 3117 } 3118 3119 private void resetMessage() { 3120 // Make the attachment editor hide its view before we destroy it. 3121 if (mAttachmentEditor != null) { 3122 mAttachmentEditor.hideView(); 3123 } 3124 3125 // Focus to the text editor. 3126 mTextEditor.requestFocus(); 3127 3128 // We have to remove the text change listener while the text editor gets cleared and 3129 // we subsequently turn the message back into SMS. When the listener is listening while 3130 // doing the clearing, it's fighting to update its counts and itself try and turn 3131 // the message one way or the other. 3132 mTextEditor.removeTextChangedListener(mTextEditorWatcher); 3133 3134 // RECIPIENTS_REQUIRE_MMS is the only state flag that is valid 3135 // when starting a new message, so preserve only that. 3136 mMessageState &= RECIPIENTS_REQUIRE_MMS; 3137 3138 // Clear the text box. 3139 TextKeyListener.clear(mTextEditor.getText()); 3140 3141 // Clear out the slideshow and message URI. New ones will be 3142 // created if we are starting a new message as MMS. 3143 mSlideshow = null; 3144 mMessageUri = null; 3145 3146 // Empty out text. 3147 mMsgText = ""; 3148 3149 // Convert back to SMS if necessary, or if we still need to 3150 // be in MMS mode, reset the MMS components. 3151 if (!requiresMms()) { 3152 // Start a new message as an SMS. 3153 convertMessage(false); 3154 } else { 3155 // Start a new message as an MMS. 3156 resetMmsComponents(); 3157 } 3158 3159 drawBottomPanel(AttachmentEditor.TEXT_ONLY); 3160 3161 // "Or not", in this case. 3162 updateSendButtonState(); 3163 3164 // Hide the recipients editor. 3165 if (mRecipientsEditor != null) { 3166 mRecipientsEditor.setVisibility(View.GONE); 3167 hideTopPanelIfNecessary(); 3168 } 3169 3170 // Our changes are done. Let the listener respond to text changes once again. 3171 mTextEditor.addTextChangedListener(mTextEditorWatcher); 3172 3173 // Close the soft on-screen keyboard if we're in landscape mode so the user can see the 3174 // conversation. 3175 if (mIsLandscape) { 3176 InputMethodManager inputMethodManager = 3177 (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); 3178 3179 inputMethodManager.hideSoftInputFromWindow(mTextEditor.getWindowToken(), 0); 3180 } 3181 } 3182 3183 private void updateSendButtonState() { 3184 boolean enable = false; 3185 if (isPreparedForSending()) { 3186 // When the type of attachment is slideshow, we should 3187 // also hide the 'Send' button since the slideshow view 3188 // already has a 'Send' button embedded. 3189 if ((mAttachmentEditor == null) || 3190 (mAttachmentEditor.getAttachmentType() != AttachmentEditor.SLIDESHOW_ATTACHMENT)) { 3191 enable = true; 3192 } else { 3193 mAttachmentEditor.setCanSend(true); 3194 } 3195 } else if (null != mAttachmentEditor){ 3196 mAttachmentEditor.setCanSend(false); 3197 } 3198 3199 mSendButton.setEnabled(enable); 3200 mSendButton.setFocusable(enable); 3201 } 3202 3203 private long getMessageDate(Uri uri) { 3204 if (uri != null) { 3205 Cursor cursor = SqliteWrapper.query(this, mContentResolver, 3206 uri, new String[] { Mms.DATE }, null, null, null); 3207 if (cursor != null) { 3208 try { 3209 if ((cursor.getCount() == 1) && cursor.moveToFirst()) { 3210 return cursor.getLong(0) * 1000L; 3211 } 3212 } finally { 3213 cursor.close(); 3214 } 3215 } 3216 } 3217 return NO_DATE_FOR_DIALOG; 3218 } 3219 3220 private void setSubjectFromIntent(Intent intent) { 3221 String subject = intent.getStringExtra("subject"); 3222 if ( !TextUtils.isEmpty(subject) ) { 3223 mSubject = subject; 3224 } 3225 } 3226 3227 private void setThreadId(long threadId) { 3228 mThreadId = threadId; 3229 } 3230 3231 private void initActivityState(Bundle savedInstanceState, Intent intent) { 3232 if (savedInstanceState != null) { 3233 setThreadId(savedInstanceState.getLong("thread_id", 0)); 3234 mMessageUri = (Uri) savedInstanceState.getParcelable("msg_uri"); 3235 mExternalAddress = savedInstanceState.getString("address"); 3236 mExitOnSent = savedInstanceState.getBoolean("exit_on_sent", false); 3237 mSubject = savedInstanceState.getString("subject"); 3238 mMsgText = savedInstanceState.getString("sms_body"); 3239 } else { 3240 setThreadId(intent.getLongExtra("thread_id", 0)); 3241 mMessageUri = (Uri) intent.getParcelableExtra("msg_uri"); 3242 if ((mMessageUri == null) && (mThreadId == 0)) { 3243 // If we haven't been given a thread id or a URI in the extras, 3244 // get it out of the intent. 3245 Uri uri = intent.getData(); 3246 if ((uri != null) && (uri.getPathSegments().size() >= 2)) { 3247 try { 3248 setThreadId(Long.parseLong(uri.getPathSegments().get(1))); 3249 } catch (NumberFormatException exception) { 3250 Log.e(TAG, "Thread ID must be a Long."); 3251 } 3252 } 3253 } 3254 mExternalAddress = intent.getStringExtra("address"); 3255 mExitOnSent = intent.getBooleanExtra("exit_on_sent", false); 3256 mMsgText = intent.getStringExtra("sms_body"); 3257 3258 setSubjectFromIntent(intent); 3259 } 3260 3261 if (!TextUtils.isEmpty(mSubject)) { 3262 updateState(HAS_SUBJECT, true); 3263 } 3264 3265 // If there was not a body already, start with a blank one. 3266 if (mMsgText == null) { 3267 mMsgText = ""; 3268 } 3269 3270 if (mExternalAddress == null) { 3271 if (mThreadId > 0L) { 3272 mExternalAddress = MessageUtils.getAddressByThreadId(this, mThreadId); 3273 } else { 3274 mExternalAddress = deriveAddress(intent); 3275 // Even if we end up creating a thread here and the user 3276 // discards the message, we will clean it up later when we 3277 // delete obsolete threads. 3278 if (!TextUtils.isEmpty(mExternalAddress)) { 3279 setThreadId(getOrCreateThreadId(new String[] { mExternalAddress })); 3280 } 3281 } 3282 } 3283 } 3284 3285 private int getThreadType() { 3286 boolean isMms = (mMessageUri != null) || requiresMms(); 3287 3288 return (!isMms 3289 && (mRecipientList != null) 3290 && (mRecipientList.size() > 1)) 3291 ? Threads.BROADCAST_THREAD 3292 : Threads.COMMON_THREAD; 3293 } 3294 3295 private void updateWindowTitle() { 3296 StringBuilder sb = new StringBuilder(); 3297 Iterator<Recipient> iter = mRecipientList.iterator(); 3298 while (iter.hasNext()) { 3299 Recipient r = iter.next(); 3300 sb.append(r.nameAndNumber).append(", "); 3301 } 3302 3303 ContactInfoCache cache = ContactInfoCache.getInstance(); 3304 String[] values = mRecipientList.getBccNumbers(); 3305 if (values.length > 0) { 3306 sb.append("Bcc: "); 3307 for (String v : values) { 3308 sb.append(cache.getContactName(this, v)).append(", "); 3309 } 3310 } 3311 3312 if (sb.length() > 0) { 3313 // Delete the trailing ", " characters. 3314 int tail = sb.length() - 2; 3315 setTitle(sb.delete(tail, tail + 2).toString()); 3316 } else { 3317 setTitle(getString(R.string.compose_title)); 3318 } 3319 } 3320 3321 private void initFocus() { 3322 if (!mIsKeyboardOpen) { 3323 return; 3324 } 3325 3326 // If the recipients editor is visible, there is nothing in it, 3327 // and the text editor is not already focused, focus the 3328 // recipients editor. 3329 if (isRecipientsEditorVisible() && TextUtils.isEmpty(mRecipientsEditor.getText()) 3330 && !mTextEditor.isFocused()) { 3331 mRecipientsEditor.requestFocus(); 3332 return; 3333 } 3334 3335 // If we decided not to focus the recipients editor, focus the text editor. 3336 mTextEditor.requestFocus(); 3337 } 3338 3339 private final MessageListAdapter.OnDataSetChangedListener 3340 mDataSetChangedListener = new MessageListAdapter.OnDataSetChangedListener() { 3341 public void onDataSetChanged(MessageListAdapter adapter) { 3342 mPossiblePendingNotification = true; 3343 } 3344 }; 3345 3346 private void checkPendingNotification() { 3347 if (mPossiblePendingNotification && hasWindowFocus()) { 3348 markAsRead(mThreadId); 3349 mPossiblePendingNotification = false; 3350 } 3351 } 3352 3353 private void markAsRead(long threadId) { 3354 if (threadId <= 0) { 3355 return; 3356 } 3357 3358 Uri threadUri = ContentUris.withAppendedId(Threads.CONTENT_URI, threadId); 3359 ContentValues values = new ContentValues(1); 3360 values.put("read", 1); 3361 String where = "read = 0"; 3362 3363 mBackgroundQueryHandler.startUpdate(MARK_AS_READ_TOKEN, null, 3364 threadUri, values, where, null); 3365 } 3366 3367 private final class BackgroundQueryHandler extends AsyncQueryHandler { 3368 public BackgroundQueryHandler(ContentResolver contentResolver) { 3369 super(contentResolver); 3370 } 3371 3372 @Override 3373 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 3374 switch(token) { 3375 case MESSAGE_LIST_QUERY_TOKEN: 3376 mMsgListAdapter.changeCursor(cursor); 3377 3378 // Once we have completed the query for the message history, if 3379 // there is nothing in the cursor and we are not composing a new 3380 // message, we must be editing a draft in a new conversation. 3381 // Show the recipients editor to give the user a chance to add 3382 // more people before the conversation begins. 3383 if (cursor.getCount() == 0 && !isRecipientsEditorVisible()) { 3384 initRecipientsEditor(); 3385 } 3386 3387 // FIXME: freshing layout changes the focused view to an unexpected 3388 // one, set it back to TextEditor forcely. 3389 mTextEditor.requestFocus(); 3390 3391 return; 3392 3393 case CALLER_ID_QUERY_TOKEN: 3394 case EMAIL_CONTACT_QUERY_TOKEN: 3395 cleanupContactInfoCursor(); 3396 mContactInfoCursor = cursor; 3397 updateContactInfo(); 3398 startPresencePollingRequest(); 3399 return; 3400 3401 } 3402 } 3403 3404 @Override 3405 protected void onDeleteComplete(int token, Object cookie, int result) { 3406 switch(token) { 3407 case DELETE_MESSAGE_TOKEN: 3408 case DELETE_CONVERSATION_TOKEN: 3409 // Update the notification for new messages since they 3410 // may be deleted. 3411 MessagingNotification.updateNewMessageIndicator( 3412 ComposeMessageActivity.this); 3413 // Update the notification for failed messages since they 3414 // may be deleted. 3415 updateSendFailedNotification(); 3416 break; 3417 } 3418 3419 if (token == DELETE_CONVERSATION_TOKEN) { 3420 ComposeMessageActivity.this.abandonDraftsAndFinish(); 3421 } 3422 } 3423 3424 @Override 3425 protected void onUpdateComplete(int token, Object cookie, int result) { 3426 switch(token) { 3427 case MARK_AS_READ_TOKEN: 3428 MessagingNotification.updateAllNotifications(ComposeMessageActivity.this); 3429 break; 3430 } 3431 } 3432 } 3433 3434 private void showSmileyDialog() { 3435 if (mSmileyDialog == null) { 3436 int[] icons = SmileyParser.DEFAULT_SMILEY_RES_IDS; 3437 String[] names = getResources().getStringArray( 3438 SmileyParser.DEFAULT_SMILEY_NAMES); 3439 final String[] texts = getResources().getStringArray( 3440 SmileyParser.DEFAULT_SMILEY_TEXTS); 3441 3442 final int N = names.length; 3443 3444 List<Map<String, ?>> entries = new ArrayList<Map<String, ?>>(); 3445 for (int i = 0; i < N; i++) { 3446 // We might have different ASCII for the same icon, skip it if 3447 // the icon is already added. 3448 boolean added = false; 3449 for (int j = 0; j < i; j++) { 3450 if (icons[i] == icons[j]) { 3451 added = true; 3452 break; 3453 } 3454 } 3455 if (!added) { 3456 HashMap<String, Object> entry = new HashMap<String, Object>(); 3457 3458 entry. put("icon", icons[i]); 3459 entry. put("name", names[i]); 3460 entry.put("text", texts[i]); 3461 3462 entries.add(entry); 3463 } 3464 } 3465 3466 final SimpleAdapter a = new SimpleAdapter( 3467 this, 3468 entries, 3469 R.layout.smiley_menu_item, 3470 new String[] {"icon", "name", "text"}, 3471 new int[] {R.id.smiley_icon, R.id.smiley_name, R.id.smiley_text}); 3472 SimpleAdapter.ViewBinder viewBinder = new SimpleAdapter.ViewBinder() { 3473 public boolean setViewValue(View view, Object data, String textRepresentation) { 3474 if (view instanceof ImageView) { 3475 Drawable img = getResources().getDrawable((Integer)data); 3476 ((ImageView)view).setImageDrawable(img); 3477 return true; 3478 } 3479 return false; 3480 } 3481 }; 3482 a.setViewBinder(viewBinder); 3483 3484 AlertDialog.Builder b = new AlertDialog.Builder(this); 3485 3486 b.setTitle(getString(R.string.menu_insert_smiley)); 3487 3488 b.setCancelable(true); 3489 b.setAdapter(a, new DialogInterface.OnClickListener() { 3490 public final void onClick(DialogInterface dialog, int which) { 3491 HashMap<String, Object> item = (HashMap<String, Object>) a.getItem(which); 3492 mTextEditor.append((String)item.get("text")); 3493 } 3494 }); 3495 3496 mSmileyDialog = b.create(); 3497 } 3498 3499 mSmileyDialog.show(); 3500 } 3501 3502 private void cleanupContactInfoCursor() { 3503 if (mContactInfoCursor != null) { 3504 mContactInfoCursor.close(); 3505 } 3506 } 3507 3508 private void cancelPresencePollingRequests() { 3509 mPresencePollingHandler.removeMessages(REFRESH_PRESENCE); 3510 } 3511 3512 private void startPresencePollingRequest() { 3513 mPresencePollingHandler.sendEmptyMessageDelayed(REFRESH_PRESENCE, 3514 60 * 1000); // refresh every minute 3515 } 3516 3517 private void startQueryForContactInfo() { 3518 String number = mRecipientList.getSingleRecipientNumber(); 3519 cancelPresencePollingRequests(); // make sure there are no outstanding polling requests 3520 if (TextUtils.isEmpty(number)) { 3521 setPresenceIcon(0); 3522 startPresencePollingRequest(); 3523 return; 3524 } 3525 3526 mContactInfoSelectionArgs[0] = number; 3527 3528 if (Mms.isEmailAddress(number)) { 3529 // Cancel any pending queries 3530 mBackgroundQueryHandler.cancelOperation(EMAIL_CONTACT_QUERY_TOKEN); 3531 3532 mBackgroundQueryHandler.startQuery(EMAIL_CONTACT_QUERY_TOKEN, null, 3533 METHOD_WITH_PRESENCE_URI, 3534 EMAIL_QUERY_PROJECTION, 3535 METHOD_LOOKUP, 3536 mContactInfoSelectionArgs, 3537 null); 3538 } else { 3539 // Cancel any pending queries 3540 mBackgroundQueryHandler.cancelOperation(CALLER_ID_QUERY_TOKEN); 3541 3542 mBackgroundQueryHandler.startQuery(CALLER_ID_QUERY_TOKEN, null, 3543 PHONES_WITH_PRESENCE_URI, 3544 CALLER_ID_PROJECTION, 3545 NUMBER_LOOKUP, 3546 mContactInfoSelectionArgs, 3547 null); 3548 } 3549 } 3550 3551 private void updateContactInfo() { 3552 boolean updated = false; 3553 if (mContactInfoCursor != null && mContactInfoCursor.moveToFirst()) { 3554 mPresenceStatus = mContactInfoCursor.getInt(PRESENCE_STATUS_COLUMN); 3555 if (mPresenceStatus != Contacts.People.OFFLINE) { 3556 int presenceIcon = Presence.getPresenceIconResourceId(mPresenceStatus); 3557 setPresenceIcon(presenceIcon); 3558 updated = true; 3559 } 3560 } 3561 if (!updated) { 3562 setPresenceIcon(0); 3563 } 3564 } 3565 3566} 3567