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