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