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