ComposeMessageActivity.java revision 1d98ae0b203e01034ddead4214d1520ce863a23b
1/* 2 * Copyright (C) 2008 Esmertec AG. 3 * Copyright (C) 2008 The Android Open Source Project 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.mms.ui; 19 20import static android.content.res.Configuration.KEYBOARDHIDDEN_NO; 21import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_ABORT; 22import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_COMPLETE; 23import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_START; 24import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_STATUS_ACTION; 25import static com.android.mms.ui.MessageListAdapter.COLUMN_ID; 26import static com.android.mms.ui.MessageListAdapter.COLUMN_MSG_TYPE; 27import static com.android.mms.ui.MessageListAdapter.PROJECTION; 28 29import com.android.mms.MmsConfig; 30import com.android.mms.R; 31import com.android.mms.data.Contact; 32import com.android.mms.data.ContactList; 33import com.android.mms.data.Conversation; 34import com.android.mms.data.WorkingMessage; 35import com.android.mms.data.WorkingMessage.MessageStatusListener; 36import com.android.mms.model.SlideshowModel; 37import com.android.mms.transaction.MessagingNotification; 38import com.android.mms.ui.MessageUtils.ResizeImageResultCallback; 39import com.android.mms.ui.RecipientsEditor.RecipientContextMenuInfo; 40import com.android.mms.util.ContactInfoCache; 41import com.android.mms.util.SendingProgressTokenManager; 42import com.android.mms.util.SmileyParser; 43 44import com.google.android.mms.ContentType; 45import com.google.android.mms.MmsException; 46import com.google.android.mms.pdu.EncodedStringValue; 47import com.google.android.mms.pdu.PduBody; 48import com.google.android.mms.pdu.PduPart; 49import com.google.android.mms.pdu.PduPersister; 50import com.google.android.mms.pdu.SendReq; 51import com.google.android.mms.util.SqliteWrapper; 52 53import android.app.Activity; 54import android.app.AlertDialog; 55import android.content.AsyncQueryHandler; 56import android.content.BroadcastReceiver; 57import android.content.ContentResolver; 58import android.content.ContentUris; 59import android.content.Context; 60import android.content.DialogInterface; 61import android.content.Intent; 62import android.content.IntentFilter; 63import android.content.DialogInterface.OnClickListener; 64import android.content.res.Configuration; 65import android.content.res.Resources; 66import android.database.Cursor; 67import android.database.DatabaseUtils; 68import android.database.sqlite.SQLiteException; 69import android.graphics.Bitmap; 70import android.graphics.drawable.Drawable; 71import android.media.RingtoneManager; 72import android.net.Uri; 73import android.os.Bundle; 74import android.os.Handler; 75import android.os.Message; 76import android.provider.Contacts; 77import android.provider.Contacts.People; 78import android.provider.Contacts.Presence; 79import android.provider.MediaStore; 80import android.provider.Settings; 81import android.provider.Telephony.Mms; 82import android.provider.Telephony.Sms; 83import android.telephony.SmsMessage; 84import android.text.ClipboardManager; 85import android.text.Editable; 86import android.text.InputFilter; 87import android.text.SpannableString; 88import android.text.Spanned; 89import android.text.TextUtils; 90import android.text.TextWatcher; 91import android.text.method.TextKeyListener; 92import android.text.style.URLSpan; 93import android.text.util.Linkify; 94import android.util.Config; 95import android.util.Log; 96import android.view.ContextMenu; 97import android.view.KeyEvent; 98import android.view.LayoutInflater; 99import android.view.Menu; 100import android.view.MenuItem; 101import android.view.View; 102import android.view.ViewStub; 103import android.view.Window; 104import android.view.ContextMenu.ContextMenuInfo; 105import android.view.View.OnCreateContextMenuListener; 106import android.view.View.OnFocusChangeListener; 107import android.view.View.OnKeyListener; 108import android.view.inputmethod.InputMethodManager; 109import android.widget.AdapterView; 110import android.widget.Button; 111import android.widget.EditText; 112import android.widget.ImageView; 113import android.widget.LinearLayout; 114import android.widget.ListView; 115import android.widget.SimpleAdapter; 116import android.widget.TextView; 117import android.widget.Toast; 118 119import java.io.InputStream; 120import java.io.IOException; 121import java.io.File; 122import java.io.FileInputStream; 123import java.io.FileOutputStream; 124import java.util.ArrayList; 125import java.util.HashMap; 126import java.util.Iterator; 127import java.util.List; 128import java.util.Map; 129 130import android.webkit.MimeTypeMap; 131 132/** 133 * This is the main UI for: 134 * 1. Composing a new message; 135 * 2. Viewing/managing message history of a conversation. 136 * 137 * This activity can handle following parameters from the intent 138 * by which it's launched. 139 * thread_id long Identify the conversation to be viewed. When creating a 140 * new message, this parameter shouldn't be present. 141 * msg_uri Uri The message which should be opened for editing in the editor. 142 * address String The addresses of the recipients in current conversation. 143 * exit_on_sent boolean Exit this activity after the message is sent. 144 */ 145public class ComposeMessageActivity extends Activity 146 implements View.OnClickListener, TextView.OnEditorActionListener, 147 MessageStatusListener { 148 public static final int REQUEST_CODE_ATTACH_IMAGE = 10; 149 public static final int REQUEST_CODE_TAKE_PICTURE = 11; 150 public static final int REQUEST_CODE_ATTACH_VIDEO = 12; 151 public static final int REQUEST_CODE_TAKE_VIDEO = 13; 152 public static final int REQUEST_CODE_ATTACH_SOUND = 14; 153 public static final int REQUEST_CODE_RECORD_SOUND = 15; 154 public static final int REQUEST_CODE_CREATE_SLIDESHOW = 16; 155 156 private static final String TAG = "ComposeMessageActivity"; 157 private static final boolean DEBUG = false; 158 private static final boolean TRACE = false; 159 private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; 160 161 // Menu ID 162 private static final int MENU_ADD_SUBJECT = 0; 163 private static final int MENU_DELETE_THREAD = 1; 164 private static final int MENU_ADD_ATTACHMENT = 2; 165 private static final int MENU_DISCARD = 3; 166 private static final int MENU_SEND = 4; 167 private static final int MENU_CALL_RECIPIENT = 5; 168 private static final int MENU_CONVERSATION_LIST = 6; 169 170 // Context menu ID 171 private static final int MENU_VIEW_CONTACT = 12; 172 private static final int MENU_ADD_TO_CONTACTS = 13; 173 174 private static final int MENU_EDIT_MESSAGE = 14; 175 private static final int MENU_VIEW_SLIDESHOW = 16; 176 private static final int MENU_VIEW_MESSAGE_DETAILS = 17; 177 private static final int MENU_DELETE_MESSAGE = 18; 178 private static final int MENU_SEARCH = 19; 179 private static final int MENU_DELIVERY_REPORT = 20; 180 private static final int MENU_FORWARD_MESSAGE = 21; 181 private static final int MENU_CALL_BACK = 22; 182 private static final int MENU_SEND_EMAIL = 23; 183 private static final int MENU_COPY_MESSAGE_TEXT = 24; 184 private static final int MENU_COPY_TO_SDCARD = 25; 185 private static final int MENU_INSERT_SMILEY = 26; 186 private static final int MENU_ADD_ADDRESS_TO_CONTACTS = 27; 187 188 private static final int RECIPIENTS_MAX_LENGTH = 312; 189 190 private static final int MESSAGE_LIST_QUERY_TOKEN = 9527; 191 192 private static final int DELETE_MESSAGE_TOKEN = 9700; 193 private static final int DELETE_CONVERSATION_TOKEN = 9701; 194 195 private static final int CALLER_ID_QUERY_TOKEN = 9800; 196 private static final int EMAIL_CONTACT_QUERY_TOKEN = 9801; 197 198 private static final int MARK_AS_READ_TOKEN = 9900; 199 200 private static final int MMS_THRESHOLD = 4; 201 202 private static final int CHARS_REMAINING_BEFORE_COUNTER_SHOWN = 10; 203 204 private static final long NO_DATE_FOR_DIALOG = -1L; 205 206 private static final int REFRESH_PRESENCE = 45236; 207 208 209 // caller id query params 210 private static final String[] CALLER_ID_PROJECTION = new String[] { 211 People.PRESENCE_STATUS, // 0 212 }; 213 private static final int PRESENCE_STATUS_COLUMN = 0; 214 215 private static final String NUMBER_LOOKUP = "PHONE_NUMBERS_EQUAL(" 216 + Contacts.Phones.NUMBER + ",?)"; 217 private static final Uri PHONES_WITH_PRESENCE_URI 218 = Uri.parse(Contacts.Phones.CONTENT_URI + "_with_presence"); 219 220 // email contact query params 221 private static final String[] EMAIL_QUERY_PROJECTION = new String[] { 222 Contacts.People.PRESENCE_STATUS, // 0 223 }; 224 225 private static final String METHOD_LOOKUP = Contacts.ContactMethods.DATA + "=?"; 226 private static final Uri METHOD_WITH_PRESENCE_URI = 227 Uri.withAppendedPath(Contacts.ContactMethods.CONTENT_URI, "with_presence"); 228 229 230 231 private ContentResolver mContentResolver; 232 233 private BackgroundQueryHandler mBackgroundQueryHandler; 234 235 private Conversation mConversation; // Conversation we are working in 236 237 private boolean mExitOnSent; // Should we finish() after sending a message? 238 239 private View mTopPanel; // View containing the recipient and subject editors 240 private View mBottomPanel; // View containing the text editor, send button, ec. 241 private EditText mTextEditor; // Text editor to type your message into 242 private TextView mTextCounter; // Shows the number of characters used in text editor 243 private Button mSendButton; // Press to detonate 244 private EditText mSubjectTextEditor; // Text editor for MMS subject 245 246 private AttachmentEditor mAttachmentEditor; 247 248 private MessageListView mMsgListView; // ListView for messages in this conversation 249 private MessageListAdapter mMsgListAdapter; // and its corresponding ListAdapter 250 251 private RecipientsEditor mRecipientsEditor; // UI control for editing recipients 252 253 private boolean mIsKeyboardOpen; // Whether the hardware keyboard is visible 254 private boolean mIsLandscape; // Whether we're in landscape mode 255 256 private boolean mPossiblePendingNotification; // If the message list has changed, we may have 257 // a pending notification to deal with. 258 259 private boolean mToastForDraftSave; // Whether to notify the user that a draft is 260 // being saved. 261 262 private WorkingMessage mWorkingMessage; // The message currently being composed. 263 264 private AlertDialog mSmileyDialog; 265 266 // Everything needed to deal with presence 267 private Cursor mContactInfoCursor; 268 private int mPresenceStatus; 269 private String[] mContactInfoSelectionArgs = new String[1]; 270 271 private boolean mWaitingForSubActivity; 272 273 @SuppressWarnings("unused") 274 private static void log(String format, Object... args) { 275 Thread current = Thread.currentThread(); 276 long tid = current.getId(); 277 StackTraceElement[] stack = current.getStackTrace(); 278 String methodName = stack[3].getMethodName(); 279 // Prepend current thread ID and name of calling method to the message. 280 format = "[" + tid + "] [" + methodName + "] " + format; 281 String logMsg = String.format(format, args); 282 Log.d(TAG, logMsg); 283 } 284 285 //========================================================== 286 // Inner classes 287 //========================================================== 288 289 private void editSlideshow() { 290 Uri dataUri = mWorkingMessage.saveAsMms(); 291 Intent intent = new Intent(this, SlideshowEditActivity.class); 292 intent.setData(dataUri); 293 startActivityForResult(intent, REQUEST_CODE_CREATE_SLIDESHOW); 294 } 295 296 private final Handler mAttachmentEditorHandler = new Handler() { 297 @Override 298 public void handleMessage(Message msg) { 299 switch (msg.what) { 300 case AttachmentEditor.MSG_EDIT_SLIDESHOW: { 301 editSlideshow(); 302 break; 303 } 304 case AttachmentEditor.MSG_SEND_SLIDESHOW: { 305 if (isPreparedForSending()) { 306 ComposeMessageActivity.this.confirmSendMessageIfNeeded(); 307 } 308 break; 309 } 310 case AttachmentEditor.MSG_VIEW_IMAGE: 311 case AttachmentEditor.MSG_PLAY_VIDEO: 312 case AttachmentEditor.MSG_PLAY_AUDIO: 313 case AttachmentEditor.MSG_PLAY_SLIDESHOW: 314 MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this, 315 mWorkingMessage); 316 break; 317 318 case AttachmentEditor.MSG_REPLACE_IMAGE: 319 case AttachmentEditor.MSG_REPLACE_VIDEO: 320 case AttachmentEditor.MSG_REPLACE_AUDIO: 321 showAddAttachmentDialog(); 322 break; 323 324 case AttachmentEditor.MSG_REMOVE_ATTACHMENT: 325 mWorkingMessage.setAttachment(WorkingMessage.TEXT, null); 326 break; 327 328 default: 329 break; 330 } 331 } 332 }; 333 334 private final Handler mMessageListItemHandler = new Handler() { 335 @Override 336 public void handleMessage(Message msg) { 337 String type; 338 switch (msg.what) { 339 case MessageListItem.MSG_LIST_EDIT_MMS: 340 type = "mms"; 341 break; 342 case MessageListItem.MSG_LIST_EDIT_SMS: 343 type = "sms"; 344 break; 345 default: 346 Log.w(TAG, "Unknown message: " + msg.what); 347 return; 348 } 349 350 MessageItem msgItem = getMessageItem(type, (Long) msg.obj); 351 if (msgItem != null) { 352 editMessageItem(msgItem); 353 drawBottomPanel(); 354 } 355 } 356 }; 357 358 private final Handler mPresencePollingHandler = new Handler() { 359 @Override 360 public void handleMessage(Message msg) { 361 if (msg.what == REFRESH_PRESENCE) { 362 startQueryForContactInfo(); 363 } 364 } 365 }; 366 367 private final OnKeyListener mSubjectKeyListener = new OnKeyListener() { 368 public boolean onKey(View v, int keyCode, KeyEvent event) { 369 if (event.getAction() != KeyEvent.ACTION_DOWN) { 370 return false; 371 } 372 373 // When the subject editor is empty, press "DEL" to hide the input field. 374 if ((keyCode == KeyEvent.KEYCODE_DEL) && (mSubjectTextEditor.length() == 0)) { 375 showSubjectEditor(false); 376 mWorkingMessage.setSubject(null); 377 return true; 378 } 379 380 return false; 381 } 382 }; 383 384 private MessageItem getMessageItem(String type, long msgId) { 385 // Check whether the cursor is valid or not. 386 Cursor cursor = mMsgListAdapter.getCursor(); 387 if (cursor.isClosed() || cursor.isBeforeFirst() || cursor.isAfterLast()) { 388 Log.e(TAG, "Bad cursor.", new RuntimeException()); 389 return null; 390 } 391 392 return mMsgListAdapter.getCachedMessageItem(type, msgId, cursor); 393 } 394 395 private void resetCounter() { 396 mTextCounter.setText(""); 397 mTextCounter.setVisibility(View.GONE); 398 } 399 400 private void updateCounter(CharSequence text, int start, int before, int count) { 401 // The worst case before we begin showing the text counter would be 402 // a UCS-2 message, providing space for 70 characters, minus 403 // CHARS_REMAINING_BEFORE_COUNTER_SHOWN. Don't bother calling 404 // the relatively expensive SmsMessage.calculateLength() until that 405 // point is reached. 406 if (text.length() < (70-CHARS_REMAINING_BEFORE_COUNTER_SHOWN)) { 407 mTextCounter.setVisibility(View.GONE); 408 return; 409 } 410 411 // If we're not removing text (i.e. no chance of converting back to SMS 412 // because of this change) and we're in MMS mode, just bail out. 413 final boolean textAdded = (before < count); 414 if (textAdded && mWorkingMessage.requiresMms()) { 415 mTextCounter.setVisibility(View.GONE); 416 return; 417 } 418 419 int[] params = SmsMessage.calculateLength(text, false); 420 /* SmsMessage.calculateLength returns an int[4] with: 421 * int[0] being the number of SMS's required, 422 * int[1] the number of code units used, 423 * int[2] is the number of code units remaining until the next message. 424 * int[3] is the encoding type that should be used for the message. 425 */ 426 int msgCount = params[0]; 427 int remainingInCurrentMessage = params[2]; 428 429 // Force send as MMS once the number of SMSes required reaches MMS_THRESHOLD. 430 mWorkingMessage.setLengthRequiresMms(msgCount >= MMS_THRESHOLD); 431 432 // Show the counter only if: 433 // - We are not in MMS mode 434 // - We are going to send more than one message OR we are getting close 435 boolean showCounter = false; 436 if (!mWorkingMessage.requiresMms() && 437 (msgCount > 1 || remainingInCurrentMessage <= CHARS_REMAINING_BEFORE_COUNTER_SHOWN)) { 438 showCounter = true; 439 } 440 441 if (showCounter) { 442 // Update the remaining characters and number of messages required. 443 mTextCounter.setText(remainingInCurrentMessage + " / " + msgCount); 444 mTextCounter.setVisibility(View.VISIBLE); 445 } else { 446 mTextCounter.setVisibility(View.GONE); 447 } 448 } 449 450 @Override 451 public void startActivityForResult(Intent intent, int requestCode) 452 { 453 // requestCode >= 0 means the activity in question is a sub-activity. 454 if (requestCode >= 0) { 455 mWaitingForSubActivity = true; 456 } 457 458 super.startActivityForResult(intent, requestCode); 459 } 460 461 private void toastConvertInfo(boolean toMms) { 462 int resId = toMms ? R.string.converting_to_picture_message 463 : R.string.converting_to_text_message; 464 Toast.makeText(this, resId, Toast.LENGTH_SHORT).show(); 465 } 466 467 private class DeleteMessageListener implements OnClickListener { 468 private final Uri mDeleteUri; 469 private final boolean mDeleteAll; 470 471 public DeleteMessageListener(Uri uri, boolean all) { 472 mDeleteUri = uri; 473 mDeleteAll = all; 474 } 475 476 public DeleteMessageListener(long msgId, String type) { 477 if ("mms".equals(type)) { 478 mDeleteUri = ContentUris.withAppendedId( 479 Mms.CONTENT_URI, msgId); 480 } else { 481 mDeleteUri = ContentUris.withAppendedId( 482 Sms.CONTENT_URI, msgId); 483 } 484 mDeleteAll = false; 485 } 486 487 public void onClick(DialogInterface dialog, int whichButton) { 488 int token = mDeleteAll ? DELETE_CONVERSATION_TOKEN 489 : DELETE_MESSAGE_TOKEN; 490 mBackgroundQueryHandler.startDelete(token, 491 null, mDeleteUri, null, null); 492 } 493 } 494 495 private class DiscardDraftListener implements OnClickListener { 496 public void onClick(DialogInterface dialog, int whichButton) { 497 mWorkingMessage.discard(); 498 goToConversationList(); 499 } 500 } 501 502 private class SendIgnoreInvalidRecipientListener implements OnClickListener { 503 public void onClick(DialogInterface dialog, int whichButton) { 504 sendMessage(); 505 } 506 } 507 508 private class CancelSendingListener implements OnClickListener { 509 public void onClick(DialogInterface dialog, int whichButton) { 510 if (isRecipientsEditorVisible()) { 511 mRecipientsEditor.requestFocus(); 512 } 513 } 514 } 515 516 private void confirmSendMessageIfNeeded() { 517 if (!isRecipientsEditorVisible()) { 518 sendMessage(); 519 return; 520 } 521 522 if (mRecipientsEditor.hasInvalidRecipient()) { 523 if (mRecipientsEditor.hasValidRecipient()) { 524 String title = getResourcesString(R.string.has_invalid_recipient, 525 mRecipientsEditor.formatInvalidNumbers()); 526 new AlertDialog.Builder(this) 527 .setIcon(android.R.drawable.ic_dialog_alert) 528 .setTitle(title) 529 .setMessage(R.string.invalid_recipient_message) 530 .setPositiveButton(R.string.try_to_send, 531 new SendIgnoreInvalidRecipientListener()) 532 .setNegativeButton(R.string.no, new CancelSendingListener()) 533 .show(); 534 } else { 535 new AlertDialog.Builder(this) 536 .setIcon(android.R.drawable.ic_dialog_alert) 537 .setTitle(R.string.cannot_send_message) 538 .setMessage(R.string.cannot_send_message_reason) 539 .setPositiveButton(R.string.yes, new CancelSendingListener()) 540 .show(); 541 } 542 } else { 543 sendMessage(); 544 } 545 } 546 547 private final TextWatcher mRecipientsWatcher = new TextWatcher() { 548 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 549 } 550 551 public void onTextChanged(CharSequence s, int start, int before, int count) { 552 // This is a workaround for bug 1609057. Since onUserInteraction() is 553 // not called when the user touches the soft keyboard, we pretend it was 554 // called when textfields changes. This should be removed when the bug 555 // is fixed. 556 onUserInteraction(); 557 } 558 559 public void afterTextChanged(Editable s) { 560 // Bug 1474782 describes a situation in which we send to 561 // the wrong recipient. We have been unable to reproduce this, 562 // but the best theory we have so far is that the contents of 563 // mRecipientList somehow become stale when entering 564 // ComposeMessageActivity via onNewIntent(). This assertion is 565 // meant to catch one possible path to that, of a non-visible 566 // mRecipientsEditor having its TextWatcher fire and refreshing 567 // mRecipientList with its stale contents. 568 if (!isRecipientsEditorVisible()) { 569 IllegalStateException e = new IllegalStateException( 570 "afterTextChanged called with invisible mRecipientsEditor"); 571 // Make sure the crash is uploaded to the service so we 572 // can see if this is happening in the field. 573 Log.e(TAG, "RecipientsWatcher called incorrectly", e); 574 throw e; 575 } 576 577 mWorkingMessage.setWorkingRecipients(mRecipientsEditor.getNumbers()); 578 mWorkingMessage.setHasEmail(mRecipientsEditor.containsEmail()); 579 580 // Walk backwards in the text box, skipping spaces. If the last 581 // character is a comma, update the title bar. 582 for (int pos = s.length() - 1; pos >= 0; pos--) { 583 char c = s.charAt(pos); 584 if (c == ' ') 585 continue; 586 587 if (c == ',') { 588 updateWindowTitle(); 589 startQueryForContactInfo(); 590 } 591 break; 592 } 593 594 // If we have gone to zero recipients, disable send button. 595 updateSendButtonState(); 596 } 597 }; 598 599 private final OnCreateContextMenuListener mRecipientsMenuCreateListener = 600 new OnCreateContextMenuListener() { 601 public void onCreateContextMenu(ContextMenu menu, View v, 602 ContextMenuInfo menuInfo) { 603 if (menuInfo != null) { 604 Contact c = ((RecipientContextMenuInfo) menuInfo).recipient; 605 RecipientsMenuClickListener l = new RecipientsMenuClickListener(c); 606 607 menu.setHeaderTitle(c.getName()); 608 609 if (c.existsInDatabase()) { 610 menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact) 611 .setOnMenuItemClickListener(l); 612 } else { 613 menu.add(0, MENU_ADD_TO_CONTACTS, 0, R.string.menu_add_to_contacts) 614 .setOnMenuItemClickListener(l); 615 } 616 } 617 } 618 }; 619 620 private final class RecipientsMenuClickListener implements MenuItem.OnMenuItemClickListener { 621 private final Contact mRecipient; 622 623 RecipientsMenuClickListener(Contact recipient) { 624 mRecipient = recipient; 625 } 626 627 public boolean onMenuItemClick(MenuItem item) { 628 switch (item.getItemId()) { 629 // Context menu handlers for the recipients editor. 630 case MENU_VIEW_CONTACT: { 631 Uri contactUri = mRecipient.getUri(); 632 startActivity(new Intent(Intent.ACTION_VIEW, contactUri)); 633 return true; 634 } 635 case MENU_ADD_TO_CONTACTS: { 636 Intent intent = ConversationList.createAddContactIntent( 637 mRecipient.getNumber()); 638 ComposeMessageActivity.this.startActivity(intent); 639 return true; 640 } 641 } 642 return false; 643 } 644 } 645 646 private void addPositionBasedMenuItems(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 647 AdapterView.AdapterContextMenuInfo info; 648 649 try { 650 info = (AdapterView.AdapterContextMenuInfo) menuInfo; 651 } catch (ClassCastException e) { 652 Log.e(TAG, "bad menuInfo"); 653 return; 654 } 655 final int position = info.position; 656 657 addUriSpecificMenuItems(menu, v, position); 658 } 659 660 private Uri getSelectedUriFromMessageList(ListView listView, int position) { 661 // If the context menu was opened over a uri, get that uri. 662 MessageListItem msglistItem = (MessageListItem) listView.getChildAt(position); 663 if (msglistItem == null) { 664 // FIXME: Should get the correct view. No such interface in ListView currently 665 // to get the view by position. The ListView.getChildAt(position) cannot 666 // get correct view since the list doesn't create one child for each item. 667 // And if setSelection(position) then getSelectedView(), 668 // cannot get corrent view when in touch mode. 669 return null; 670 } 671 672 TextView textView; 673 CharSequence text = null; 674 int selStart = -1; 675 int selEnd = -1; 676 677 //check if message sender is selected 678 textView = (TextView) msglistItem.findViewById(R.id.text_view); 679 if (textView != null) { 680 text = textView.getText(); 681 selStart = textView.getSelectionStart(); 682 selEnd = textView.getSelectionEnd(); 683 } 684 685 if (selStart == -1) { 686 //sender is not being selected, it may be within the message body 687 textView = (TextView) msglistItem.findViewById(R.id.body_text_view); 688 if (textView != null) { 689 text = textView.getText(); 690 selStart = textView.getSelectionStart(); 691 selEnd = textView.getSelectionEnd(); 692 } 693 } 694 695 // Check that some text is actually selected, rather than the cursor 696 // just being placed within the TextView. 697 if (selStart != selEnd) { 698 int min = Math.min(selStart, selEnd); 699 int max = Math.max(selStart, selEnd); 700 701 URLSpan[] urls = ((Spanned) text).getSpans(min, max, 702 URLSpan.class); 703 704 if (urls.length == 1) { 705 return Uri.parse(urls[0].getURL()); 706 } 707 } 708 709 //no uri was selected 710 return null; 711 } 712 713 private void addUriSpecificMenuItems(ContextMenu menu, View v, int position) { 714 Uri uri = getSelectedUriFromMessageList((ListView) v, position); 715 716 if (uri != null) { 717 Intent intent = new Intent(null, uri); 718 intent.addCategory(Intent.CATEGORY_SELECTED_ALTERNATIVE); 719 menu.addIntentOptions(0, 0, 0, 720 new android.content.ComponentName(this, ComposeMessageActivity.class), 721 null, intent, 0, null); 722 } 723 } 724 725 private final void addCallAndContactMenuItems( 726 ContextMenu menu, MsgListMenuClickListener l, MessageItem msgItem) { 727 // Add all possible links in the address & message 728 StringBuilder textToSpannify = new StringBuilder(); 729 if (msgItem.mBoxId == Mms.MESSAGE_BOX_INBOX) { 730 textToSpannify.append(msgItem.mAddress + ": "); 731 } 732 textToSpannify.append(msgItem.mBody); 733 734 SpannableString msg = new SpannableString(textToSpannify.toString()); 735 Linkify.addLinks(msg, Linkify.ALL); 736 ArrayList<String> uris = 737 MessageUtils.extractUris(msg.getSpans(0, msg.length(), URLSpan.class)); 738 739 while (uris.size() > 0) { 740 String uriString = uris.remove(0); 741 // Remove any dupes so they don't get added to the menu multiple times 742 while (uris.contains(uriString)) { 743 uris.remove(uriString); 744 } 745 746 int sep = uriString.indexOf(":"); 747 String prefix = null; 748 if (sep >= 0) { 749 prefix = uriString.substring(0, sep); 750 uriString = uriString.substring(sep + 1); 751 } 752 boolean addToContacts = false; 753 if ("mailto".equalsIgnoreCase(prefix)) { 754 String sendEmailString = getString( 755 R.string.menu_send_email).replace("%s", uriString); 756 menu.add(0, MENU_SEND_EMAIL, 0, sendEmailString) 757 .setOnMenuItemClickListener(l) 758 .setIntent(new Intent( 759 Intent.ACTION_VIEW, 760 Uri.parse("mailto:" + uriString))); 761 addToContacts = !haveEmailContact(uriString); 762 } else if ("tel".equalsIgnoreCase(prefix)) { 763 String callBackString = getString( 764 R.string.menu_call_back).replace("%s", uriString); 765 menu.add(0, MENU_CALL_BACK, 0, callBackString) 766 .setOnMenuItemClickListener(l) 767 .setIntent(new Intent( 768 Intent.ACTION_DIAL, 769 Uri.parse("tel:" + uriString))); 770 addToContacts = !isNumberInContacts(uriString); 771 } 772 if (addToContacts) { 773 Intent intent = ConversationList.createAddContactIntent(uriString); 774 String addContactString = getString( 775 R.string.menu_add_address_to_contacts).replace("%s", uriString); 776 menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, addContactString) 777 .setOnMenuItemClickListener(l) 778 .setIntent(intent); 779 } 780 } 781 } 782 783 private boolean haveEmailContact(String emailAddress) { 784 Cursor cursor = SqliteWrapper.query(this, getContentResolver(), 785 Contacts.ContactMethods.CONTENT_EMAIL_URI, 786 new String[] { Contacts.ContactMethods.NAME }, 787 Contacts.ContactMethods.DATA + " = " + DatabaseUtils.sqlEscapeString(emailAddress), 788 null, null); 789 790 if (cursor != null) { 791 try { 792 while (cursor.moveToNext()) { 793 String name = cursor.getString(0); 794 if (!TextUtils.isEmpty(name)) { 795 return true; 796 } 797 } 798 } finally { 799 cursor.close(); 800 } 801 } 802 return false; 803 } 804 805 private boolean isNumberInContacts(String phoneNumber) { 806 ContactInfoCache.CacheEntry entry = 807 ContactInfoCache.getInstance().getContactInfo(phoneNumber); 808 return !TextUtils.isEmpty(entry.name); 809 } 810 811 private final OnCreateContextMenuListener mMsgListMenuCreateListener = 812 new OnCreateContextMenuListener() { 813 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 814 Cursor cursor = mMsgListAdapter.getCursor(); 815 String type = cursor.getString(COLUMN_MSG_TYPE); 816 long msgId = cursor.getLong(COLUMN_ID); 817 818 addPositionBasedMenuItems(menu, v, menuInfo); 819 820 MessageItem msgItem = mMsgListAdapter.getCachedMessageItem(type, msgId, cursor); 821 if (msgItem == null) { 822 Log.e(TAG, "Cannot load message item for type = " + type 823 + ", msgId = " + msgId); 824 return; 825 } 826 827 menu.setHeaderTitle(R.string.message_options); 828 829 MsgListMenuClickListener l = new MsgListMenuClickListener(); 830 if (msgItem.isMms()) { 831 switch (msgItem.mBoxId) { 832 case Mms.MESSAGE_BOX_INBOX: 833 break; 834 case Mms.MESSAGE_BOX_OUTBOX: 835 // Since we currently break outgoing messages to multiple 836 // recipients into one message per recipient, only allow 837 // editing a message for single-recipient conversations. 838 if (getRecipients().size() == 1) { 839 menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit) 840 .setOnMenuItemClickListener(l); 841 } 842 break; 843 } 844 switch (msgItem.mAttachmentType) { 845 case WorkingMessage.TEXT: 846 break; 847 case WorkingMessage.VIDEO: 848 case WorkingMessage.IMAGE: 849 if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) { 850 menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard) 851 .setOnMenuItemClickListener(l); 852 } 853 break; 854 case WorkingMessage.SLIDESHOW: 855 default: 856 menu.add(0, MENU_VIEW_SLIDESHOW, 0, R.string.view_slideshow) 857 .setOnMenuItemClickListener(l); 858 if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) { 859 menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard) 860 .setOnMenuItemClickListener(l); 861 } 862 break; 863 } 864 } else { 865 // Message type is sms. Only allow "edit" if the message has a single recipient 866 if (getRecipients().size() == 1 && 867 (msgItem.mBoxId == Sms.MESSAGE_TYPE_OUTBOX || 868 msgItem.mBoxId == Sms.MESSAGE_TYPE_FAILED)) { 869 menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit) 870 .setOnMenuItemClickListener(l); 871 } 872 } 873 874 addCallAndContactMenuItems(menu, l, msgItem); 875 876 // Forward is not available for undownloaded messages. 877 if (msgItem.isDownloaded()) { 878 menu.add(0, MENU_FORWARD_MESSAGE, 0, R.string.menu_forward) 879 .setOnMenuItemClickListener(l); 880 } 881 882 // It is unclear what would make most sense for copying an MMS message 883 // to the clipboard, so we currently do SMS only. 884 if (msgItem.isSms()) { 885 menu.add(0, MENU_COPY_MESSAGE_TEXT, 0, R.string.copy_message_text) 886 .setOnMenuItemClickListener(l); 887 } 888 889 menu.add(0, MENU_VIEW_MESSAGE_DETAILS, 0, R.string.view_message_details) 890 .setOnMenuItemClickListener(l); 891 menu.add(0, MENU_DELETE_MESSAGE, 0, R.string.delete_message) 892 .setOnMenuItemClickListener(l); 893 if (msgItem.mDeliveryReport || msgItem.mReadReport) { 894 menu.add(0, MENU_DELIVERY_REPORT, 0, R.string.view_delivery_report) 895 .setOnMenuItemClickListener(l); 896 } 897 } 898 }; 899 900 private void editMessageItem(MessageItem msgItem) { 901 if ("sms".equals(msgItem.mType)) { 902 editSmsMessageItem(msgItem); 903 } else { 904 editMmsMessageItem(msgItem); 905 } 906 if (MessageListItem.isFailedMessage(msgItem) && mMsgListAdapter.getCount() <= 1) { 907 // For messages with bad addresses, let the user re-edit the recipients. 908 initRecipientsEditor(); 909 } 910 } 911 912 private void editSmsMessageItem(MessageItem msgItem) { 913 // Delete the old undelivered SMS and load its content. 914 Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgItem.mMsgId); 915 SqliteWrapper.delete(ComposeMessageActivity.this, 916 mContentResolver, uri, null, null); 917 mWorkingMessage.setText(msgItem.mBody); 918 } 919 920 private void editMmsMessageItem(MessageItem msgItem) { 921 // Discard the current message in progress. 922 mWorkingMessage.discard(); 923 924 // Load the selected message in as the working message. 925 mWorkingMessage = WorkingMessage.load(this, msgItem.mMessageUri); 926 mWorkingMessage.setConversation(mConversation); 927 928 mAttachmentEditor.update(mWorkingMessage); 929 drawTopPanel(); 930 } 931 932 private void copyToClipboard(String str) { 933 ClipboardManager clip = 934 (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); 935 clip.setText(str); 936 } 937 938 private void forwardMessage(MessageItem msgItem) { 939 Intent intent = new Intent(ComposeMessageActivity.this, 940 ComposeMessageActivity.class); 941 942 intent.putExtra("exit_on_sent", true); 943 intent.putExtra("forwarded_message", true); 944 if (msgItem.mType.equals("sms")) { 945 intent.putExtra("sms_body", msgItem.mBody); 946 } else { 947 SendReq sendReq = new SendReq(); 948 String subject = getString(R.string.forward_prefix); 949 if (msgItem.mSubject != null) { 950 subject += msgItem.mSubject; 951 } 952 sendReq.setSubject(new EncodedStringValue(subject)); 953 sendReq.setBody(msgItem.mSlideshow.makeCopy( 954 ComposeMessageActivity.this)); 955 956 Uri uri = null; 957 try { 958 PduPersister persister = PduPersister.getPduPersister(this); 959 // Copy the parts of the message here. 960 uri = persister.persist(sendReq, Mms.Draft.CONTENT_URI); 961 } catch (MmsException e) { 962 Log.e(TAG, "Failed to copy message: " + msgItem.mMessageUri, e); 963 Toast.makeText(ComposeMessageActivity.this, 964 R.string.cannot_save_message, Toast.LENGTH_SHORT).show(); 965 return; 966 } 967 968 intent.putExtra("msg_uri", uri); 969 intent.putExtra("subject", subject); 970 } 971 startActivityIfNeeded(intent, -1); 972 } 973 974 /** 975 * Context menu handlers for the message list view. 976 */ 977 private final class MsgListMenuClickListener implements MenuItem.OnMenuItemClickListener { 978 public boolean onMenuItemClick(MenuItem item) { 979 Cursor cursor = mMsgListAdapter.getCursor(); 980 String type = cursor.getString(COLUMN_MSG_TYPE); 981 long msgId = cursor.getLong(COLUMN_ID); 982 MessageItem msgItem = getMessageItem(type, msgId); 983 984 if (msgItem == null) { 985 return false; 986 } 987 988 switch (item.getItemId()) { 989 case MENU_EDIT_MESSAGE: 990 editMessageItem(msgItem); 991 drawBottomPanel(); 992 return true; 993 994 case MENU_COPY_MESSAGE_TEXT: 995 copyToClipboard(msgItem.mBody); 996 return true; 997 998 case MENU_FORWARD_MESSAGE: 999 forwardMessage(msgItem); 1000 return true; 1001 1002 case MENU_VIEW_SLIDESHOW: 1003 MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this, 1004 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId), null); 1005 return true; 1006 1007 case MENU_VIEW_MESSAGE_DETAILS: { 1008 String messageDetails = MessageUtils.getMessageDetails( 1009 ComposeMessageActivity.this, cursor, msgItem.mMessageSize); 1010 new AlertDialog.Builder(ComposeMessageActivity.this) 1011 .setTitle(R.string.message_details_title) 1012 .setMessage(messageDetails) 1013 .setPositiveButton(android.R.string.ok, null) 1014 .setCancelable(true) 1015 .show(); 1016 return true; 1017 } 1018 case MENU_DELETE_MESSAGE: { 1019 DeleteMessageListener l = new DeleteMessageListener( 1020 msgItem.mMessageUri, false); 1021 confirmDeleteDialog(l, false); 1022 return true; 1023 } 1024 case MENU_DELIVERY_REPORT: 1025 showDeliveryReport(msgId, type); 1026 return true; 1027 1028 case MENU_COPY_TO_SDCARD: { 1029 int resId = copyMedia(msgId) ? R.string.copy_to_sdcard_success : 1030 R.string.copy_to_sdcard_fail; 1031 Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show(); 1032 return true; 1033 } 1034 1035 default: 1036 return false; 1037 } 1038 } 1039 } 1040 1041 /** 1042 * Looks to see if there are any valid parts of the attachment that can be copied to a SD card. 1043 * @param msgId 1044 */ 1045 private boolean haveSomethingToCopyToSDCard(long msgId) { 1046 PduBody body; 1047 try { 1048 body = SlideshowModel.getPduBody(this, 1049 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId)); 1050 } catch (MmsException e) { 1051 Log.e(TAG, e.getMessage(), e); 1052 return false; 1053 } 1054 1055 boolean result = false; 1056 int partNum = body.getPartsNum(); 1057 for(int i = 0; i < partNum; i++) { 1058 PduPart part = body.getPart(i); 1059 String type = new String(part.getContentType()); 1060 1061 if ((ContentType.isImageType(type) || ContentType.isVideoType(type) || 1062 ContentType.isAudioType(type))) { 1063 result = true; 1064 break; 1065 } 1066 } 1067 return result; 1068 } 1069 1070 /** 1071 * Copies media from an Mms to the "download" directory on the SD card 1072 * @param msgId 1073 */ 1074 private boolean copyMedia(long msgId) { 1075 PduBody body; 1076 boolean result = true; 1077 try { 1078 body = SlideshowModel.getPduBody(this, ContentUris.withAppendedId(Mms.CONTENT_URI, msgId)); 1079 } catch (MmsException e) { 1080 Log.e(TAG, e.getMessage(), e); 1081 return false; 1082 } 1083 1084 int partNum = body.getPartsNum(); 1085 for(int i = 0; i < partNum; i++) { 1086 PduPart part = body.getPart(i); 1087 String type = new String(part.getContentType()); 1088 1089 if ((ContentType.isImageType(type) || ContentType.isVideoType(type) || 1090 ContentType.isAudioType(type))) { 1091 result &= copyPart(part); // all parts have to be successful for a valid result. 1092 } 1093 } 1094 return result; 1095 } 1096 1097 private boolean copyPart(PduPart part) { 1098 Uri uri = part.getDataUri(); 1099 1100 InputStream input = null; 1101 FileOutputStream fout = null; 1102 try { 1103 input = mContentResolver.openInputStream(uri); 1104 if (input instanceof FileInputStream) { 1105 FileInputStream fin = (FileInputStream) input; 1106 1107 byte[] location = part.getName(); 1108 if (location == null) { 1109 location = part.getFilename(); 1110 } 1111 if (location == null) { 1112 location = part.getContentLocation(); 1113 } 1114 1115 // Depending on the location, there may be an 1116 // extension already on the name or not 1117 String fileName = new String(location); 1118 String dir = "/sdcard/download/"; 1119 String extension; 1120 int index; 1121 if ((index = fileName.indexOf(".")) == -1) { 1122 String type = new String(part.getContentType()); 1123 extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type); 1124 } else { 1125 extension = fileName.substring(index + 1, fileName.length()); 1126 fileName = fileName.substring(0, index); 1127 } 1128 1129 File file = getUniqueDestination(dir + fileName, extension); 1130 1131 // make sure the path is valid and directories created for this file. 1132 File parentFile = file.getParentFile(); 1133 if (!parentFile.exists() && !parentFile.mkdirs()) { 1134 Log.e(TAG, "[MMS] copyPart: mkdirs for " + parentFile.getPath() + " failed!"); 1135 return false; 1136 } 1137 1138 fout = new FileOutputStream(file); 1139 1140 byte[] buffer = new byte[8000]; 1141 while(fin.read(buffer) != -1) { 1142 fout.write(buffer); 1143 } 1144 1145 // Notify other applications listening to scanner events 1146 // that a media file has been added to the sd card 1147 sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, 1148 Uri.fromFile(file))); 1149 } 1150 } catch (IOException e) { 1151 // Ignore 1152 Log.e(TAG, "IOException caught while opening or reading stream", e); 1153 return false; 1154 } finally { 1155 if (null != input) { 1156 try { 1157 input.close(); 1158 } catch (IOException e) { 1159 // Ignore 1160 Log.e(TAG, "IOException caught while closing stream", e); 1161 return false; 1162 } 1163 } 1164 if (null != fout) { 1165 try { 1166 fout.close(); 1167 } catch (IOException e) { 1168 // Ignore 1169 Log.e(TAG, "IOException caught while closing stream", e); 1170 return false; 1171 } 1172 } 1173 } 1174 return true; 1175 } 1176 1177 private File getUniqueDestination(String base, String extension) { 1178 File file = new File(base + "." + extension); 1179 1180 for (int i = 2; file.exists(); i++) { 1181 file = new File(base + "_" + i + "." + extension); 1182 } 1183 return file; 1184 } 1185 1186 private void showDeliveryReport(long messageId, String type) { 1187 Intent intent = new Intent(this, DeliveryReportActivity.class); 1188 intent.putExtra("message_id", messageId); 1189 intent.putExtra("message_type", type); 1190 1191 startActivity(intent); 1192 } 1193 1194 private final IntentFilter mHttpProgressFilter = new IntentFilter(PROGRESS_STATUS_ACTION); 1195 1196 private final BroadcastReceiver mHttpProgressReceiver = new BroadcastReceiver() { 1197 @Override 1198 public void onReceive(Context context, Intent intent) { 1199 if (PROGRESS_STATUS_ACTION.equals(intent.getAction())) { 1200 long token = intent.getLongExtra("token", 1201 SendingProgressTokenManager.NO_TOKEN); 1202 if (token != mConversation.getThreadId()) { 1203 return; 1204 } 1205 1206 int progress = intent.getIntExtra("progress", 0); 1207 switch (progress) { 1208 case PROGRESS_START: 1209 setProgressBarVisibility(true); 1210 break; 1211 case PROGRESS_ABORT: 1212 case PROGRESS_COMPLETE: 1213 setProgressBarVisibility(false); 1214 break; 1215 default: 1216 setProgress(100 * progress); 1217 } 1218 } 1219 } 1220 }; 1221 1222 private static ContactList sEmptyContactList; 1223 1224 private ContactList getRecipients() { 1225 // If the recipients editor is visible, the conversation has 1226 // not really officially 'started' yet. Recipients will be set 1227 // on the conversation once it has been saved or sent. In the 1228 // meantime, let anyone who needs the recipient list think it 1229 // is empty rather than giving them a stale one. 1230 if (isRecipientsEditorVisible()) { 1231 if (sEmptyContactList == null) { 1232 sEmptyContactList = new ContactList(); 1233 } 1234 return sEmptyContactList; 1235 } 1236 return mConversation.getRecipients(); 1237 } 1238 1239 // Get the recipients editor ready to be displayed onscreen. 1240 private void initRecipientsEditor() { 1241 if (isRecipientsEditorVisible()) { 1242 return; 1243 } 1244 ViewStub stub = (ViewStub)findViewById(R.id.recipients_editor_stub); 1245 if (stub != null) { 1246 mRecipientsEditor = (RecipientsEditor) stub.inflate(); 1247 } else { 1248 mRecipientsEditor = (RecipientsEditor)findViewById(R.id.recipients_editor); 1249 mRecipientsEditor.setVisibility(View.VISIBLE); 1250 } 1251 1252 mRecipientsEditor.setAdapter(new RecipientsAdapter(this)); 1253 mRecipientsEditor.populate(getRecipients()); 1254 mRecipientsEditor.setOnCreateContextMenuListener(mRecipientsMenuCreateListener); 1255 mRecipientsEditor.addTextChangedListener(mRecipientsWatcher); 1256 mRecipientsEditor.setFilters(new InputFilter[] { 1257 new InputFilter.LengthFilter(RECIPIENTS_MAX_LENGTH) }); 1258 mRecipientsEditor.setOnItemClickListener(new AdapterView.OnItemClickListener() { 1259 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1260 // After the user selects an item in the pop-up contacts list, move the 1261 // focus to the text editor if there is only one recipient. This helps 1262 // the common case of selecting one recipient and then typing a message, 1263 // but avoids annoying a user who is trying to add five recipients and 1264 // keeps having focus stolen away. 1265 if (mRecipientsEditor.getRecipientCount() == 1) { 1266 // if we're in extract mode then don't request focus 1267 final InputMethodManager inputManager = (InputMethodManager) 1268 getSystemService(Context.INPUT_METHOD_SERVICE); 1269 if (inputManager == null || !inputManager.isFullscreenMode()) { 1270 mTextEditor.requestFocus(); 1271 } 1272 } 1273 } 1274 }); 1275 1276 mTopPanel.setVisibility(View.VISIBLE); 1277 } 1278 1279 //========================================================== 1280 // Activity methods 1281 //========================================================== 1282 1283 private void setPresenceIcon(int iconId) { 1284 Drawable icon = iconId == 0 ? null : this.getResources().getDrawable(iconId); 1285 getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, icon); 1286 } 1287 1288 static public boolean cancelFailedToDeliverNotification(Intent intent, Context context) { 1289 if (ConversationList.isFailedToDeliver(intent)) { 1290 // Cancel any failed message notifications 1291 MessagingNotification.cancelNotification(context, 1292 MessagingNotification.MESSAGE_FAILED_NOTIFICATION_ID); 1293 return true; 1294 } 1295 return false; 1296 } 1297 1298 @Override 1299 protected void onCreate(Bundle savedInstanceState) { 1300 super.onCreate(savedInstanceState); 1301 requestWindowFeature(Window.FEATURE_PROGRESS); 1302 requestWindowFeature(Window.FEATURE_LEFT_ICON); 1303 1304 setContentView(R.layout.compose_message_activity); 1305 setProgressBarVisibility(false); 1306 1307 setTitle(""); 1308 1309 // Initialize members for UI elements. 1310 initResourceRefs(); 1311 1312 mContentResolver = getContentResolver(); 1313 mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver); 1314 1315 // Create a new empty working message. 1316 mWorkingMessage = WorkingMessage.createEmpty(this); 1317 1318 // Read parameters or previously saved state of this activity. 1319 initActivityState(savedInstanceState, getIntent()); 1320 1321 if (LOCAL_LOGV) { 1322 Log.v(TAG, "onCreate(): savedInstanceState = " + savedInstanceState); 1323 Log.v(TAG, "onCreate(): intent = " + getIntent()); 1324 } 1325 1326 if (cancelFailedToDeliverNotification(getIntent(), getApplicationContext())) { 1327 // Show a pop-up dialog to inform user the message was 1328 // failed to deliver. 1329 undeliveredMessageDialog(getMessageDate(null)); 1330 } 1331 1332 // Set up the message history ListAdapter 1333 initMessageList(); 1334 1335 // Mark the current thread as read. 1336 mConversation.markAsRead(); 1337 1338 // Load the draft for this thread, if we aren't already handling 1339 // existing data, such as a shared picture or forwarded message. 1340 if (!handleSendIntent(getIntent()) && !handleForwardedMessage()) { 1341 loadDraft(); 1342 } 1343 1344 // Let the working message know what conversation it belongs to. 1345 mWorkingMessage.setConversation(mConversation); 1346 1347 // Show the recipients editor if we don't have a valid thread. 1348 if (mConversation.getThreadId() <= 0) { 1349 initRecipientsEditor(); 1350 } 1351 1352 updateSendButtonState(); 1353 1354 drawTopPanel(); 1355 drawBottomPanel(); 1356 mAttachmentEditor.update(mWorkingMessage); 1357 1358 Configuration config = getResources().getConfiguration(); 1359 mIsKeyboardOpen = config.keyboardHidden == KEYBOARDHIDDEN_NO; 1360 mIsLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE; 1361 onKeyboardStateChanged(mIsKeyboardOpen); 1362 1363 if (TRACE) { 1364 android.os.Debug.startMethodTracing("compose"); 1365 } 1366 } 1367 1368 private void showSubjectEditor(boolean show) { 1369 if (mSubjectTextEditor == null) { 1370 // Don't bother to initialize the subject editor if 1371 // we're just going to hide it. 1372 if (show == false) { 1373 return; 1374 } 1375 mSubjectTextEditor = (EditText)findViewById(R.id.subject); 1376 mSubjectTextEditor.setOnKeyListener(mSubjectKeyListener); 1377 mSubjectTextEditor.addTextChangedListener(mSubjectEditorWatcher); 1378 } 1379 mSubjectTextEditor.setText(mWorkingMessage.getSubject()); 1380 mSubjectTextEditor.setVisibility(show ? View.VISIBLE : View.GONE); 1381 hideOrShowTopPanel(); 1382 } 1383 1384 private void hideOrShowTopPanel() { 1385 boolean anySubViewsVisible = (isSubjectEditorVisible() || isRecipientsEditorVisible()); 1386 mTopPanel.setVisibility(anySubViewsVisible ? View.VISIBLE : View.GONE); 1387 } 1388 1389 @Override 1390 protected void onRestart() { 1391 super.onRestart(); 1392 1393 mConversation.markAsRead(); 1394 } 1395 1396 @Override 1397 protected void onStart() { 1398 super.onStart(); 1399 1400 updateWindowTitle(); 1401 initFocus(); 1402 1403 // Register a BroadcastReceiver to listen on HTTP I/O process. 1404 registerReceiver(mHttpProgressReceiver, mHttpProgressFilter); 1405 1406 startMsgListQuery(); 1407 startQueryForContactInfo(); 1408 updateSendFailedNotification(); 1409 1410 } 1411 1412 @Override 1413 protected void onResume() { 1414 super.onResume(); 1415 startPresencePollingRequest(); 1416 } 1417 1418 private void updateSendFailedNotification() { 1419 final long threadId = mConversation.getThreadId(); 1420 if (threadId <= 0) 1421 return; 1422 1423 // updateSendFailedNotificationForThread makes a database call, so do the work off 1424 // of the ui thread. 1425 new Thread(new Runnable() { 1426 public void run() { 1427 MessagingNotification.updateSendFailedNotificationForThread( 1428 ComposeMessageActivity.this, threadId); 1429 } 1430 }).run(); 1431 } 1432 1433 @Override 1434 public void onSaveInstanceState(Bundle outState) { 1435 super.onSaveInstanceState(outState); 1436 1437 outState.putString("recipients", getRecipients().serialize()); 1438 1439 mWorkingMessage.writeStateToBundle(outState); 1440 1441 if (mExitOnSent) { 1442 outState.putBoolean("exit_on_sent", mExitOnSent); 1443 } 1444 } 1445 1446 @Override 1447 protected void onPause() { 1448 super.onPause(); 1449 cancelPresencePollingRequests(); 1450 } 1451 1452 @Override 1453 protected void onStop() { 1454 super.onStop(); 1455 1456 if (mMsgListAdapter != null) { 1457 mMsgListAdapter.changeCursor(null); 1458 } 1459 1460 saveDraft(); 1461 1462 // Cleanup the BroadcastReceiver. 1463 unregisterReceiver(mHttpProgressReceiver); 1464 1465 cleanupContactInfoCursor(); 1466 } 1467 1468 @Override 1469 protected void onDestroy() { 1470 if (TRACE) { 1471 android.os.Debug.stopMethodTracing(); 1472 } 1473 1474 super.onDestroy(); 1475 } 1476 1477 @Override 1478 public void onConfigurationChanged(Configuration newConfig) { 1479 super.onConfigurationChanged(newConfig); 1480 if (LOCAL_LOGV) { 1481 Log.v(TAG, "onConfigurationChanged: " + newConfig); 1482 } 1483 1484 mIsKeyboardOpen = newConfig.keyboardHidden == KEYBOARDHIDDEN_NO; 1485 mIsLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE; 1486 onKeyboardStateChanged(mIsKeyboardOpen); 1487 } 1488 1489 private void onKeyboardStateChanged(boolean isKeyboardOpen) { 1490 // If the keyboard is hidden, don't show focus highlights for 1491 // things that cannot receive input. 1492 if (isKeyboardOpen) { 1493 if (mRecipientsEditor != null) { 1494 mRecipientsEditor.setFocusableInTouchMode(true); 1495 } 1496 if (mSubjectTextEditor != null) { 1497 mSubjectTextEditor.setFocusableInTouchMode(true); 1498 } 1499 mTextEditor.setFocusableInTouchMode(true); 1500 mTextEditor.setHint(R.string.type_to_compose_text_enter_to_send); 1501 initFocus(); 1502 } else { 1503 if (mRecipientsEditor != null) { 1504 mRecipientsEditor.setFocusable(false); 1505 } 1506 if (mSubjectTextEditor != null) { 1507 mSubjectTextEditor.setFocusable(false); 1508 } 1509 mTextEditor.setFocusable(false); 1510 mTextEditor.setHint(R.string.open_keyboard_to_compose_message); 1511 } 1512 } 1513 1514 @Override 1515 public void onUserInteraction() { 1516 checkPendingNotification(); 1517 } 1518 1519 @Override 1520 public void onWindowFocusChanged(boolean hasFocus) { 1521 if (hasFocus) { 1522 checkPendingNotification(); 1523 } 1524 } 1525 1526 @Override 1527 public boolean onKeyDown(int keyCode, KeyEvent event) { 1528 switch (keyCode) { 1529 case KeyEvent.KEYCODE_DEL: 1530 if ((mMsgListAdapter != null) && mMsgListView.isFocused()) { 1531 Cursor cursor; 1532 try { 1533 cursor = (Cursor) mMsgListView.getSelectedItem(); 1534 } catch (ClassCastException e) { 1535 Log.e(TAG, "Unexpected ClassCastException.", e); 1536 return super.onKeyDown(keyCode, event); 1537 } 1538 1539 if (cursor != null) { 1540 DeleteMessageListener l = new DeleteMessageListener( 1541 cursor.getLong(COLUMN_ID), 1542 cursor.getString(COLUMN_MSG_TYPE)); 1543 confirmDeleteDialog(l, false); 1544 return true; 1545 } 1546 } 1547 break; 1548 case KeyEvent.KEYCODE_DPAD_CENTER: 1549 case KeyEvent.KEYCODE_ENTER: 1550 if (isPreparedForSending()) { 1551 confirmSendMessageIfNeeded(); 1552 return true; 1553 } 1554 break; 1555 case KeyEvent.KEYCODE_BACK: 1556 exitComposeMessageActivity(new Runnable() { 1557 public void run() { 1558 finish(); 1559 } 1560 }); 1561 return true; 1562 } 1563 1564 return super.onKeyDown(keyCode, event); 1565 } 1566 1567 private void exitComposeMessageActivity(final Runnable exit) { 1568 // If the message is empty, just quit -- finishing the 1569 // activity will cause an empty draft to be deleted. 1570 if (!mWorkingMessage.isWorthSaving()) { 1571 exit.run(); 1572 return; 1573 } 1574 1575 if (isRecipientsEditorVisible() 1576 && !mRecipientsEditor.hasValidRecipient()) { 1577 MessageUtils.showDiscardDraftConfirmDialog(this, 1578 new DiscardDraftListener()); 1579 return; 1580 } 1581 1582 mToastForDraftSave = true; 1583 exit.run(); 1584 } 1585 1586 private void goToConversationList() { 1587 finish(); 1588 startActivity(new Intent(this, ConversationList.class)); 1589 } 1590 1591 private boolean isRecipientsEditorVisible() { 1592 return (null != mRecipientsEditor) 1593 && (View.VISIBLE == mRecipientsEditor.getVisibility()); 1594 } 1595 1596 private boolean isSubjectEditorVisible() { 1597 return (null != mSubjectTextEditor) 1598 && (View.VISIBLE == mSubjectTextEditor.getVisibility()); 1599 } 1600 1601 public void onAttachmentChanged() { 1602 drawBottomPanel(); 1603 updateSendButtonState(); 1604 mAttachmentEditor.update(mWorkingMessage); 1605 } 1606 1607 public void onProtocolChanged(boolean mms) { 1608 toastConvertInfo(mms); 1609 } 1610 1611 public void onMessageSent() { 1612 // If we already have messages in the list adapter, it 1613 // will be auto-requerying; don't thrash another query in. 1614 if (mMsgListAdapter.getCount() == 0) { 1615 startMsgListQuery(); 1616 } 1617 } 1618 1619 // We don't want to show the "call" option unless there is only one 1620 // recipient and it's a phone number. 1621 private boolean isRecipientCallable() { 1622 ContactList recipients = getRecipients(); 1623 return (recipients.size() == 1 && !recipients.containsEmail()); 1624 } 1625 1626 private void dialRecipient() { 1627 String number = getRecipients().get(0).getNumber(); 1628 Intent dialIntent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + number)); 1629 startActivity(dialIntent); 1630 } 1631 1632 @Override 1633 public boolean onPrepareOptionsMenu(Menu menu) { 1634 menu.clear(); 1635 1636 if (isRecipientCallable()) { 1637 menu.add(0, MENU_CALL_RECIPIENT, 0, R.string.menu_call).setIcon( 1638 com.android.internal.R.drawable.ic_menu_call); 1639 } 1640 1641 // Only add the "View contact" menu item when there's a single recipient and that 1642 // recipient is someone in contacts. 1643 ContactList recipients = getRecipients(); 1644 if (recipients.size() == 1 && recipients.get(0).existsInDatabase()) { 1645 menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact).setIcon( 1646 R.drawable.ic_menu_contact); 1647 } 1648 1649 if (MmsConfig.getMmsEnabled()) { 1650 if (!isSubjectEditorVisible()) { 1651 menu.add(0, MENU_ADD_SUBJECT, 0, R.string.add_subject).setIcon( 1652 com.android.internal.R.drawable.ic_menu_edit); 1653 } 1654 1655 if (!mWorkingMessage.hasAttachment()) { 1656 menu.add(0, MENU_ADD_ATTACHMENT, 0, R.string.add_attachment).setIcon( 1657 R.drawable.ic_menu_attachment); 1658 } 1659 } 1660 1661 if (isPreparedForSending()) { 1662 menu.add(0, MENU_SEND, 0, R.string.send).setIcon(android.R.drawable.ic_menu_send); 1663 } 1664 1665 menu.add(0, MENU_INSERT_SMILEY, 0, R.string.menu_insert_smiley).setIcon( 1666 com.android.internal.R.drawable.ic_menu_emoticons); 1667 1668 if (mMsgListAdapter.getCount() > 0) { 1669 // Removed search as part of b/1205708 1670 //menu.add(0, MENU_SEARCH, 0, R.string.menu_search).setIcon( 1671 // R.drawable.ic_menu_search); 1672 Cursor cursor = mMsgListAdapter.getCursor(); 1673 if ((null != cursor) && (cursor.getCount() > 0)) { 1674 menu.add(0, MENU_DELETE_THREAD, 0, R.string.delete_thread).setIcon( 1675 android.R.drawable.ic_menu_delete); 1676 } 1677 } else { 1678 menu.add(0, MENU_DISCARD, 0, R.string.discard).setIcon(android.R.drawable.ic_menu_delete); 1679 } 1680 1681 menu.add(0, MENU_CONVERSATION_LIST, 0, R.string.all_threads).setIcon( 1682 com.android.internal.R.drawable.ic_menu_friendslist); 1683 1684 buildAddAddressToContactMenuItem(menu); 1685 return true; 1686 } 1687 1688 private void buildAddAddressToContactMenuItem(Menu menu) { 1689 // Look for the first recipient we don't have a contact for and create a menu item to 1690 // add the number to contacts. 1691 for (Contact c : getRecipients()) { 1692 if (!c.existsInDatabase()) { 1693 Intent intent = ConversationList.createAddContactIntent(c.getNumber()); 1694 menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, R.string.menu_add_to_contacts) 1695 .setIcon(android.R.drawable.ic_menu_add) 1696 .setIntent(intent); 1697 break; 1698 } 1699 } 1700 } 1701 1702 @Override 1703 public boolean onOptionsItemSelected(MenuItem item) { 1704 switch (item.getItemId()) { 1705 case MENU_ADD_SUBJECT: 1706 showSubjectEditor(true); 1707 mWorkingMessage.setSubject(""); 1708 mSubjectTextEditor.requestFocus(); 1709 break; 1710 case MENU_ADD_ATTACHMENT: 1711 // Launch the add-attachment list dialog 1712 showAddAttachmentDialog(); 1713 break; 1714 case MENU_DISCARD: 1715 mWorkingMessage.discard(); 1716 finish(); 1717 break; 1718 case MENU_SEND: 1719 if (isPreparedForSending()) { 1720 confirmSendMessageIfNeeded(); 1721 } 1722 break; 1723 case MENU_SEARCH: 1724 onSearchRequested(); 1725 break; 1726 case MENU_DELETE_THREAD: 1727 DeleteMessageListener l = new DeleteMessageListener( 1728 mConversation.getUri(), true); 1729 confirmDeleteDialog(l, true); 1730 break; 1731 case MENU_CONVERSATION_LIST: 1732 exitComposeMessageActivity(new Runnable() { 1733 public void run() { 1734 goToConversationList(); 1735 } 1736 }); 1737 break; 1738 case MENU_CALL_RECIPIENT: 1739 dialRecipient(); 1740 break; 1741 case MENU_INSERT_SMILEY: 1742 showSmileyDialog(); 1743 break; 1744 case MENU_VIEW_CONTACT: { 1745 // View the contact for the first (and only) recipient. 1746 ContactList list = getRecipients(); 1747 if (list.size() == 1 && list.get(0).existsInDatabase()) { 1748 Uri contactUri = list.get(0).getUri(); 1749 startActivity(new Intent(Intent.ACTION_VIEW, contactUri)); 1750 } 1751 break; 1752 } 1753 case MENU_ADD_ADDRESS_TO_CONTACTS: 1754 return false; // so the intent attached to the menu item will get launched. 1755 } 1756 1757 return true; 1758 } 1759 1760 private void addAttachment(int type) { 1761 switch (type) { 1762 case AttachmentTypeSelectorAdapter.ADD_IMAGE: 1763 MessageUtils.selectImage(this, REQUEST_CODE_ATTACH_IMAGE); 1764 break; 1765 1766 case AttachmentTypeSelectorAdapter.TAKE_PICTURE: { 1767 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 1768 startActivityForResult(intent, REQUEST_CODE_TAKE_PICTURE); 1769 } 1770 break; 1771 1772 case AttachmentTypeSelectorAdapter.ADD_VIDEO: 1773 MessageUtils.selectVideo(this, REQUEST_CODE_ATTACH_VIDEO); 1774 break; 1775 1776 case AttachmentTypeSelectorAdapter.RECORD_VIDEO: { 1777 Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); 1778 intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0); 1779 startActivityForResult(intent, REQUEST_CODE_TAKE_VIDEO); 1780 } 1781 break; 1782 1783 case AttachmentTypeSelectorAdapter.ADD_SOUND: 1784 MessageUtils.selectAudio(this, REQUEST_CODE_ATTACH_SOUND); 1785 break; 1786 1787 case AttachmentTypeSelectorAdapter.RECORD_SOUND: 1788 MessageUtils.recordSound(this, REQUEST_CODE_RECORD_SOUND); 1789 break; 1790 1791 case AttachmentTypeSelectorAdapter.ADD_SLIDESHOW: 1792 editSlideshow(); 1793 break; 1794 1795 default: 1796 break; 1797 } 1798 } 1799 1800 private void showAddAttachmentDialog() { 1801 AlertDialog.Builder builder = new AlertDialog.Builder(this); 1802 builder.setIcon(R.drawable.ic_dialog_attach); 1803 builder.setTitle(R.string.add_attachment); 1804 1805 AttachmentTypeSelectorAdapter adapter = new AttachmentTypeSelectorAdapter( 1806 this, AttachmentTypeSelectorAdapter.MODE_WITH_SLIDESHOW); 1807 1808 builder.setAdapter(adapter, new DialogInterface.OnClickListener() { 1809 public void onClick(DialogInterface dialog, int which) { 1810 addAttachment(which); 1811 } 1812 }); 1813 1814 builder.show(); 1815 } 1816 1817 @Override 1818 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 1819 if (LOCAL_LOGV) { 1820 Log.v(TAG, "onActivityResult: requestCode=" + requestCode 1821 + ", resultCode=" + resultCode + ", data=" + data); 1822 } 1823 mWaitingForSubActivity = false; // We're back! 1824 1825 // If there's no data (because the user didn't select a picture and 1826 // just hit BACK, for example), there's nothing to do. 1827 if (data == null) { 1828 return; 1829 } 1830 1831 switch(requestCode) { 1832 case REQUEST_CODE_CREATE_SLIDESHOW: 1833 if (data != null) { 1834 mWorkingMessage = WorkingMessage.load(this, data.getData()); 1835 mWorkingMessage.setConversation(mConversation); 1836 mAttachmentEditor.update(mWorkingMessage); 1837 drawTopPanel(); 1838 } 1839 break; 1840 1841 case REQUEST_CODE_TAKE_PICTURE: 1842 Bitmap bitmap = (Bitmap) data.getParcelableExtra("data"); 1843 if (bitmap == null) { 1844 handleAddAttachmentError(WorkingMessage.UNKNOWN_ERROR, R.string.type_picture); 1845 return; 1846 } 1847 addImage(bitmap); 1848 break; 1849 1850 case REQUEST_CODE_ATTACH_IMAGE: 1851 addImage(data.getData()); 1852 break; 1853 1854 case REQUEST_CODE_TAKE_VIDEO: 1855 case REQUEST_CODE_ATTACH_VIDEO: 1856 addVideo(data.getData()); 1857 break; 1858 1859 case REQUEST_CODE_ATTACH_SOUND: 1860 Uri uri = (Uri) data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); 1861 if (Settings.System.DEFAULT_RINGTONE_URI.equals(uri)) { 1862 break; 1863 } 1864 addAudio(uri); 1865 break; 1866 1867 case REQUEST_CODE_RECORD_SOUND: 1868 addAudio(data.getData()); 1869 break; 1870 1871 default: 1872 // TODO 1873 break; 1874 } 1875 } 1876 1877 private void addImage(Bitmap bitmap) { 1878 try { 1879 Uri messageUri = mWorkingMessage.saveAsMms(); 1880 addImage(MessageUtils.saveBitmapAsPart(this, messageUri, bitmap)); 1881 } catch (MmsException e) { 1882 handleAddAttachmentError(WorkingMessage.UNKNOWN_ERROR, R.string.type_picture); 1883 } 1884 } 1885 1886 private final ResizeImageResultCallback mResizeImageCallback = new ResizeImageResultCallback() { 1887 // TODO: make this produce a Uri, that's what we want anyway 1888 public void onResizeResult(PduPart part) { 1889 if (part == null) { 1890 handleAddAttachmentError(WorkingMessage.UNKNOWN_ERROR, R.string.type_picture); 1891 return; 1892 } 1893 1894 Context context = ComposeMessageActivity.this; 1895 PduPersister persister = PduPersister.getPduPersister(context); 1896 int result; 1897 1898 Uri messageUri = mWorkingMessage.saveAsMms(); 1899 try { 1900 Uri dataUri = persister.persistPart(part, ContentUris.parseId(messageUri)); 1901 result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, dataUri); 1902 } catch (MmsException e) { 1903 result = WorkingMessage.UNKNOWN_ERROR; 1904 } 1905 1906 handleAddAttachmentError(result, R.string.type_picture); 1907 } 1908 }; 1909 1910 private void handleAddAttachmentError(int error, int mediaTypeStringId) { 1911 if (error == WorkingMessage.OK) { 1912 return; 1913 } 1914 1915 Resources res = getResources(); 1916 String mediaType = res.getString(mediaTypeStringId); 1917 String title, message; 1918 1919 switch(error) { 1920 case WorkingMessage.UNKNOWN_ERROR: 1921 message = res.getString(R.string.failed_to_add_media, mediaType); 1922 Toast.makeText(this, message, Toast.LENGTH_SHORT); 1923 return; 1924 case WorkingMessage.UNSUPPORTED_TYPE: 1925 title = res.getString(R.string.unsupported_media_format, mediaType); 1926 message = res.getString(R.string.select_different_media, mediaType); 1927 break; 1928 case WorkingMessage.MESSAGE_SIZE_EXCEEDED: 1929 title = res.getString(R.string.exceed_message_size_limitation, mediaType); 1930 message = res.getString(R.string.failed_to_add_media, mediaType); 1931 break; 1932 case WorkingMessage.IMAGE_TOO_LARGE: 1933 title = res.getString(R.string.failed_to_resize_image); 1934 message = res.getString(R.string.resize_image_error_information); 1935 break; 1936 default: 1937 throw new IllegalArgumentException("unknown error " + error); 1938 } 1939 1940 MessageUtils.showErrorDialog(this, title, message); 1941 } 1942 1943 private void addImage(Uri uri) { 1944 int result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, uri); 1945 1946 if (result == WorkingMessage.IMAGE_TOO_LARGE) { 1947 MessageUtils.resizeImageAsync(this, 1948 uri, mAttachmentEditorHandler, mResizeImageCallback); 1949 return; 1950 } 1951 handleAddAttachmentError(result, R.string.type_picture); 1952 } 1953 1954 private void addVideo(Uri uri) { 1955 int result = mWorkingMessage.setAttachment(WorkingMessage.VIDEO, uri); 1956 handleAddAttachmentError(result, R.string.type_video); 1957 } 1958 1959 private void addAudio(Uri uri) { 1960 int result = mWorkingMessage.setAttachment(WorkingMessage.AUDIO, uri); 1961 handleAddAttachmentError(result, R.string.type_audio); 1962 } 1963 1964 private boolean handleForwardedMessage() { 1965 Intent intent = getIntent(); 1966 1967 // If this is a forwarded message, it will have an Intent extra 1968 // indicating so. If not, bail out. 1969 if (intent.getBooleanExtra("forwarded_message", false) == false) { 1970 return false; 1971 } 1972 1973 Uri uri = intent.getParcelableExtra("msg_uri"); 1974 if (uri != null) { 1975 mWorkingMessage = WorkingMessage.load(this, uri); 1976 mWorkingMessage.setSubject(intent.getStringExtra("subject")); 1977 } else { 1978 mWorkingMessage.setText(intent.getStringExtra("sms_body")); 1979 } 1980 1981 return true; 1982 } 1983 1984 private boolean handleSendIntent(Intent intent) { 1985 Bundle extras = intent.getExtras(); 1986 1987 if (!Intent.ACTION_SEND.equals(intent.getAction()) || (extras == null)) { 1988 return false; 1989 } 1990 1991 if (extras.containsKey(Intent.EXTRA_STREAM)) { 1992 Uri uri = (Uri)extras.getParcelable(Intent.EXTRA_STREAM); 1993 if (uri != null) { 1994 if (intent.getType().startsWith("image/")) { 1995 addImage(uri); 1996 } else if (intent.getType().startsWith("video/")) { 1997 addVideo(uri); 1998 } 1999 } 2000 return true; 2001 } else if (extras.containsKey(Intent.EXTRA_TEXT)) { 2002 mWorkingMessage.setText(extras.getString(Intent.EXTRA_TEXT)); 2003 return true; 2004 } 2005 2006 return false; 2007 } 2008 2009 private String getResourcesString(int id, String mediaName) { 2010 Resources r = getResources(); 2011 return r.getString(id, mediaName); 2012 } 2013 2014 private void drawBottomPanel() { 2015 // Reset the counter for text editor. 2016 resetCounter(); 2017 2018 if (mWorkingMessage.hasSlideshow()) { 2019 mBottomPanel.setVisibility(View.GONE); 2020 mAttachmentEditor.requestFocus(); 2021 return; 2022 } 2023 2024 mBottomPanel.setVisibility(View.VISIBLE); 2025 mTextEditor.setText(mWorkingMessage.getText()); 2026 } 2027 2028 private void drawTopPanel() { 2029 showSubjectEditor(mWorkingMessage.hasSubject()); 2030 } 2031 2032 //========================================================== 2033 // Interface methods 2034 //========================================================== 2035 2036 public void onClick(View v) { 2037 if ((v == mSendButton) && isPreparedForSending()) { 2038 confirmSendMessageIfNeeded(); 2039 } 2040 } 2041 2042 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 2043 if (event != null) { 2044 if (!event.isShiftPressed()) { 2045 if (isPreparedForSending()) { 2046 sendMessage(); 2047 } 2048 return true; 2049 } 2050 return false; 2051 } 2052 2053 if (isPreparedForSending()) { 2054 confirmSendMessageIfNeeded(); 2055 } 2056 return true; 2057 } 2058 2059 private final TextWatcher mTextEditorWatcher = new TextWatcher() { 2060 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 2061 } 2062 2063 public void onTextChanged(CharSequence s, int start, int before, int count) { 2064 // This is a workaround for bug 1609057. Since onUserInteraction() is 2065 // not called when the user touches the soft keyboard, we pretend it was 2066 // called when textfields changes. This should be removed when the bug 2067 // is fixed. 2068 onUserInteraction(); 2069 2070 mWorkingMessage.setText(s); 2071 2072 updateSendButtonState(); 2073 2074 updateCounter(s, start, before, count); 2075 } 2076 2077 public void afterTextChanged(Editable s) { 2078 } 2079 }; 2080 2081 private final TextWatcher mSubjectEditorWatcher = new TextWatcher() { 2082 public void beforeTextChanged(CharSequence s, int start, int count, int after) { } 2083 2084 public void onTextChanged(CharSequence s, int start, int before, int count) { 2085 mWorkingMessage.setSubject(s); 2086 } 2087 2088 public void afterTextChanged(Editable s) { } 2089 }; 2090 2091 //========================================================== 2092 // Private methods 2093 //========================================================== 2094 2095 /** 2096 * Initialize all UI elements from resources. 2097 */ 2098 private void initResourceRefs() { 2099 mMsgListView = (MessageListView) findViewById(R.id.history); 2100 mMsgListView.setDivider(null); // no divider so we look like IM conversation. 2101 mBottomPanel = findViewById(R.id.bottom_panel); 2102 mTextEditor = (EditText) findViewById(R.id.embedded_text_editor); 2103 mTextEditor.setOnEditorActionListener(this); 2104 mTextEditor.addTextChangedListener(mTextEditorWatcher); 2105 mTextCounter = (TextView) findViewById(R.id.text_counter); 2106 mSendButton = (Button) findViewById(R.id.send_button); 2107 mSendButton.setOnClickListener(this); 2108 mTopPanel = findViewById(R.id.recipients_subject_linear); 2109 mTopPanel.setFocusable(false); 2110 mAttachmentEditor = (AttachmentEditor) findViewById(R.id.attachment_editor); 2111 mAttachmentEditor.setHandler(mAttachmentEditorHandler); 2112 } 2113 2114 private void confirmDeleteDialog(OnClickListener listener, boolean allMessages) { 2115 AlertDialog.Builder builder = new AlertDialog.Builder(this); 2116 builder.setTitle(R.string.confirm_dialog_title); 2117 builder.setIcon(android.R.drawable.ic_dialog_alert); 2118 builder.setCancelable(true); 2119 builder.setMessage(allMessages 2120 ? R.string.confirm_delete_conversation 2121 : R.string.confirm_delete_message); 2122 builder.setPositiveButton(R.string.yes, listener); 2123 builder.setNegativeButton(R.string.no, null); 2124 builder.show(); 2125 } 2126 2127 void undeliveredMessageDialog(long date) { 2128 String body; 2129 LinearLayout dialog = (LinearLayout) LayoutInflater.from(this).inflate( 2130 R.layout.retry_sending_dialog, null); 2131 2132 if (date >= 0) { 2133 body = getString(R.string.undelivered_msg_dialog_body, 2134 MessageUtils.formatTimeStampString(this, date)); 2135 } else { 2136 // FIXME: we can not get sms retry time. 2137 body = getString(R.string.undelivered_sms_dialog_body); 2138 } 2139 2140 ((TextView) dialog.findViewById(R.id.body_text_view)).setText(body); 2141 2142 Toast undeliveredDialog = new Toast(this); 2143 undeliveredDialog.setView(dialog); 2144 undeliveredDialog.setDuration(Toast.LENGTH_LONG); 2145 undeliveredDialog.show(); 2146 } 2147 2148 private void startMsgListQuery() { 2149 Uri conversationUri = mConversation.getUri(); 2150 if (conversationUri == null) { 2151 return; 2152 } 2153 2154 // Cancel any pending queries 2155 mBackgroundQueryHandler.cancelOperation(MESSAGE_LIST_QUERY_TOKEN); 2156 try { 2157 // Kick off the new query 2158 mBackgroundQueryHandler.startQuery( 2159 MESSAGE_LIST_QUERY_TOKEN, null, conversationUri, 2160 PROJECTION, null, null, null); 2161 } catch (SQLiteException e) { 2162 SqliteWrapper.checkSQLiteException(this, e); 2163 } 2164 } 2165 2166 private void initMessageList() { 2167 if (mMsgListAdapter != null) { 2168 return; 2169 } 2170 2171 // Initialize the list adapter with a null cursor. 2172 mMsgListAdapter = new MessageListAdapter(this, null, mMsgListView, true); 2173 mMsgListAdapter.setOnDataSetChangedListener(mDataSetChangedListener); 2174 mMsgListAdapter.setMsgListItemHandler(mMessageListItemHandler); 2175 mMsgListView.setAdapter(mMsgListAdapter); 2176 mMsgListView.setItemsCanFocus(false); 2177 mMsgListView.setVisibility(View.VISIBLE); 2178 mMsgListView.setOnCreateContextMenuListener(mMsgListMenuCreateListener); 2179 mMsgListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 2180 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 2181 ((MessageListItem) view).onMessageListItemClick(); 2182 } 2183 }); 2184 } 2185 2186 private void loadDraft() { 2187 if (mWorkingMessage.isWorthSaving()) { 2188 Log.w(TAG, "loadDraft() called with non-empty working message"); 2189 return; 2190 } 2191 2192 mWorkingMessage = WorkingMessage.loadDraft(this, mConversation); 2193 } 2194 2195 private void saveDraft() { 2196 // TODO: Do something better here. Maybe make discard() legal 2197 // to call twice and make isEmpty() return true if discarded 2198 // so it is caught in the clause above this one? 2199 if (mWorkingMessage.isDiscarded()) { 2200 return; 2201 } 2202 2203 if (!mWaitingForSubActivity && !mWorkingMessage.isWorthSaving()) { 2204 mWorkingMessage.discard(); 2205 return; 2206 } 2207 2208 mWorkingMessage.saveDraft(); 2209 2210 if (mToastForDraftSave) { 2211 Toast.makeText(this, R.string.message_saved_as_draft, 2212 Toast.LENGTH_SHORT).show(); 2213 } 2214 } 2215 2216 private boolean isPreparedForSending() { 2217 boolean hasRecipient; 2218 2219 // To avoid creating a bunch of invalid Contacts when the recipients 2220 // editor is in flux, we keep the recipients list empty. So if the 2221 // recipients editor is showing, see if there is anything in it rather 2222 // than consulting the empty recipient list. 2223 if (isRecipientsEditorVisible()) { 2224 hasRecipient = mRecipientsEditor.getText().length() > 0; 2225 } else { 2226 hasRecipient = getRecipients().size() > 0; 2227 } 2228 2229 return hasRecipient && (mWorkingMessage.hasAttachment() || mWorkingMessage.hasText()); 2230 } 2231 2232 private void sendMessage() { 2233 mWorkingMessage.send(); 2234 2235 // Reset the UI to be ready for the next message. 2236 resetMessage(); 2237 2238 // But bail out if we are supposed to exit after the message is sent. 2239 if (mExitOnSent) { 2240 finish(); 2241 } 2242 } 2243 2244 private void resetMessage() { 2245 // Make the attachment editor hide its view. 2246 mAttachmentEditor.hideView(); 2247 2248 // Hide the subject editor. 2249 showSubjectEditor(false); 2250 2251 // Focus to the text editor. 2252 mTextEditor.requestFocus(); 2253 2254 // We have to remove the text change listener while the text editor gets cleared and 2255 // we subsequently turn the message back into SMS. When the listener is listening while 2256 // doing the clearing, it's fighting to update its counts and itself try and turn 2257 // the message one way or the other. 2258 mTextEditor.removeTextChangedListener(mTextEditorWatcher); 2259 2260 // Clear the text box. 2261 TextKeyListener.clear(mTextEditor.getText()); 2262 2263 mWorkingMessage = WorkingMessage.createEmpty(this); 2264 mWorkingMessage.setConversation(mConversation); 2265 2266 // Hide the recipients editor. 2267 if (mRecipientsEditor != null) { 2268 mRecipientsEditor.setVisibility(View.GONE); 2269 hideOrShowTopPanel(); 2270 } 2271 2272 drawBottomPanel(); 2273 updateWindowTitle(); 2274 2275 // "Or not", in this case. 2276 updateSendButtonState(); 2277 2278 // Our changes are done. Let the listener respond to text changes once again. 2279 mTextEditor.addTextChangedListener(mTextEditorWatcher); 2280 2281 // Close the soft on-screen keyboard if we're in landscape mode so the user can see the 2282 // conversation. 2283 if (mIsLandscape) { 2284 InputMethodManager inputMethodManager = 2285 (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); 2286 2287 inputMethodManager.hideSoftInputFromWindow(mTextEditor.getWindowToken(), 0); 2288 } 2289 } 2290 2291 private void updateSendButtonState() { 2292 boolean enable = false; 2293 if (isPreparedForSending()) { 2294 // When the type of attachment is slideshow, we should 2295 // also hide the 'Send' button since the slideshow view 2296 // already has a 'Send' button embedded. 2297 if (!mWorkingMessage.hasSlideshow()) { 2298 enable = true; 2299 } else { 2300 mAttachmentEditor.setCanSend(true); 2301 } 2302 } else if (null != mAttachmentEditor){ 2303 mAttachmentEditor.setCanSend(false); 2304 } 2305 2306 mSendButton.setEnabled(enable); 2307 mSendButton.setFocusable(enable); 2308 } 2309 2310 private long getMessageDate(Uri uri) { 2311 if (uri != null) { 2312 Cursor cursor = SqliteWrapper.query(this, mContentResolver, 2313 uri, new String[] { Mms.DATE }, null, null, null); 2314 if (cursor != null) { 2315 try { 2316 if ((cursor.getCount() == 1) && cursor.moveToFirst()) { 2317 return cursor.getLong(0) * 1000L; 2318 } 2319 } finally { 2320 cursor.close(); 2321 } 2322 } 2323 } 2324 return NO_DATE_FOR_DIALOG; 2325 } 2326 2327 private void initActivityState(Bundle bundle, Intent intent) { 2328 if (bundle != null) { 2329 String recipients = bundle.getString("recipients"); 2330 mConversation = Conversation.get(this, ContactList.getByNumbers(recipients, false)); 2331 mExitOnSent = bundle.getBoolean("exit_on_sent", false); 2332 mWorkingMessage.readStateFromBundle(bundle); 2333 return; 2334 } 2335 2336 // If we have been passed a thread_id, use that to find our 2337 // conversation. 2338 long threadId = intent.getLongExtra("thread_id", 0); 2339 if (threadId > 0) { 2340 mConversation = Conversation.get(this, threadId); 2341 } else { 2342 // Otherwise, try to get a conversation based on the 2343 // data URI passed to our intent. 2344 mConversation = Conversation.get(this, intent.getData()); 2345 } 2346 2347 mExitOnSent = intent.getBooleanExtra("exit_on_sent", false); 2348 mWorkingMessage.setText(intent.getStringExtra("sms_body")); 2349 mWorkingMessage.setSubject(intent.getStringExtra("subject")); 2350 } 2351 2352 private void updateWindowTitle() { 2353 ContactList recipients = getRecipients(); 2354 if (recipients.size() > 0) { 2355 setTitle(recipients.formatNamesAndNumbers(", ")); 2356 } else { 2357 setTitle(getString(R.string.compose_title)); 2358 } 2359 } 2360 2361 private void initFocus() { 2362 if (!mIsKeyboardOpen) { 2363 return; 2364 } 2365 2366 // If the recipients editor is visible, there is nothing in it, 2367 // and the text editor is not already focused, focus the 2368 // recipients editor. 2369 if (isRecipientsEditorVisible() && TextUtils.isEmpty(mRecipientsEditor.getText()) 2370 && !mTextEditor.isFocused()) { 2371 mRecipientsEditor.requestFocus(); 2372 return; 2373 } 2374 2375 // If we decided not to focus the recipients editor, focus the text editor. 2376 mTextEditor.requestFocus(); 2377 } 2378 2379 private final MessageListAdapter.OnDataSetChangedListener 2380 mDataSetChangedListener = new MessageListAdapter.OnDataSetChangedListener() { 2381 public void onDataSetChanged(MessageListAdapter adapter) { 2382 mPossiblePendingNotification = true; 2383 } 2384 }; 2385 2386 private void checkPendingNotification() { 2387 if (mPossiblePendingNotification && hasWindowFocus()) { 2388 mConversation.markAsRead(); 2389 mPossiblePendingNotification = false; 2390 } 2391 } 2392 2393 private final class BackgroundQueryHandler extends AsyncQueryHandler { 2394 public BackgroundQueryHandler(ContentResolver contentResolver) { 2395 super(contentResolver); 2396 } 2397 2398 @Override 2399 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 2400 switch(token) { 2401 case MESSAGE_LIST_QUERY_TOKEN: 2402 mMsgListAdapter.changeCursor(cursor); 2403 2404 // Once we have completed the query for the message history, if 2405 // there is nothing in the cursor and we are not composing a new 2406 // message, we must be editing a draft in a new conversation. 2407 // Show the recipients editor to give the user a chance to add 2408 // more people before the conversation begins. 2409 if (cursor.getCount() == 0 && !isRecipientsEditorVisible()) { 2410 initRecipientsEditor(); 2411 } 2412 2413 // FIXME: freshing layout changes the focused view to an unexpected 2414 // one, set it back to TextEditor forcely. 2415 mTextEditor.requestFocus(); 2416 2417 return; 2418 2419 case CALLER_ID_QUERY_TOKEN: 2420 case EMAIL_CONTACT_QUERY_TOKEN: 2421 cleanupContactInfoCursor(); 2422 mContactInfoCursor = cursor; 2423 updateContactInfo(); 2424 startPresencePollingRequest(); 2425 return; 2426 2427 } 2428 } 2429 2430 @Override 2431 protected void onDeleteComplete(int token, Object cookie, int result) { 2432 switch(token) { 2433 case DELETE_MESSAGE_TOKEN: 2434 case DELETE_CONVERSATION_TOKEN: 2435 // Update the notification for new messages since they 2436 // may be deleted. 2437 MessagingNotification.updateNewMessageIndicator( 2438 ComposeMessageActivity.this); 2439 // Update the notification for failed messages since they 2440 // may be deleted. 2441 updateSendFailedNotification(); 2442 break; 2443 } 2444 2445 // If we're deleting the whole conversation, throw away 2446 // our current working message and bail. 2447 if (token == DELETE_CONVERSATION_TOKEN) { 2448 mWorkingMessage.discard(); 2449 finish(); 2450 } 2451 } 2452 2453 @Override 2454 protected void onUpdateComplete(int token, Object cookie, int result) { 2455 switch(token) { 2456 case MARK_AS_READ_TOKEN: 2457 MessagingNotification.updateAllNotifications(ComposeMessageActivity.this); 2458 break; 2459 } 2460 } 2461 } 2462 2463 private void showSmileyDialog() { 2464 if (mSmileyDialog == null) { 2465 int[] icons = SmileyParser.DEFAULT_SMILEY_RES_IDS; 2466 String[] names = getResources().getStringArray( 2467 SmileyParser.DEFAULT_SMILEY_NAMES); 2468 final String[] texts = getResources().getStringArray( 2469 SmileyParser.DEFAULT_SMILEY_TEXTS); 2470 2471 final int N = names.length; 2472 2473 List<Map<String, ?>> entries = new ArrayList<Map<String, ?>>(); 2474 for (int i = 0; i < N; i++) { 2475 // We might have different ASCII for the same icon, skip it if 2476 // the icon is already added. 2477 boolean added = false; 2478 for (int j = 0; j < i; j++) { 2479 if (icons[i] == icons[j]) { 2480 added = true; 2481 break; 2482 } 2483 } 2484 if (!added) { 2485 HashMap<String, Object> entry = new HashMap<String, Object>(); 2486 2487 entry. put("icon", icons[i]); 2488 entry. put("name", names[i]); 2489 entry.put("text", texts[i]); 2490 2491 entries.add(entry); 2492 } 2493 } 2494 2495 final SimpleAdapter a = new SimpleAdapter( 2496 this, 2497 entries, 2498 R.layout.smiley_menu_item, 2499 new String[] {"icon", "name", "text"}, 2500 new int[] {R.id.smiley_icon, R.id.smiley_name, R.id.smiley_text}); 2501 SimpleAdapter.ViewBinder viewBinder = new SimpleAdapter.ViewBinder() { 2502 public boolean setViewValue(View view, Object data, String textRepresentation) { 2503 if (view instanceof ImageView) { 2504 Drawable img = getResources().getDrawable((Integer)data); 2505 ((ImageView)view).setImageDrawable(img); 2506 return true; 2507 } 2508 return false; 2509 } 2510 }; 2511 a.setViewBinder(viewBinder); 2512 2513 AlertDialog.Builder b = new AlertDialog.Builder(this); 2514 2515 b.setTitle(getString(R.string.menu_insert_smiley)); 2516 2517 b.setCancelable(true); 2518 b.setAdapter(a, new DialogInterface.OnClickListener() { 2519 @SuppressWarnings("unchecked") 2520 public final void onClick(DialogInterface dialog, int which) { 2521 HashMap<String, Object> item = (HashMap<String, Object>) a.getItem(which); 2522 mTextEditor.append((String)item.get("text")); 2523 } 2524 }); 2525 2526 mSmileyDialog = b.create(); 2527 } 2528 2529 mSmileyDialog.show(); 2530 } 2531 2532 private void cleanupContactInfoCursor() { 2533 if (mContactInfoCursor != null) { 2534 mContactInfoCursor.close(); 2535 } 2536 } 2537 2538 private void cancelPresencePollingRequests() { 2539 mPresencePollingHandler.removeMessages(REFRESH_PRESENCE); 2540 } 2541 2542 private void startPresencePollingRequest() { 2543 mPresencePollingHandler.sendEmptyMessageDelayed(REFRESH_PRESENCE, 2544 60 * 1000); // refresh every minute 2545 } 2546 2547 private void startQueryForContactInfo() { 2548 ContactList recipients = getRecipients(); 2549 cancelPresencePollingRequests(); // make sure there are no outstanding polling requests 2550 if (recipients.size() != 1) { 2551 setPresenceIcon(0); 2552 startPresencePollingRequest(); 2553 return; 2554 } 2555 2556 String number = recipients.get(0).getNumber(); 2557 mContactInfoSelectionArgs[0] = number; 2558 2559 if (Mms.isEmailAddress(number)) { 2560 // Cancel any pending queries 2561 mBackgroundQueryHandler.cancelOperation(EMAIL_CONTACT_QUERY_TOKEN); 2562 2563 mBackgroundQueryHandler.startQuery(EMAIL_CONTACT_QUERY_TOKEN, null, 2564 METHOD_WITH_PRESENCE_URI, 2565 EMAIL_QUERY_PROJECTION, 2566 METHOD_LOOKUP, 2567 mContactInfoSelectionArgs, 2568 null); 2569 } else { 2570 // Cancel any pending queries 2571 mBackgroundQueryHandler.cancelOperation(CALLER_ID_QUERY_TOKEN); 2572 2573 mBackgroundQueryHandler.startQuery(CALLER_ID_QUERY_TOKEN, null, 2574 PHONES_WITH_PRESENCE_URI, 2575 CALLER_ID_PROJECTION, 2576 NUMBER_LOOKUP, 2577 mContactInfoSelectionArgs, 2578 null); 2579 } 2580 } 2581 2582 private void updateContactInfo() { 2583 boolean updated = false; 2584 if (mContactInfoCursor != null && mContactInfoCursor.moveToFirst()) { 2585 mPresenceStatus = mContactInfoCursor.getInt(PRESENCE_STATUS_COLUMN); 2586 if (mPresenceStatus != Contacts.People.OFFLINE) { 2587 int presenceIcon = Presence.getPresenceIconResourceId(mPresenceStatus); 2588 setPresenceIcon(presenceIcon); 2589 updated = true; 2590 } 2591 } 2592 if (!updated) { 2593 setPresenceIcon(0); 2594 } 2595 } 2596 2597} 2598 2599 2600 2601 2602 2603