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