ComposeMessageActivity.java revision b45f4f5d4a97f9adb4d9562026caf8b2cbfd7309
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_MMS_LOCKED; 27import static com.android.mms.ui.MessageListAdapter.COLUMN_MSG_TYPE; 28import static com.android.mms.ui.MessageListAdapter.PROJECTION; 29 30import java.io.File; 31import java.io.FileInputStream; 32import java.io.FileOutputStream; 33import java.io.IOException; 34import java.io.InputStream; 35import java.util.ArrayList; 36import java.util.HashMap; 37import java.util.List; 38import java.util.Map; 39import java.util.regex.Pattern; 40 41import android.app.Activity; 42import android.app.AlertDialog; 43import android.app.Dialog; 44import android.app.ProgressDialog; 45import android.content.ActivityNotFoundException; 46import android.content.AsyncQueryHandler; 47import android.content.BroadcastReceiver; 48import android.content.ContentResolver; 49import android.content.ContentUris; 50import android.content.ContentValues; 51import android.content.Context; 52import android.content.DialogInterface; 53import android.content.Intent; 54import android.content.IntentFilter; 55import android.content.DialogInterface.OnClickListener; 56import android.content.res.Configuration; 57import android.content.res.Resources; 58import android.database.Cursor; 59import android.database.sqlite.SQLiteException; 60import android.database.sqlite.SqliteWrapper; 61import android.drm.mobile1.DrmException; 62import android.drm.mobile1.DrmRawContent; 63import android.graphics.drawable.Drawable; 64import android.media.CamcorderProfile; 65import android.media.RingtoneManager; 66import android.net.Uri; 67import android.os.Bundle; 68import android.os.Environment; 69import android.os.Handler; 70import android.os.Message; 71import android.os.Parcelable; 72import android.os.SystemProperties; 73import android.provider.ContactsContract; 74import android.provider.DrmStore; 75import android.provider.MediaStore; 76import android.provider.Settings; 77import android.provider.ContactsContract.Intents; 78import android.provider.ContactsContract.Contacts; 79import android.provider.ContactsContract.CommonDataKinds.Email; 80import android.provider.MediaStore.Images; 81import android.provider.MediaStore.Video; 82import android.provider.Telephony.Mms; 83import android.provider.Telephony.Sms; 84import android.provider.ContactsContract.CommonDataKinds.Phone; 85import android.telephony.SmsMessage; 86import android.text.ClipboardManager; 87import android.text.Editable; 88import android.text.InputFilter; 89import android.text.SpannableString; 90import android.text.Spanned; 91import android.text.TextUtils; 92import android.text.TextWatcher; 93import android.text.method.TextKeyListener; 94import android.text.style.AbsoluteSizeSpan; 95import android.text.style.URLSpan; 96import android.text.util.Linkify; 97import android.util.Config; 98import android.util.Log; 99import android.view.ContextMenu; 100import android.view.KeyEvent; 101import android.view.LayoutInflater; 102import android.view.Menu; 103import android.view.MenuItem; 104import android.view.View; 105import android.view.ViewStub; 106import android.view.WindowManager; 107import android.view.ContextMenu.ContextMenuInfo; 108import android.view.View.OnCreateContextMenuListener; 109import android.view.View.OnKeyListener; 110import android.view.inputmethod.InputMethodManager; 111import android.webkit.MimeTypeMap; 112import android.widget.AdapterView; 113import android.widget.Button; 114import android.widget.EditText; 115import android.widget.ImageButton; 116import android.widget.ImageView; 117import android.widget.LinearLayout; 118import android.widget.ListView; 119import android.widget.SimpleAdapter; 120import android.widget.TextView; 121import android.widget.Toast; 122 123import com.android.internal.telephony.TelephonyIntents; 124import com.android.internal.telephony.TelephonyProperties; 125import com.android.mms.LogTag; 126import com.android.mms.MmsConfig; 127import com.android.mms.R; 128import com.android.mms.data.Contact; 129import com.android.mms.data.ContactList; 130import com.android.mms.data.Conversation; 131import com.android.mms.data.WorkingMessage; 132import com.android.mms.data.WorkingMessage.MessageStatusListener; 133import com.google.android.mms.ContentType; 134import com.google.android.mms.pdu.EncodedStringValue; 135import com.google.android.mms.MmsException; 136import com.google.android.mms.pdu.PduBody; 137import com.google.android.mms.pdu.PduPart; 138import com.google.android.mms.pdu.PduPersister; 139import com.google.android.mms.pdu.SendReq; 140import com.android.mms.model.SlideModel; 141import com.android.mms.model.SlideshowModel; 142import com.android.mms.transaction.MessagingNotification; 143import com.android.mms.ui.MessageUtils.ResizeImageResultCallback; 144import com.android.mms.ui.RecipientsEditor.RecipientContextMenuInfo; 145import com.android.mms.util.SendingProgressTokenManager; 146import com.android.mms.util.SmileyParser; 147 148/** 149 * This is the main UI for: 150 * 1. Composing a new message; 151 * 2. Viewing/managing message history of a conversation. 152 * 153 * This activity can handle following parameters from the intent 154 * by which it's launched. 155 * thread_id long Identify the conversation to be viewed. When creating a 156 * new message, this parameter shouldn't be present. 157 * msg_uri Uri The message which should be opened for editing in the editor. 158 * address String The addresses of the recipients in current conversation. 159 * exit_on_sent boolean Exit this activity after the message is sent. 160 */ 161public class ComposeMessageActivity extends Activity 162 implements View.OnClickListener, TextView.OnEditorActionListener, 163 MessageStatusListener, Contact.UpdateListener { 164 public static final int REQUEST_CODE_ATTACH_IMAGE = 10; 165 public static final int REQUEST_CODE_TAKE_PICTURE = 11; 166 public static final int REQUEST_CODE_ATTACH_VIDEO = 12; 167 public static final int REQUEST_CODE_TAKE_VIDEO = 13; 168 public static final int REQUEST_CODE_ATTACH_SOUND = 14; 169 public static final int REQUEST_CODE_RECORD_SOUND = 15; 170 public static final int REQUEST_CODE_CREATE_SLIDESHOW = 16; 171 public static final int REQUEST_CODE_ECM_EXIT_DIALOG = 17; 172 public static final int REQUEST_CODE_ADD_CONTACT = 18; 173 public static final int REQUEST_CODE_PICK = 19; 174 175 private static final String TAG = "Mms/compose"; 176 177 private static final boolean DEBUG = false; 178 private static final boolean TRACE = false; 179 private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; 180 181 // Menu ID 182 private static final int MENU_ADD_SUBJECT = 0; 183 private static final int MENU_DELETE_THREAD = 1; 184 private static final int MENU_ADD_ATTACHMENT = 2; 185 private static final int MENU_DISCARD = 3; 186 private static final int MENU_SEND = 4; 187 private static final int MENU_CALL_RECIPIENT = 5; 188 private static final int MENU_CONVERSATION_LIST = 6; 189 190 // Context menu ID 191 private static final int MENU_VIEW_CONTACT = 12; 192 private static final int MENU_ADD_TO_CONTACTS = 13; 193 194 private static final int MENU_EDIT_MESSAGE = 14; 195 private static final int MENU_VIEW_SLIDESHOW = 16; 196 private static final int MENU_VIEW_MESSAGE_DETAILS = 17; 197 private static final int MENU_DELETE_MESSAGE = 18; 198 private static final int MENU_SEARCH = 19; 199 private static final int MENU_DELIVERY_REPORT = 20; 200 private static final int MENU_FORWARD_MESSAGE = 21; 201 private static final int MENU_CALL_BACK = 22; 202 private static final int MENU_SEND_EMAIL = 23; 203 private static final int MENU_COPY_MESSAGE_TEXT = 24; 204 private static final int MENU_COPY_TO_SDCARD = 25; 205 private static final int MENU_INSERT_SMILEY = 26; 206 private static final int MENU_ADD_ADDRESS_TO_CONTACTS = 27; 207 private static final int MENU_LOCK_MESSAGE = 28; 208 private static final int MENU_UNLOCK_MESSAGE = 29; 209 private static final int MENU_COPY_TO_DRM_PROVIDER = 30; 210 211 private static final int RECIPIENTS_MAX_LENGTH = 312; 212 213 private static final int MESSAGE_LIST_QUERY_TOKEN = 9527; 214 215 private static final int DELETE_MESSAGE_TOKEN = 9700; 216 217 private static final int CHARS_REMAINING_BEFORE_COUNTER_SHOWN = 10; 218 219 private static final long NO_DATE_FOR_DIALOG = -1L; 220 221 private static final String EXIT_ECM_RESULT = "exit_ecm_result"; 222 223 private ContentResolver mContentResolver; 224 225 private BackgroundQueryHandler mBackgroundQueryHandler; 226 227 private Conversation mConversation; // Conversation we are working in 228 229 private boolean mExitOnSent; // Should we finish() after sending a message? 230 231 private View mTopPanel; // View containing the recipient and subject editors 232 private View mBottomPanel; // View containing the text editor, send button, ec. 233 private EditText mTextEditor; // Text editor to type your message into 234 private TextView mTextCounter; // Shows the number of characters used in text editor 235 private Button mSendButton; // Press to detonate 236 private EditText mSubjectTextEditor; // Text editor for MMS subject 237 238 private AttachmentEditor mAttachmentEditor; 239 private View mAttachmentEditorScrollView; 240 241 private MessageListView mMsgListView; // ListView for messages in this conversation 242 public MessageListAdapter mMsgListAdapter; // and its corresponding ListAdapter 243 244 private RecipientsEditor mRecipientsEditor; // UI control for editing recipients 245 private ImageButton mRecipientsPicker; // UI control for recipients picker 246 247 private boolean mIsKeyboardOpen; // Whether the hardware keyboard is visible 248 private boolean mIsLandscape; // Whether we're in landscape mode 249 250 private boolean mPossiblePendingNotification; // If the message list has changed, we may have 251 // a pending notification to deal with. 252 253 private boolean mToastForDraftSave; // Whether to notify the user that a draft is being saved 254 255 private boolean mSentMessage; // true if the user has sent a message while in this 256 // activity. On a new compose message case, when the first 257 // message is sent is a MMS w/ attachment, the list blanks 258 // for a second before showing the sent message. But we'd 259 // think the message list is empty, thus show the recipients 260 // editor thinking it's a draft message. This flag should 261 // help clarify the situation. 262 263 private WorkingMessage mWorkingMessage; // The message currently being composed. 264 265 private AlertDialog mSmileyDialog; 266 267 private boolean mWaitingForSubActivity; 268 private int mLastRecipientCount; // Used for warning the user on too many recipients. 269 private AttachmentTypeSelectorAdapter mAttachmentTypeSelectorAdapter; 270 271 private boolean mSendingMessage; // Indicates the current message is sending, and shouldn't send again. 272 273 private Intent mAddContactIntent; // Intent used to add a new contact 274 275 @SuppressWarnings("unused") 276 private static void log(String logMsg) { 277 Thread current = Thread.currentThread(); 278 long tid = current.getId(); 279 StackTraceElement[] stack = current.getStackTrace(); 280 String methodName = stack[3].getMethodName(); 281 // Prepend current thread ID and name of calling method to the message. 282 logMsg = "[" + tid + "] [" + methodName + "] " + logMsg; 283 Log.d(TAG, logMsg); 284 } 285 286 //========================================================== 287 // Inner classes 288 //========================================================== 289 290 private void editSlideshow() { 291 Uri dataUri = mWorkingMessage.saveAsMms(false); 292 Intent intent = new Intent(this, SlideshowEditActivity.class); 293 intent.setData(dataUri); 294 startActivityForResult(intent, REQUEST_CODE_CREATE_SLIDESHOW); 295 } 296 297 private final Handler mAttachmentEditorHandler = new Handler() { 298 @Override 299 public void handleMessage(Message msg) { 300 switch (msg.what) { 301 case AttachmentEditor.MSG_EDIT_SLIDESHOW: { 302 editSlideshow(); 303 break; 304 } 305 case AttachmentEditor.MSG_SEND_SLIDESHOW: { 306 if (isPreparedForSending()) { 307 ComposeMessageActivity.this.confirmSendMessageIfNeeded(); 308 } 309 break; 310 } 311 case AttachmentEditor.MSG_VIEW_IMAGE: 312 case AttachmentEditor.MSG_PLAY_VIDEO: 313 case AttachmentEditor.MSG_PLAY_AUDIO: 314 case AttachmentEditor.MSG_PLAY_SLIDESHOW: 315 MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this, 316 mWorkingMessage); 317 break; 318 319 case AttachmentEditor.MSG_REPLACE_IMAGE: 320 case AttachmentEditor.MSG_REPLACE_VIDEO: 321 case AttachmentEditor.MSG_REPLACE_AUDIO: 322 showAddAttachmentDialog(true); 323 break; 324 325 case AttachmentEditor.MSG_REMOVE_ATTACHMENT: 326 mWorkingMessage.setAttachment(WorkingMessage.TEXT, null, false); 327 break; 328 329 default: 330 break; 331 } 332 } 333 }; 334 335 private final Handler mMessageListItemHandler = new Handler() { 336 @Override 337 public void handleMessage(Message msg) { 338 String type; 339 switch (msg.what) { 340 case MessageListItem.MSG_LIST_EDIT_MMS: 341 type = "mms"; 342 break; 343 case MessageListItem.MSG_LIST_EDIT_SMS: 344 type = "sms"; 345 break; 346 default: 347 Log.w(TAG, "Unknown message: " + msg.what); 348 return; 349 } 350 351 MessageItem msgItem = getMessageItem(type, (Long) msg.obj, false); 352 if (msgItem != null) { 353 editMessageItem(msgItem); 354 drawBottomPanel(); 355 } 356 } 357 }; 358 359 private final OnKeyListener mSubjectKeyListener = new OnKeyListener() { 360 public boolean onKey(View v, int keyCode, KeyEvent event) { 361 if (event.getAction() != KeyEvent.ACTION_DOWN) { 362 return false; 363 } 364 365 // When the subject editor is empty, press "DEL" to hide the input field. 366 if ((keyCode == KeyEvent.KEYCODE_DEL) && (mSubjectTextEditor.length() == 0)) { 367 showSubjectEditor(false); 368 mWorkingMessage.setSubject(null, true); 369 return true; 370 } 371 372 return false; 373 } 374 }; 375 376 /** 377 * Return the messageItem associated with the type ("mms" or "sms") and message id. 378 * @param type Type of the message: "mms" or "sms" 379 * @param msgId Message id of the message. This is the _id of the sms or pdu row and is 380 * stored in the MessageItem 381 * @param createFromCursorIfNotInCache true if the item is not found in the MessageListAdapter's 382 * cache and the code can create a new MessageItem based on the position of the current cursor. 383 * If false, the function returns null if the MessageItem isn't in the cache. 384 * @return MessageItem or null if not found and createFromCursorIfNotInCache is false 385 */ 386 private MessageItem getMessageItem(String type, long msgId, 387 boolean createFromCursorIfNotInCache) { 388 return mMsgListAdapter.getCachedMessageItem(type, msgId, 389 createFromCursorIfNotInCache ? mMsgListAdapter.getCursor() : null); 390 } 391 392 private boolean isCursorValid() { 393 // Check whether the cursor is valid or not. 394 Cursor cursor = mMsgListAdapter.getCursor(); 395 if (cursor.isClosed() || cursor.isBeforeFirst() || cursor.isAfterLast()) { 396 Log.e(TAG, "Bad cursor.", new RuntimeException()); 397 return false; 398 } 399 return true; 400 } 401 402 private void resetCounter() { 403 mTextCounter.setText(""); 404 mTextCounter.setVisibility(View.GONE); 405 } 406 407 private void updateCounter(CharSequence text, int start, int before, int count) { 408 WorkingMessage workingMessage = mWorkingMessage; 409 if (workingMessage.requiresMms()) { 410 // If we're not removing text (i.e. no chance of converting back to SMS 411 // because of this change) and we're in MMS mode, just bail out since we 412 // then won't have to calculate the length unnecessarily. 413 final boolean textRemoved = (before > count); 414 if (!textRemoved) { 415 setSendButtonText(workingMessage.requiresMms()); 416 return; 417 } 418 } 419 420 int[] params = SmsMessage.calculateLength(text, false); 421 /* SmsMessage.calculateLength returns an int[4] with: 422 * int[0] being the number of SMS's required, 423 * int[1] the number of code units used, 424 * int[2] is the number of code units remaining until the next message. 425 * int[3] is the encoding type that should be used for the message. 426 */ 427 int msgCount = params[0]; 428 int remainingInCurrentMessage = params[2]; 429 430 // Show the counter only if: 431 // - We are not in MMS mode 432 // - We are going to send more than one message OR we are getting close 433 boolean showCounter = false; 434 if (!workingMessage.requiresMms() && 435 (msgCount > 1 || 436 remainingInCurrentMessage <= CHARS_REMAINING_BEFORE_COUNTER_SHOWN)) { 437 showCounter = true; 438 } 439 440 setSendButtonText(workingMessage.requiresMms()); 441 442 if (showCounter) { 443 // Update the remaining characters and number of messages required. 444 String counterText = msgCount > 1 ? remainingInCurrentMessage + " / " + msgCount 445 : String.valueOf(remainingInCurrentMessage); 446 mTextCounter.setText(counterText); 447 mTextCounter.setVisibility(View.VISIBLE); 448 } else { 449 mTextCounter.setVisibility(View.GONE); 450 } 451 } 452 453 @Override 454 public void startActivityForResult(Intent intent, int requestCode) 455 { 456 // requestCode >= 0 means the activity in question is a sub-activity. 457 if (requestCode >= 0) { 458 mWaitingForSubActivity = true; 459 } 460 461 super.startActivityForResult(intent, requestCode); 462 } 463 464 private void toastConvertInfo(boolean toMms) { 465 final int resId = toMms ? R.string.converting_to_picture_message 466 : R.string.converting_to_text_message; 467 Toast.makeText(this, resId, Toast.LENGTH_SHORT).show(); 468 } 469 470 private class DeleteMessageListener implements OnClickListener { 471 private final Uri mDeleteUri; 472 private final boolean mDeleteLocked; 473 474 public DeleteMessageListener(Uri uri, boolean deleteLocked) { 475 mDeleteUri = uri; 476 mDeleteLocked = deleteLocked; 477 } 478 479 public DeleteMessageListener(long msgId, String type, boolean deleteLocked) { 480 if ("mms".equals(type)) { 481 mDeleteUri = ContentUris.withAppendedId(Mms.CONTENT_URI, msgId); 482 } else { 483 mDeleteUri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgId); 484 } 485 mDeleteLocked = deleteLocked; 486 } 487 488 public void onClick(DialogInterface dialog, int whichButton) { 489 mBackgroundQueryHandler.startDelete(DELETE_MESSAGE_TOKEN, 490 null, mDeleteUri, mDeleteLocked ? null : "locked=0", null); 491 } 492 } 493 494 private class DiscardDraftListener implements OnClickListener { 495 public void onClick(DialogInterface dialog, int whichButton) { 496 mWorkingMessage.discard(); 497 finish(); 498 } 499 } 500 501 private class SendIgnoreInvalidRecipientListener implements OnClickListener { 502 public void onClick(DialogInterface dialog, int whichButton) { 503 sendMessage(true); 504 } 505 } 506 507 private class CancelSendingListener implements OnClickListener { 508 public void onClick(DialogInterface dialog, int whichButton) { 509 if (isRecipientsEditorVisible()) { 510 mRecipientsEditor.requestFocus(); 511 } 512 } 513 } 514 515 private void confirmSendMessageIfNeeded() { 516 if (!isRecipientsEditorVisible()) { 517 sendMessage(true); 518 return; 519 } 520 521 boolean isMms = mWorkingMessage.requiresMms(); 522 if (mRecipientsEditor.hasInvalidRecipient(isMms)) { 523 if (mRecipientsEditor.hasValidRecipient(isMms)) { 524 String title = getResourcesString(R.string.has_invalid_recipient, 525 mRecipientsEditor.formatInvalidNumbers(isMms)); 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(true); 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.w(TAG, 574 "RecipientsWatcher: afterTextChanged called with invisible mRecipientsEditor"); 575 return; 576 } 577 578 mWorkingMessage.setWorkingRecipients(mRecipientsEditor.getNumbers()); 579 mWorkingMessage.setHasEmail(mRecipientsEditor.containsEmail(), true); 580 581 checkForTooManyRecipients(); 582 583 // Walk backwards in the text box, skipping spaces. If the last 584 // character is a comma, update the title bar. 585 for (int pos = s.length() - 1; pos >= 0; pos--) { 586 char c = s.charAt(pos); 587 if (c == ' ') 588 continue; 589 590 if (c == ',') { 591 updateTitle(mConversation.getRecipients()); 592 } 593 594 break; 595 } 596 597 // If we have gone to zero recipients, disable send button. 598 updateSendButtonState(); 599 } 600 }; 601 602 private void checkForTooManyRecipients() { 603 final int recipientLimit = MmsConfig.getRecipientLimit(); 604 if (recipientLimit != Integer.MAX_VALUE) { 605 final int recipientCount = recipientCount(); 606 boolean tooMany = recipientCount > recipientLimit; 607 608 if (recipientCount != mLastRecipientCount) { 609 // Don't warn the user on every character they type when they're over the limit, 610 // only when the actual # of recipients changes. 611 mLastRecipientCount = recipientCount; 612 if (tooMany) { 613 String tooManyMsg = getString(R.string.too_many_recipients, recipientCount, 614 recipientLimit); 615 Toast.makeText(ComposeMessageActivity.this, 616 tooManyMsg, Toast.LENGTH_LONG).show(); 617 } 618 } 619 } 620 } 621 622 private final OnCreateContextMenuListener mRecipientsMenuCreateListener = 623 new OnCreateContextMenuListener() { 624 public void onCreateContextMenu(ContextMenu menu, View v, 625 ContextMenuInfo menuInfo) { 626 if (menuInfo != null) { 627 Contact c = ((RecipientContextMenuInfo) menuInfo).recipient; 628 RecipientsMenuClickListener l = new RecipientsMenuClickListener(c); 629 630 menu.setHeaderTitle(c.getName()); 631 632 if (c.existsInDatabase()) { 633 menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact) 634 .setOnMenuItemClickListener(l); 635 } else if (canAddToContacts(c)){ 636 menu.add(0, MENU_ADD_TO_CONTACTS, 0, R.string.menu_add_to_contacts) 637 .setOnMenuItemClickListener(l); 638 } 639 } 640 } 641 }; 642 643 private final class RecipientsMenuClickListener implements MenuItem.OnMenuItemClickListener { 644 private final Contact mRecipient; 645 646 RecipientsMenuClickListener(Contact recipient) { 647 mRecipient = recipient; 648 } 649 650 public boolean onMenuItemClick(MenuItem item) { 651 switch (item.getItemId()) { 652 // Context menu handlers for the recipients editor. 653 case MENU_VIEW_CONTACT: { 654 Uri contactUri = mRecipient.getUri(); 655 Intent intent = new Intent(Intent.ACTION_VIEW, contactUri); 656 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 657 startActivity(intent); 658 return true; 659 } 660 case MENU_ADD_TO_CONTACTS: { 661 mAddContactIntent = ConversationList.createAddContactIntent( 662 mRecipient.getNumber()); 663 ComposeMessageActivity.this.startActivityForResult(mAddContactIntent, 664 REQUEST_CODE_ADD_CONTACT); 665 return true; 666 } 667 } 668 return false; 669 } 670 } 671 672 private boolean canAddToContacts(Contact contact) { 673 // There are some kind of automated messages, like STK messages, that we don't want 674 // to add to contacts. These names begin with special characters, like, "*Info". 675 final String name = contact.getName(); 676 if (!TextUtils.isEmpty(contact.getNumber())) { 677 char c = contact.getNumber().charAt(0); 678 if (isSpecialChar(c)) { 679 return false; 680 } 681 } 682 if (!TextUtils.isEmpty(name)) { 683 char c = name.charAt(0); 684 if (isSpecialChar(c)) { 685 return false; 686 } 687 } 688 if (!(Mms.isEmailAddress(name) || Mms.isPhoneNumber(name) || 689 MessageUtils.isLocalNumber(contact.getNumber()))) { // Handle "Me" 690 return false; 691 } 692 return true; 693 } 694 695 private boolean isSpecialChar(char c) { 696 return c == '*' || c == '%' || c == '$'; 697 } 698 699 private void addPositionBasedMenuItems(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 700 AdapterView.AdapterContextMenuInfo info; 701 702 try { 703 info = (AdapterView.AdapterContextMenuInfo) menuInfo; 704 } catch (ClassCastException e) { 705 Log.e(TAG, "bad menuInfo"); 706 return; 707 } 708 final int position = info.position; 709 710 addUriSpecificMenuItems(menu, v, position); 711 } 712 713 private Uri getSelectedUriFromMessageList(ListView listView, int position) { 714 // If the context menu was opened over a uri, get that uri. 715 MessageListItem msglistItem = (MessageListItem) listView.getChildAt(position); 716 if (msglistItem == null) { 717 // FIXME: Should get the correct view. No such interface in ListView currently 718 // to get the view by position. The ListView.getChildAt(position) cannot 719 // get correct view since the list doesn't create one child for each item. 720 // And if setSelection(position) then getSelectedView(), 721 // cannot get corrent view when in touch mode. 722 return null; 723 } 724 725 TextView textView; 726 CharSequence text = null; 727 int selStart = -1; 728 int selEnd = -1; 729 730 //check if message sender is selected 731 textView = (TextView) msglistItem.findViewById(R.id.text_view); 732 if (textView != null) { 733 text = textView.getText(); 734 selStart = textView.getSelectionStart(); 735 selEnd = textView.getSelectionEnd(); 736 } 737 738 if (selStart == -1) { 739 //sender is not being selected, it may be within the message body 740 textView = (TextView) msglistItem.findViewById(R.id.body_text_view); 741 if (textView != null) { 742 text = textView.getText(); 743 selStart = textView.getSelectionStart(); 744 selEnd = textView.getSelectionEnd(); 745 } 746 } 747 748 // Check that some text is actually selected, rather than the cursor 749 // just being placed within the TextView. 750 if (selStart != selEnd) { 751 int min = Math.min(selStart, selEnd); 752 int max = Math.max(selStart, selEnd); 753 754 URLSpan[] urls = ((Spanned) text).getSpans(min, max, 755 URLSpan.class); 756 757 if (urls.length == 1) { 758 return Uri.parse(urls[0].getURL()); 759 } 760 } 761 762 //no uri was selected 763 return null; 764 } 765 766 private void addUriSpecificMenuItems(ContextMenu menu, View v, int position) { 767 Uri uri = getSelectedUriFromMessageList((ListView) v, position); 768 769 if (uri != null) { 770 Intent intent = new Intent(null, uri); 771 intent.addCategory(Intent.CATEGORY_SELECTED_ALTERNATIVE); 772 menu.addIntentOptions(0, 0, 0, 773 new android.content.ComponentName(this, ComposeMessageActivity.class), 774 null, intent, 0, null); 775 } 776 } 777 778 private final void addCallAndContactMenuItems( 779 ContextMenu menu, MsgListMenuClickListener l, MessageItem msgItem) { 780 // Add all possible links in the address & message 781 StringBuilder textToSpannify = new StringBuilder(); 782 if (msgItem.mBoxId == Mms.MESSAGE_BOX_INBOX) { 783 textToSpannify.append(msgItem.mAddress + ": "); 784 } 785 textToSpannify.append(msgItem.mBody); 786 787 SpannableString msg = new SpannableString(textToSpannify.toString()); 788 Linkify.addLinks(msg, Linkify.ALL); 789 ArrayList<String> uris = 790 MessageUtils.extractUris(msg.getSpans(0, msg.length(), URLSpan.class)); 791 792 while (uris.size() > 0) { 793 String uriString = uris.remove(0); 794 // Remove any dupes so they don't get added to the menu multiple times 795 while (uris.contains(uriString)) { 796 uris.remove(uriString); 797 } 798 799 int sep = uriString.indexOf(":"); 800 String prefix = null; 801 if (sep >= 0) { 802 prefix = uriString.substring(0, sep); 803 uriString = uriString.substring(sep + 1); 804 } 805 boolean addToContacts = false; 806 if ("mailto".equalsIgnoreCase(prefix)) { 807 String sendEmailString = getString( 808 R.string.menu_send_email).replace("%s", uriString); 809 Intent intent = new Intent(Intent.ACTION_VIEW, 810 Uri.parse("mailto:" + uriString)); 811 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 812 menu.add(0, MENU_SEND_EMAIL, 0, sendEmailString) 813 .setOnMenuItemClickListener(l) 814 .setIntent(intent); 815 addToContacts = !haveEmailContact(uriString); 816 } else if ("tel".equalsIgnoreCase(prefix)) { 817 String callBackString = getString( 818 R.string.menu_call_back).replace("%s", uriString); 819 Intent intent = new Intent(Intent.ACTION_CALL, 820 Uri.parse("tel:" + uriString)); 821 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 822 menu.add(0, MENU_CALL_BACK, 0, callBackString) 823 .setOnMenuItemClickListener(l) 824 .setIntent(intent); 825 addToContacts = !isNumberInContacts(uriString); 826 } 827 if (addToContacts) { 828 Intent intent = ConversationList.createAddContactIntent(uriString); 829 String addContactString = getString( 830 R.string.menu_add_address_to_contacts).replace("%s", uriString); 831 menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, addContactString) 832 .setOnMenuItemClickListener(l) 833 .setIntent(intent); 834 } 835 } 836 } 837 838 private boolean haveEmailContact(String emailAddress) { 839 Cursor cursor = SqliteWrapper.query(this, getContentResolver(), 840 Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(emailAddress)), 841 new String[] { Contacts.DISPLAY_NAME }, null, null, null); 842 843 if (cursor != null) { 844 try { 845 while (cursor.moveToNext()) { 846 String name = cursor.getString(0); 847 if (!TextUtils.isEmpty(name)) { 848 return true; 849 } 850 } 851 } finally { 852 cursor.close(); 853 } 854 } 855 return false; 856 } 857 858 private boolean isNumberInContacts(String phoneNumber) { 859 return Contact.get(phoneNumber, false).existsInDatabase(); 860 } 861 862 private final OnCreateContextMenuListener mMsgListMenuCreateListener = 863 new OnCreateContextMenuListener() { 864 public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { 865 if (!isCursorValid()) { 866 return; 867 } 868 Cursor cursor = mMsgListAdapter.getCursor(); 869 String type = cursor.getString(COLUMN_MSG_TYPE); 870 long msgId = cursor.getLong(COLUMN_ID); 871 872 addPositionBasedMenuItems(menu, v, menuInfo); 873 874 MessageItem msgItem = mMsgListAdapter.getCachedMessageItem(type, msgId, cursor); 875 if (msgItem == null) { 876 Log.e(TAG, "Cannot load message item for type = " + type 877 + ", msgId = " + msgId); 878 return; 879 } 880 881 menu.setHeaderTitle(R.string.message_options); 882 883 MsgListMenuClickListener l = new MsgListMenuClickListener(); 884 885 if (msgItem.mLocked) { 886 menu.add(0, MENU_UNLOCK_MESSAGE, 0, R.string.menu_unlock) 887 .setOnMenuItemClickListener(l); 888 } else { 889 menu.add(0, MENU_LOCK_MESSAGE, 0, R.string.menu_lock) 890 .setOnMenuItemClickListener(l); 891 } 892 893 if (msgItem.isMms()) { 894 switch (msgItem.mBoxId) { 895 case Mms.MESSAGE_BOX_INBOX: 896 break; 897 case Mms.MESSAGE_BOX_OUTBOX: 898 // Since we currently break outgoing messages to multiple 899 // recipients into one message per recipient, only allow 900 // editing a message for single-recipient conversations. 901 if (getRecipients().size() == 1) { 902 menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit) 903 .setOnMenuItemClickListener(l); 904 } 905 break; 906 } 907 switch (msgItem.mAttachmentType) { 908 case WorkingMessage.TEXT: 909 break; 910 case WorkingMessage.VIDEO: 911 case WorkingMessage.IMAGE: 912 if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) { 913 menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard) 914 .setOnMenuItemClickListener(l); 915 } 916 break; 917 case WorkingMessage.SLIDESHOW: 918 default: 919 menu.add(0, MENU_VIEW_SLIDESHOW, 0, R.string.view_slideshow) 920 .setOnMenuItemClickListener(l); 921 if (haveSomethingToCopyToSDCard(msgItem.mMsgId)) { 922 menu.add(0, MENU_COPY_TO_SDCARD, 0, R.string.copy_to_sdcard) 923 .setOnMenuItemClickListener(l); 924 } 925 if (haveSomethingToCopyToDrmProvider(msgItem.mMsgId)) { 926 menu.add(0, MENU_COPY_TO_DRM_PROVIDER, 0, 927 getDrmMimeMenuStringRsrc(msgItem.mMsgId)) 928 .setOnMenuItemClickListener(l); 929 } 930 break; 931 } 932 } else { 933 // Message type is sms. Only allow "edit" if the message has a single recipient 934 if (getRecipients().size() == 1 && 935 (msgItem.mBoxId == Sms.MESSAGE_TYPE_OUTBOX || 936 msgItem.mBoxId == Sms.MESSAGE_TYPE_FAILED)) { 937 menu.add(0, MENU_EDIT_MESSAGE, 0, R.string.menu_edit) 938 .setOnMenuItemClickListener(l); 939 } 940 } 941 942 addCallAndContactMenuItems(menu, l, msgItem); 943 944 // Forward is not available for undownloaded messages. 945 if (msgItem.isDownloaded()) { 946 menu.add(0, MENU_FORWARD_MESSAGE, 0, R.string.menu_forward) 947 .setOnMenuItemClickListener(l); 948 } 949 950 // It is unclear what would make most sense for copying an MMS message 951 // to the clipboard, so we currently do SMS only. 952 if (msgItem.isSms()) { 953 menu.add(0, MENU_COPY_MESSAGE_TEXT, 0, R.string.copy_message_text) 954 .setOnMenuItemClickListener(l); 955 } 956 957 menu.add(0, MENU_VIEW_MESSAGE_DETAILS, 0, R.string.view_message_details) 958 .setOnMenuItemClickListener(l); 959 menu.add(0, MENU_DELETE_MESSAGE, 0, R.string.delete_message) 960 .setOnMenuItemClickListener(l); 961 if (msgItem.mDeliveryStatus != MessageItem.DeliveryStatus.NONE || msgItem.mReadReport) { 962 menu.add(0, MENU_DELIVERY_REPORT, 0, R.string.view_delivery_report) 963 .setOnMenuItemClickListener(l); 964 } 965 } 966 }; 967 968 private void editMessageItem(MessageItem msgItem) { 969 if ("sms".equals(msgItem.mType)) { 970 editSmsMessageItem(msgItem); 971 } else { 972 editMmsMessageItem(msgItem); 973 } 974 if (msgItem.isFailedMessage() && mMsgListAdapter.getCount() <= 1) { 975 // For messages with bad addresses, let the user re-edit the recipients. 976 initRecipientsEditor(); 977 } 978 } 979 980 private void editSmsMessageItem(MessageItem msgItem) { 981 // When the message being edited is the only message in the conversation, the delete 982 // below does something subtle. The trigger "delete_obsolete_threads_pdu" sees that a 983 // thread contains no messages and silently deletes the thread. Meanwhile, the mConversation 984 // object still holds onto the old thread_id and code thinks there's a backing thread in 985 // the DB when it really has been deleted. Here we try and notice that situation and 986 // clear out the thread_id. Later on, when Conversation.ensureThreadId() is called, we'll 987 // create a new thread if necessary. 988 synchronized(mConversation) { 989 if (mConversation.getMessageCount() <= 1) { 990 mConversation.clearThreadId(); 991 } 992 } 993 // Delete the old undelivered SMS and load its content. 994 Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgItem.mMsgId); 995 SqliteWrapper.delete(ComposeMessageActivity.this, 996 mContentResolver, uri, null, null); 997 998 mWorkingMessage.setText(msgItem.mBody); 999 } 1000 1001 private void editMmsMessageItem(MessageItem msgItem) { 1002 // Discard the current message in progress. 1003 mWorkingMessage.discard(); 1004 1005 // Load the selected message in as the working message. 1006 mWorkingMessage = WorkingMessage.load(this, msgItem.mMessageUri); 1007 mWorkingMessage.setConversation(mConversation); 1008 1009 drawTopPanel(); 1010 1011 // WorkingMessage.load() above only loads the slideshow. Set the 1012 // subject here because we already know what it is and avoid doing 1013 // another DB lookup in load() just to get it. 1014 mWorkingMessage.setSubject(msgItem.mSubject, false); 1015 1016 if (mWorkingMessage.hasSubject()) { 1017 showSubjectEditor(true); 1018 } 1019 } 1020 1021 private void copyToClipboard(String str) { 1022 ClipboardManager clip = 1023 (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE); 1024 clip.setText(str); 1025 } 1026 1027 private void forwardMessage(MessageItem msgItem) { 1028 Intent intent = createIntent(this, 0); 1029 1030 intent.putExtra("exit_on_sent", true); 1031 intent.putExtra("forwarded_message", true); 1032 1033 if (msgItem.mType.equals("sms")) { 1034 intent.putExtra("sms_body", msgItem.mBody); 1035 } else { 1036 SendReq sendReq = new SendReq(); 1037 String subject = getString(R.string.forward_prefix); 1038 if (msgItem.mSubject != null) { 1039 subject += msgItem.mSubject; 1040 } 1041 sendReq.setSubject(new EncodedStringValue(subject)); 1042 sendReq.setBody(msgItem.mSlideshow.makeCopy( 1043 ComposeMessageActivity.this)); 1044 1045 Uri uri = null; 1046 try { 1047 PduPersister persister = PduPersister.getPduPersister(this); 1048 // Copy the parts of the message here. 1049 uri = persister.persist(sendReq, Mms.Draft.CONTENT_URI); 1050 } catch (MmsException e) { 1051 Log.e(TAG, "Failed to copy message: " + msgItem.mMessageUri, e); 1052 Toast.makeText(ComposeMessageActivity.this, 1053 R.string.cannot_save_message, Toast.LENGTH_SHORT).show(); 1054 return; 1055 } 1056 1057 intent.putExtra("msg_uri", uri); 1058 intent.putExtra("subject", subject); 1059 } 1060 // ForwardMessageActivity is simply an alias in the manifest for ComposeMessageActivity. 1061 // We have to make an alias because ComposeMessageActivity launch flags specify 1062 // singleTop. When we forward a message, we want to start a separate ComposeMessageActivity. 1063 // The only way to do that is to override the singleTop flag, which is impossible to do 1064 // in code. By creating an alias to the activity, without the singleTop flag, we can 1065 // launch a separate ComposeMessageActivity to edit the forward message. 1066 intent.setClassName(this, "com.android.mms.ui.ForwardMessageActivity"); 1067 startActivity(intent); 1068 } 1069 1070 /** 1071 * Context menu handlers for the message list view. 1072 */ 1073 private final class MsgListMenuClickListener implements MenuItem.OnMenuItemClickListener { 1074 public boolean onMenuItemClick(MenuItem item) { 1075 if (!isCursorValid()) { 1076 return false; 1077 } 1078 Cursor cursor = mMsgListAdapter.getCursor(); 1079 String type = cursor.getString(COLUMN_MSG_TYPE); 1080 long msgId = cursor.getLong(COLUMN_ID); 1081 MessageItem msgItem = getMessageItem(type, msgId, true); 1082 1083 if (msgItem == null) { 1084 return false; 1085 } 1086 1087 switch (item.getItemId()) { 1088 case MENU_EDIT_MESSAGE: 1089 editMessageItem(msgItem); 1090 drawBottomPanel(); 1091 return true; 1092 1093 case MENU_COPY_MESSAGE_TEXT: 1094 copyToClipboard(msgItem.mBody); 1095 return true; 1096 1097 case MENU_FORWARD_MESSAGE: 1098 forwardMessage(msgItem); 1099 return true; 1100 1101 case MENU_VIEW_SLIDESHOW: 1102 MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this, 1103 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId), null); 1104 return true; 1105 1106 case MENU_VIEW_MESSAGE_DETAILS: { 1107 String messageDetails = MessageUtils.getMessageDetails( 1108 ComposeMessageActivity.this, cursor, msgItem.mMessageSize); 1109 new AlertDialog.Builder(ComposeMessageActivity.this) 1110 .setTitle(R.string.message_details_title) 1111 .setMessage(messageDetails) 1112 .setPositiveButton(android.R.string.ok, null) 1113 .setCancelable(true) 1114 .show(); 1115 return true; 1116 } 1117 case MENU_DELETE_MESSAGE: { 1118 DeleteMessageListener l = new DeleteMessageListener( 1119 msgItem.mMessageUri, msgItem.mLocked); 1120 confirmDeleteDialog(l, msgItem.mLocked); 1121 return true; 1122 } 1123 case MENU_DELIVERY_REPORT: 1124 showDeliveryReport(msgId, type); 1125 return true; 1126 1127 case MENU_COPY_TO_SDCARD: { 1128 int resId = copyMedia(msgId) ? R.string.copy_to_sdcard_success : 1129 R.string.copy_to_sdcard_fail; 1130 Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show(); 1131 return true; 1132 } 1133 1134 case MENU_COPY_TO_DRM_PROVIDER: { 1135 int resId = getDrmMimeSavedStringRsrc(msgId, copyToDrmProvider(msgId)); 1136 Toast.makeText(ComposeMessageActivity.this, resId, Toast.LENGTH_SHORT).show(); 1137 return true; 1138 } 1139 1140 case MENU_LOCK_MESSAGE: { 1141 lockMessage(msgItem, true); 1142 return true; 1143 } 1144 1145 case MENU_UNLOCK_MESSAGE: { 1146 lockMessage(msgItem, false); 1147 return true; 1148 } 1149 1150 default: 1151 return false; 1152 } 1153 } 1154 } 1155 1156 private void lockMessage(MessageItem msgItem, boolean locked) { 1157 Uri uri; 1158 if ("sms".equals(msgItem.mType)) { 1159 uri = Sms.CONTENT_URI; 1160 } else { 1161 uri = Mms.CONTENT_URI; 1162 } 1163 final Uri lockUri = ContentUris.withAppendedId(uri, msgItem.mMsgId); 1164 1165 final ContentValues values = new ContentValues(1); 1166 values.put("locked", locked ? 1 : 0); 1167 1168 new Thread(new Runnable() { 1169 public void run() { 1170 getContentResolver().update(lockUri, 1171 values, null, null); 1172 } 1173 }).start(); 1174 } 1175 1176 /** 1177 * Looks to see if there are any valid parts of the attachment that can be copied to a SD card. 1178 * @param msgId 1179 */ 1180 private boolean haveSomethingToCopyToSDCard(long msgId) { 1181 PduBody body = PduBodyCache.getPduBody(this, 1182 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId)); 1183 if (body == null) { 1184 return false; 1185 } 1186 1187 boolean result = false; 1188 int partNum = body.getPartsNum(); 1189 for(int i = 0; i < partNum; i++) { 1190 PduPart part = body.getPart(i); 1191 String type = new String(part.getContentType()); 1192 1193 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1194 log("[CMA] haveSomethingToCopyToSDCard: part[" + i + "] contentType=" + type); 1195 } 1196 1197 if (ContentType.isImageType(type) || ContentType.isVideoType(type) || 1198 ContentType.isAudioType(type)) { 1199 result = true; 1200 break; 1201 } 1202 } 1203 return result; 1204 } 1205 1206 /** 1207 * Looks to see if there are any drm'd parts of the attachment that can be copied to the 1208 * DrmProvider. Right now we only support saving audio (e.g. ringtones). 1209 * @param msgId 1210 */ 1211 private boolean haveSomethingToCopyToDrmProvider(long msgId) { 1212 String mimeType = getDrmMimeType(msgId); 1213 return isAudioMimeType(mimeType); 1214 } 1215 1216 /** 1217 * Simple cache to prevent having to load the same PduBody again and again for the same uri. 1218 */ 1219 private static class PduBodyCache { 1220 private static PduBody mLastPduBody; 1221 private static Uri mLastUri; 1222 1223 static public PduBody getPduBody(Context context, Uri contentUri) { 1224 if (contentUri.equals(mLastUri)) { 1225 return mLastPduBody; 1226 } 1227 try { 1228 mLastPduBody = SlideshowModel.getPduBody(context, contentUri); 1229 mLastUri = contentUri; 1230 } catch (MmsException e) { 1231 Log.e(TAG, e.getMessage(), e); 1232 return null; 1233 } 1234 return mLastPduBody; 1235 } 1236 }; 1237 1238 /** 1239 * Copies media from an Mms to the DrmProvider 1240 * @param msgId 1241 */ 1242 private boolean copyToDrmProvider(long msgId) { 1243 boolean result = true; 1244 PduBody body = PduBodyCache.getPduBody(this, 1245 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId)); 1246 if (body == null) { 1247 return false; 1248 } 1249 1250 int partNum = body.getPartsNum(); 1251 for(int i = 0; i < partNum; i++) { 1252 PduPart part = body.getPart(i); 1253 String type = new String(part.getContentType()); 1254 1255 if (ContentType.isDrmType(type)) { 1256 // All parts (but there's probably only a single one) have to be successful 1257 // for a valid result. 1258 result &= copyPartToDrmProvider(part); 1259 } 1260 } 1261 return result; 1262 } 1263 1264 private String mimeTypeOfDrmPart(PduPart part) { 1265 Uri uri = part.getDataUri(); 1266 InputStream input = null; 1267 try { 1268 input = mContentResolver.openInputStream(uri); 1269 if (input instanceof FileInputStream) { 1270 FileInputStream fin = (FileInputStream) input; 1271 1272 DrmRawContent content = new DrmRawContent(fin, fin.available(), 1273 DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING); 1274 String mimeType = content.getContentType(); 1275 return mimeType; 1276 } 1277 } catch (IOException e) { 1278 // Ignore 1279 Log.e(TAG, "IOException caught while opening or reading stream", e); 1280 } catch (DrmException e) { 1281 Log.e(TAG, "DrmException caught ", e); 1282 } finally { 1283 if (null != input) { 1284 try { 1285 input.close(); 1286 } catch (IOException e) { 1287 // Ignore 1288 Log.e(TAG, "IOException caught while closing stream", e); 1289 } 1290 } 1291 } 1292 return null; 1293 } 1294 1295 /** 1296 * Returns the type of the first drm'd pdu part. 1297 * @param msgId 1298 */ 1299 private String getDrmMimeType(long msgId) { 1300 PduBody body = PduBodyCache.getPduBody(this, 1301 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId)); 1302 if (body == null) { 1303 return null; 1304 } 1305 1306 int partNum = body.getPartsNum(); 1307 for(int i = 0; i < partNum; i++) { 1308 PduPart part = body.getPart(i); 1309 String type = new String(part.getContentType()); 1310 1311 if (ContentType.isDrmType(type)) { 1312 return mimeTypeOfDrmPart(part); 1313 } 1314 } 1315 return null; 1316 } 1317 1318 private int getDrmMimeMenuStringRsrc(long msgId) { 1319 String mimeType = getDrmMimeType(msgId); 1320 if (isAudioMimeType(mimeType)) { 1321 return R.string.save_ringtone; 1322 } 1323 return 0; 1324 } 1325 1326 private int getDrmMimeSavedStringRsrc(long msgId, boolean success) { 1327 String mimeType = getDrmMimeType(msgId); 1328 if (isAudioMimeType(mimeType)) { 1329 return success ? R.string.saved_ringtone : R.string.saved_ringtone_fail; 1330 } 1331 return 0; 1332 } 1333 1334 private boolean isAudioMimeType(String mimeType) { 1335 return mimeType != null && mimeType.startsWith("audio/"); 1336 } 1337 1338 private boolean isImageMimeType(String mimeType) { 1339 return mimeType != null && mimeType.startsWith("image/"); 1340 } 1341 1342 private boolean copyPartToDrmProvider(PduPart part) { 1343 Uri uri = part.getDataUri(); 1344 1345 InputStream input = null; 1346 try { 1347 input = mContentResolver.openInputStream(uri); 1348 if (input instanceof FileInputStream) { 1349 FileInputStream fin = (FileInputStream) input; 1350 1351 // Build a nice title 1352 byte[] location = part.getName(); 1353 if (location == null) { 1354 location = part.getFilename(); 1355 } 1356 if (location == null) { 1357 location = part.getContentLocation(); 1358 } 1359 1360 // Depending on the location, there may be an 1361 // extension already on the name or not 1362 String title = new String(location); 1363 int index; 1364 if ((index = title.indexOf(".")) == -1) { 1365 String type = new String(part.getContentType()); 1366 } else { 1367 title = title.substring(0, index); 1368 } 1369 1370 // transfer the file to the DRM content provider 1371 Intent item = DrmStore.addDrmFile(mContentResolver, fin, title); 1372 if (item == null) { 1373 Log.w(TAG, "unable to add file " + uri + " to DrmProvider"); 1374 return false; 1375 } 1376 } 1377 } catch (IOException e) { 1378 // Ignore 1379 Log.e(TAG, "IOException caught while opening or reading stream", e); 1380 return false; 1381 } finally { 1382 if (null != input) { 1383 try { 1384 input.close(); 1385 } catch (IOException e) { 1386 // Ignore 1387 Log.e(TAG, "IOException caught while closing stream", e); 1388 return false; 1389 } 1390 } 1391 } 1392 return true; 1393 } 1394 1395 /** 1396 * Copies media from an Mms to the "download" directory on the SD card 1397 * @param msgId 1398 */ 1399 private boolean copyMedia(long msgId) { 1400 boolean result = true; 1401 PduBody body = PduBodyCache.getPduBody(this, 1402 ContentUris.withAppendedId(Mms.CONTENT_URI, msgId)); 1403 if (body == null) { 1404 return false; 1405 } 1406 1407 int partNum = body.getPartsNum(); 1408 for(int i = 0; i < partNum; i++) { 1409 PduPart part = body.getPart(i); 1410 String type = new String(part.getContentType()); 1411 1412 if (ContentType.isImageType(type) || ContentType.isVideoType(type) || 1413 ContentType.isAudioType(type)) { 1414 result &= copyPart(part, Long.toHexString(msgId)); // all parts have to be successful for a valid result. 1415 } 1416 } 1417 return result; 1418 } 1419 1420 private boolean copyPart(PduPart part, String fallback) { 1421 Uri uri = part.getDataUri(); 1422 1423 InputStream input = null; 1424 FileOutputStream fout = null; 1425 try { 1426 input = mContentResolver.openInputStream(uri); 1427 if (input instanceof FileInputStream) { 1428 FileInputStream fin = (FileInputStream) input; 1429 1430 byte[] location = part.getName(); 1431 if (location == null) { 1432 location = part.getFilename(); 1433 } 1434 if (location == null) { 1435 location = part.getContentLocation(); 1436 } 1437 1438 String fileName; 1439 if (location == null) { 1440 // Use fallback name. 1441 fileName = fallback; 1442 } else { 1443 fileName = new String(location); 1444 } 1445 // Depending on the location, there may be an 1446 // extension already on the name or not 1447 String dir = Environment.getExternalStorageDirectory() + "/" 1448 + Environment.DIRECTORY_DOWNLOADS + "/"; 1449 String extension; 1450 int index; 1451 if ((index = fileName.indexOf(".")) == -1) { 1452 String type = new String(part.getContentType()); 1453 extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type); 1454 } else { 1455 extension = fileName.substring(index + 1, fileName.length()); 1456 fileName = fileName.substring(0, index); 1457 } 1458 1459 File file = getUniqueDestination(dir + fileName, extension); 1460 1461 // make sure the path is valid and directories created for this file. 1462 File parentFile = file.getParentFile(); 1463 if (!parentFile.exists() && !parentFile.mkdirs()) { 1464 Log.e(TAG, "[MMS] copyPart: mkdirs for " + parentFile.getPath() + " failed!"); 1465 return false; 1466 } 1467 1468 fout = new FileOutputStream(file); 1469 1470 byte[] buffer = new byte[8000]; 1471 int size = 0; 1472 while ((size=fin.read(buffer)) != -1) { 1473 fout.write(buffer, 0, size); 1474 } 1475 1476 // Notify other applications listening to scanner events 1477 // that a media file has been added to the sd card 1478 sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, 1479 Uri.fromFile(file))); 1480 } 1481 } catch (IOException e) { 1482 // Ignore 1483 Log.e(TAG, "IOException caught while opening or reading stream", e); 1484 return false; 1485 } finally { 1486 if (null != input) { 1487 try { 1488 input.close(); 1489 } catch (IOException e) { 1490 // Ignore 1491 Log.e(TAG, "IOException caught while closing stream", e); 1492 return false; 1493 } 1494 } 1495 if (null != fout) { 1496 try { 1497 fout.close(); 1498 } catch (IOException e) { 1499 // Ignore 1500 Log.e(TAG, "IOException caught while closing stream", e); 1501 return false; 1502 } 1503 } 1504 } 1505 return true; 1506 } 1507 1508 private File getUniqueDestination(String base, String extension) { 1509 File file = new File(base + "." + extension); 1510 1511 for (int i = 2; file.exists(); i++) { 1512 file = new File(base + "_" + i + "." + extension); 1513 } 1514 return file; 1515 } 1516 1517 private void showDeliveryReport(long messageId, String type) { 1518 Intent intent = new Intent(this, DeliveryReportActivity.class); 1519 intent.putExtra("message_id", messageId); 1520 intent.putExtra("message_type", type); 1521 1522 startActivity(intent); 1523 } 1524 1525 private final IntentFilter mHttpProgressFilter = new IntentFilter(PROGRESS_STATUS_ACTION); 1526 1527 private final BroadcastReceiver mHttpProgressReceiver = new BroadcastReceiver() { 1528 @Override 1529 public void onReceive(Context context, Intent intent) { 1530 if (PROGRESS_STATUS_ACTION.equals(intent.getAction())) { 1531 long token = intent.getLongExtra("token", 1532 SendingProgressTokenManager.NO_TOKEN); 1533 if (token != mConversation.getThreadId()) { 1534 return; 1535 } 1536 1537 int progress = intent.getIntExtra("progress", 0); 1538 switch (progress) { 1539 case PROGRESS_START: 1540 setProgressBarVisibility(true); 1541 break; 1542 case PROGRESS_ABORT: 1543 case PROGRESS_COMPLETE: 1544 setProgressBarVisibility(false); 1545 break; 1546 default: 1547 setProgress(100 * progress); 1548 } 1549 } 1550 } 1551 }; 1552 1553 private static ContactList sEmptyContactList; 1554 1555 private ContactList getRecipients() { 1556 // If the recipients editor is visible, the conversation has 1557 // not really officially 'started' yet. Recipients will be set 1558 // on the conversation once it has been saved or sent. In the 1559 // meantime, let anyone who needs the recipient list think it 1560 // is empty rather than giving them a stale one. 1561 if (isRecipientsEditorVisible()) { 1562 if (sEmptyContactList == null) { 1563 sEmptyContactList = new ContactList(); 1564 } 1565 return sEmptyContactList; 1566 } 1567 return mConversation.getRecipients(); 1568 } 1569 1570 private void updateTitle(ContactList list) { 1571 String s; 1572 switch (list.size()) { 1573 case 0: { 1574 String recipient = ""; 1575 if (mRecipientsEditor != null) { 1576 recipient = mRecipientsEditor.getText().toString(); 1577 } 1578 s = recipient; 1579 break; 1580 } 1581 case 1: { 1582 s = list.get(0).getNameAndNumber(); 1583 break; 1584 } 1585 default: { 1586 // Handle multiple recipients 1587 s = list.formatNames(", "); 1588 break; 1589 } 1590 } 1591 getWindow().setTitle(s); 1592 } 1593 1594 // Get the recipients editor ready to be displayed onscreen. 1595 private void initRecipientsEditor() { 1596 if (isRecipientsEditorVisible()) { 1597 return; 1598 } 1599 // Must grab the recipients before the view is made visible because getRecipients() 1600 // returns empty recipients when the editor is visible. 1601 ContactList recipients = getRecipients(); 1602 1603 ViewStub stub = (ViewStub)findViewById(R.id.recipients_editor_stub); 1604 if (stub != null) { 1605 View stubView = stub.inflate(); 1606 mRecipientsEditor = (RecipientsEditor) stubView.findViewById(R.id.recipients_editor); 1607 mRecipientsPicker = (ImageButton) stubView.findViewById(R.id.recipients_picker); 1608 } else { 1609 mRecipientsEditor = (RecipientsEditor)findViewById(R.id.recipients_editor); 1610 mRecipientsEditor.setVisibility(View.VISIBLE); 1611 mRecipientsPicker = (ImageButton)findViewById(R.id.recipients_picker); 1612 } 1613 mRecipientsPicker.setOnClickListener(this); 1614 1615 mRecipientsEditor.setAdapter(new RecipientsAdapter(this)); 1616 mRecipientsEditor.populate(recipients); 1617 mRecipientsEditor.setOnCreateContextMenuListener(mRecipientsMenuCreateListener); 1618 mRecipientsEditor.addTextChangedListener(mRecipientsWatcher); 1619 // TODO : Remove the max length limitation due to the multiple phone picker is added and the 1620 // user is able to select a large number of recipients from the Contacts. The coming 1621 // potential issue is that it is hard for user to edit a recipient from hundred of 1622 // recipients in the editor box. We may redesign the editor box UI for this use case. 1623 // mRecipientsEditor.setFilters(new InputFilter[] { 1624 // new InputFilter.LengthFilter(RECIPIENTS_MAX_LENGTH) }); 1625 mRecipientsEditor.setOnItemClickListener(new AdapterView.OnItemClickListener() { 1626 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 1627 // After the user selects an item in the pop-up contacts list, move the 1628 // focus to the text editor if there is only one recipient. This helps 1629 // the common case of selecting one recipient and then typing a message, 1630 // but avoids annoying a user who is trying to add five recipients and 1631 // keeps having focus stolen away. 1632 if (mRecipientsEditor.getRecipientCount() == 1) { 1633 // if we're in extract mode then don't request focus 1634 final InputMethodManager inputManager = (InputMethodManager) 1635 getSystemService(Context.INPUT_METHOD_SERVICE); 1636 if (inputManager == null || !inputManager.isFullscreenMode()) { 1637 mTextEditor.requestFocus(); 1638 } 1639 } 1640 } 1641 }); 1642 1643 mRecipientsEditor.setOnFocusChangeListener(new View.OnFocusChangeListener() { 1644 public void onFocusChange(View v, boolean hasFocus) { 1645 if (!hasFocus) { 1646 RecipientsEditor editor = (RecipientsEditor) v; 1647 ContactList contacts = editor.constructContactsFromInput(false); 1648 updateTitle(contacts); 1649 } 1650 } 1651 }); 1652 1653 mTopPanel.setVisibility(View.VISIBLE); 1654 } 1655 1656 //========================================================== 1657 // Activity methods 1658 //========================================================== 1659 1660 public static boolean cancelFailedToDeliverNotification(Intent intent, Context context) { 1661 if (MessagingNotification.isFailedToDeliver(intent)) { 1662 // Cancel any failed message notifications 1663 MessagingNotification.cancelNotification(context, 1664 MessagingNotification.MESSAGE_FAILED_NOTIFICATION_ID); 1665 return true; 1666 } 1667 return false; 1668 } 1669 1670 public static boolean cancelFailedDownloadNotification(Intent intent, Context context) { 1671 if (MessagingNotification.isFailedToDownload(intent)) { 1672 // Cancel any failed download notifications 1673 MessagingNotification.cancelNotification(context, 1674 MessagingNotification.DOWNLOAD_FAILED_NOTIFICATION_ID); 1675 return true; 1676 } 1677 return false; 1678 } 1679 1680 @Override 1681 protected void onCreate(Bundle savedInstanceState) { 1682 super.onCreate(savedInstanceState); 1683 1684 setContentView(R.layout.compose_message_activity); 1685 setProgressBarVisibility(false); 1686 1687 getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | 1688 WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); 1689 1690 // Initialize members for UI elements. 1691 initResourceRefs(); 1692 1693 mContentResolver = getContentResolver(); 1694 mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver); 1695 1696 initialize(savedInstanceState); 1697 1698 if (TRACE) { 1699 android.os.Debug.startMethodTracing("compose"); 1700 } 1701 } 1702 1703 private void showSubjectEditor(boolean show) { 1704 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1705 log("showSubjectEditor: " + show); 1706 } 1707 1708 if (mSubjectTextEditor == null) { 1709 // Don't bother to initialize the subject editor if 1710 // we're just going to hide it. 1711 if (show == false) { 1712 return; 1713 } 1714 mSubjectTextEditor = (EditText)findViewById(R.id.subject); 1715 } 1716 1717 mSubjectTextEditor.setOnKeyListener(show ? mSubjectKeyListener : null); 1718 1719 if (show) { 1720 mSubjectTextEditor.addTextChangedListener(mSubjectEditorWatcher); 1721 } else { 1722 mSubjectTextEditor.removeTextChangedListener(mSubjectEditorWatcher); 1723 } 1724 1725 mSubjectTextEditor.setText(mWorkingMessage.getSubject()); 1726 mSubjectTextEditor.setVisibility(show ? View.VISIBLE : View.GONE); 1727 hideOrShowTopPanel(); 1728 } 1729 1730 private void hideOrShowTopPanel() { 1731 boolean anySubViewsVisible = (isSubjectEditorVisible() || isRecipientsEditorVisible()); 1732 mTopPanel.setVisibility(anySubViewsVisible ? View.VISIBLE : View.GONE); 1733 } 1734 1735 public void initialize(Bundle savedInstanceState) { 1736 Intent intent = getIntent(); 1737 1738 // Create a new empty working message. 1739 mWorkingMessage = WorkingMessage.createEmpty(this); 1740 1741 // Read parameters or previously saved state of this activity. 1742 initActivityState(savedInstanceState, intent); 1743 1744 log("initialize: savedInstanceState = " + savedInstanceState + 1745 " intent = " + intent + 1746 " mConversation = " + mConversation); 1747 1748 if (cancelFailedToDeliverNotification(getIntent(), this)) { 1749 // Show a pop-up dialog to inform user the message was 1750 // failed to deliver. 1751 undeliveredMessageDialog(getMessageDate(null)); 1752 } 1753 cancelFailedDownloadNotification(getIntent(), this); 1754 1755 // Set up the message history ListAdapter 1756 initMessageList(); 1757 1758 // Load the draft for this thread, if we aren't already handling 1759 // existing data, such as a shared picture or forwarded message. 1760 boolean isForwardedMessage = false; 1761 if (!handleSendIntent(intent)) { 1762 isForwardedMessage = handleForwardedMessage(); 1763 if (!isForwardedMessage) { 1764 loadDraft(); 1765 } 1766 } 1767 1768 // Let the working message know what conversation it belongs to 1769 mWorkingMessage.setConversation(mConversation); 1770 1771 // Show the recipients editor if we don't have a valid thread. Hide it otherwise. 1772 if (mConversation.getThreadId() <= 0) { 1773 // Hide the recipients editor so the call to initRecipientsEditor won't get 1774 // short-circuited. 1775 hideRecipientEditor(); 1776 initRecipientsEditor(); 1777 1778 // Bring up the softkeyboard so the user can immediately enter recipients. This 1779 // call won't do anything on devices with a hard keyboard. 1780 getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | 1781 WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); 1782 } else { 1783 hideRecipientEditor(); 1784 } 1785 1786 updateSendButtonState(); 1787 1788 drawTopPanel(); 1789 drawBottomPanel(); 1790 1791 Configuration config = getResources().getConfiguration(); 1792 mIsKeyboardOpen = config.keyboardHidden == KEYBOARDHIDDEN_NO; 1793 mIsLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE; 1794 onKeyboardStateChanged(mIsKeyboardOpen); 1795 1796 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1797 log("initialize: update title, mConversation=" + mConversation.toString()); 1798 } 1799 1800 updateTitle(mConversation.getRecipients()); 1801 1802 if (isForwardedMessage && isRecipientsEditorVisible()) { 1803 // The user is forwarding the message to someone. Put the focus on the 1804 // recipient editor rather than in the message editor. 1805 mRecipientsEditor.requestFocus(); 1806 } 1807 } 1808 1809 @Override 1810 protected void onNewIntent(Intent intent) { 1811 super.onNewIntent(intent); 1812 1813 setIntent(intent); 1814 1815 Conversation conversation = null; 1816 mSentMessage = false; 1817 1818 // If we have been passed a thread_id, use that to find our 1819 // conversation. 1820 long threadId = intent.getLongExtra("thread_id", 0); 1821 Uri intentUri = intent.getData(); 1822 1823 boolean sameThread = false; 1824 if (threadId > 0) { 1825 conversation = Conversation.get(this, threadId, false); 1826 } else { 1827 if (mConversation.getThreadId() == 0) { 1828 // We've got a draft. See if the new intent's recipient is the same as 1829 // the draft's recipient. First make sure the working recipients are synched 1830 // to the conversation. 1831 mWorkingMessage.syncWorkingRecipients(); 1832 sameThread = mConversation.sameRecipient(intentUri); 1833 } 1834 if (!sameThread) { 1835 // Otherwise, try to get a conversation based on the 1836 // data URI passed to our intent. 1837 conversation = Conversation.get(this, intentUri, false); 1838 } 1839 } 1840 1841 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1842 log("onNewIntent: data=" + intentUri + ", thread_id extra is " + threadId); 1843 log(" new conversation=" + conversation + ", mConversation=" + mConversation); 1844 } 1845 1846 long convThreadId = 0; 1847 if (conversation != null) { 1848 // Don't let any markAsRead DB updates occur before we've loaded the messages for 1849 // the thread. 1850 conversation.blockMarkAsRead(true); 1851 convThreadId = conversation.getThreadId(); 1852 } 1853 if (sameThread || (convThreadId != 0 && convThreadId == mConversation.getThreadId())) { 1854 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1855 log("onNewIntent: same conversation"); 1856 } 1857 } else { 1858 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1859 log("onNewIntent: different conversation, initialize..."); 1860 } 1861 saveDraft(); // if we've got a draft, save it first 1862 1863 initialize(null); 1864 loadMessageContent(); 1865 } 1866 1867 } 1868 1869 @Override 1870 protected void onRestart() { 1871 super.onRestart(); 1872 1873 if (mWorkingMessage.isDiscarded()) { 1874 mWorkingMessage.unDiscard(); // it was discarded in onStop(). 1875 } 1876 } 1877 1878 @Override 1879 protected void onStart() { 1880 super.onStart(); 1881 mConversation.blockMarkAsRead(true); 1882 1883 initFocus(); 1884 1885 // Register a BroadcastReceiver to listen on HTTP I/O process. 1886 registerReceiver(mHttpProgressReceiver, mHttpProgressFilter); 1887 1888 loadMessageContent(); 1889 1890 // Update the fasttrack info in case any of the recipients' contact info changed 1891 // while we were paused. This can happen, for example, if a user changes or adds 1892 // an avatar associated with a contact. 1893 mWorkingMessage.syncWorkingRecipients(); 1894 1895 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1896 log("onStart: update title, mConversation=" + mConversation.toString()); 1897 } 1898 1899 updateTitle(mConversation.getRecipients()); 1900 } 1901 1902 public void loadMessageContent() { 1903 startMsgListQuery(); 1904 updateSendFailedNotification(); 1905 drawBottomPanel(); 1906 } 1907 1908 private void updateSendFailedNotification() { 1909 final long threadId = mConversation.getThreadId(); 1910 if (threadId <= 0) 1911 return; 1912 1913 // updateSendFailedNotificationForThread makes a database call, so do the work off 1914 // of the ui thread. 1915 new Thread(new Runnable() { 1916 public void run() { 1917 MessagingNotification.updateSendFailedNotificationForThread( 1918 ComposeMessageActivity.this, threadId); 1919 } 1920 }).run(); 1921 } 1922 1923 @Override 1924 public void onSaveInstanceState(Bundle outState) { 1925 super.onSaveInstanceState(outState); 1926 1927 outState.putString("recipients", getRecipients().serialize()); 1928 1929 mWorkingMessage.writeStateToBundle(outState); 1930 1931 if (mExitOnSent) { 1932 outState.putBoolean("exit_on_sent", mExitOnSent); 1933 } 1934 } 1935 1936 @Override 1937 protected void onResume() { 1938 super.onResume(); 1939 1940 // OLD: get notified of presence updates to update the titlebar. 1941 // NEW: we are using ContactHeaderWidget which displays presence, but updating presence 1942 // there is out of our control. 1943 //Contact.startPresenceObserver(); 1944 1945 addRecipientsListeners(); 1946 1947 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1948 log("onResume: update title, mConversation=" + mConversation.toString()); 1949 } 1950 1951 // There seems to be a bug in the framework such that setting the title 1952 // here gets overwritten to the original title. Do this delayed as a 1953 // workaround. 1954 mMessageListItemHandler.postDelayed(new Runnable() { 1955 public void run() { 1956 ContactList recipients = isRecipientsEditorVisible() ? 1957 mRecipientsEditor.constructContactsFromInput(false) : getRecipients(); 1958 updateTitle(recipients); 1959 } 1960 }, 100); 1961 } 1962 1963 @Override 1964 protected void onPause() { 1965 super.onPause(); 1966 1967 // OLD: stop getting notified of presence updates to update the titlebar. 1968 // NEW: we are using ContactHeaderWidget which displays presence, but updating presence 1969 // there is out of our control. 1970 //Contact.stopPresenceObserver(); 1971 1972 removeRecipientsListeners(); 1973 } 1974 1975 @Override 1976 protected void onStop() { 1977 super.onStop(); 1978 1979 // Allow any blocked calls to update the thread's read status. 1980 mConversation.blockMarkAsRead(false); 1981 1982 if (mMsgListAdapter != null) { 1983 mMsgListAdapter.changeCursor(null); 1984 } 1985 1986 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 1987 log("onStop: save draft"); 1988 } 1989 saveDraft(); 1990 1991 // Cleanup the BroadcastReceiver. 1992 unregisterReceiver(mHttpProgressReceiver); 1993 } 1994 1995 @Override 1996 protected void onDestroy() { 1997 if (TRACE) { 1998 android.os.Debug.stopMethodTracing(); 1999 } 2000 2001 super.onDestroy(); 2002 } 2003 2004 @Override 2005 public void onConfigurationChanged(Configuration newConfig) { 2006 super.onConfigurationChanged(newConfig); 2007 if (LOCAL_LOGV) { 2008 Log.v(TAG, "onConfigurationChanged: " + newConfig); 2009 } 2010 2011 mIsKeyboardOpen = newConfig.keyboardHidden == KEYBOARDHIDDEN_NO; 2012 boolean isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE; 2013 if (mIsLandscape != isLandscape) { 2014 mIsLandscape = isLandscape; 2015 2016 // Have to re-layout the attachment editor because we have different layouts 2017 // depending on whether we're portrait or landscape. 2018 drawTopPanel(); 2019 } 2020 onKeyboardStateChanged(mIsKeyboardOpen); 2021 } 2022 2023 private void onKeyboardStateChanged(boolean isKeyboardOpen) { 2024 // If the keyboard is hidden, don't show focus highlights for 2025 // things that cannot receive input. 2026 if (isKeyboardOpen) { 2027 if (mRecipientsEditor != null) { 2028 mRecipientsEditor.setFocusableInTouchMode(true); 2029 } 2030 if (mSubjectTextEditor != null) { 2031 mSubjectTextEditor.setFocusableInTouchMode(true); 2032 } 2033 mTextEditor.setFocusableInTouchMode(true); 2034 mTextEditor.setHint(R.string.type_to_compose_text_enter_to_send); 2035 } else { 2036 if (mRecipientsEditor != null) { 2037 mRecipientsEditor.setFocusable(false); 2038 } 2039 if (mSubjectTextEditor != null) { 2040 mSubjectTextEditor.setFocusable(false); 2041 } 2042 mTextEditor.setFocusable(false); 2043 mTextEditor.setHint(R.string.open_keyboard_to_compose_message); 2044 } 2045 } 2046 2047 @Override 2048 public void onUserInteraction() { 2049 checkPendingNotification(); 2050 } 2051 2052 @Override 2053 public void onWindowFocusChanged(boolean hasFocus) { 2054 if (hasFocus) { 2055 checkPendingNotification(); 2056 } 2057 } 2058 2059 @Override 2060 public boolean onKeyDown(int keyCode, KeyEvent event) { 2061 switch (keyCode) { 2062 case KeyEvent.KEYCODE_DEL: 2063 if ((mMsgListAdapter != null) && mMsgListView.isFocused()) { 2064 Cursor cursor; 2065 try { 2066 cursor = (Cursor) mMsgListView.getSelectedItem(); 2067 } catch (ClassCastException e) { 2068 Log.e(TAG, "Unexpected ClassCastException.", e); 2069 return super.onKeyDown(keyCode, event); 2070 } 2071 2072 if (cursor != null) { 2073 boolean locked = cursor.getInt(COLUMN_MMS_LOCKED) != 0; 2074 DeleteMessageListener l = new DeleteMessageListener( 2075 cursor.getLong(COLUMN_ID), 2076 cursor.getString(COLUMN_MSG_TYPE), 2077 locked); 2078 confirmDeleteDialog(l, locked); 2079 return true; 2080 } 2081 } 2082 break; 2083 case KeyEvent.KEYCODE_DPAD_CENTER: 2084 case KeyEvent.KEYCODE_ENTER: 2085 if (isPreparedForSending()) { 2086 confirmSendMessageIfNeeded(); 2087 return true; 2088 } 2089 break; 2090 case KeyEvent.KEYCODE_BACK: 2091 exitComposeMessageActivity(new Runnable() { 2092 public void run() { 2093 finish(); 2094 } 2095 }); 2096 return true; 2097 } 2098 2099 return super.onKeyDown(keyCode, event); 2100 } 2101 2102 private void exitComposeMessageActivity(final Runnable exit) { 2103 // If the message is empty, just quit -- finishing the 2104 // activity will cause an empty draft to be deleted. 2105 if (!mWorkingMessage.isWorthSaving()) { 2106 exit.run(); 2107 return; 2108 } 2109 2110 if (isRecipientsEditorVisible() && 2111 !mRecipientsEditor.hasValidRecipient(mWorkingMessage.requiresMms())) { 2112 MessageUtils.showDiscardDraftConfirmDialog(this, new DiscardDraftListener()); 2113 return; 2114 } 2115 2116 mToastForDraftSave = true; 2117 exit.run(); 2118 } 2119 2120 private void goToConversationList() { 2121 finish(); 2122 startActivity(new Intent(this, ConversationList.class)); 2123 } 2124 2125 private void hideRecipientEditor() { 2126 if (mRecipientsEditor != null) { 2127 mRecipientsEditor.removeTextChangedListener(mRecipientsWatcher); 2128 mRecipientsEditor.setVisibility(View.GONE); 2129 hideOrShowTopPanel(); 2130 } 2131 } 2132 2133 private boolean isRecipientsEditorVisible() { 2134 return (null != mRecipientsEditor) 2135 && (View.VISIBLE == mRecipientsEditor.getVisibility()); 2136 } 2137 2138 private boolean isSubjectEditorVisible() { 2139 return (null != mSubjectTextEditor) 2140 && (View.VISIBLE == mSubjectTextEditor.getVisibility()); 2141 } 2142 2143 public void onAttachmentChanged() { 2144 // Have to make sure we're on the UI thread. This function can be called off of the UI 2145 // thread when we're adding multi-attachments 2146 runOnUiThread(new Runnable() { 2147 public void run() { 2148 drawBottomPanel(); 2149 updateSendButtonState(); 2150 drawTopPanel(); 2151 } 2152 }); 2153 } 2154 2155 public void onProtocolChanged(final boolean mms) { 2156 // Have to make sure we're on the UI thread. This function can be called off of the UI 2157 // thread when we're adding multi-attachments 2158 runOnUiThread(new Runnable() { 2159 public void run() { 2160 toastConvertInfo(mms); 2161 setSendButtonText(mms); 2162 } 2163 }); 2164 } 2165 2166 private void setSendButtonText(boolean isMms) { 2167 Button sendButton = mSendButton; 2168 sendButton.setText(R.string.send); 2169 2170 if (isMms) { 2171 // Create and append the "MMS" text in a smaller font than the "Send" text. 2172 sendButton.append("\n"); 2173 SpannableString spannable = new SpannableString(getString(R.string.mms)); 2174 int mmsTextSize = (int) (sendButton.getTextSize() * 0.75f); 2175 spannable.setSpan(new AbsoluteSizeSpan(mmsTextSize), 0, spannable.length(), 0); 2176 sendButton.append(spannable); 2177 mTextCounter.setText(""); 2178 } 2179 } 2180 2181 Runnable mResetMessageRunnable = new Runnable() { 2182 public void run() { 2183 resetMessage(); 2184 } 2185 }; 2186 2187 public void onPreMessageSent() { 2188 runOnUiThread(mResetMessageRunnable); 2189 } 2190 2191 public void onMessageSent() { 2192 // If we already have messages in the list adapter, it 2193 // will be auto-requerying; don't thrash another query in. 2194 if (mMsgListAdapter.getCount() == 0) { 2195 startMsgListQuery(); 2196 } 2197 } 2198 2199 public void onMaxPendingMessagesReached() { 2200 saveDraft(); 2201 2202 runOnUiThread(new Runnable() { 2203 public void run() { 2204 Toast.makeText(ComposeMessageActivity.this, R.string.too_many_unsent_mms, 2205 Toast.LENGTH_LONG).show(); 2206 } 2207 }); 2208 } 2209 2210 public void onAttachmentError(final int error) { 2211 runOnUiThread(new Runnable() { 2212 public void run() { 2213 handleAddAttachmentError(error, R.string.type_picture); 2214 onMessageSent(); // now requery the list of messages 2215 } 2216 }); 2217 } 2218 2219 // We don't want to show the "call" option unless there is only one 2220 // recipient and it's a phone number. 2221 private boolean isRecipientCallable() { 2222 ContactList recipients = getRecipients(); 2223 return (recipients.size() == 1 && !recipients.containsEmail()); 2224 } 2225 2226 private void dialRecipient() { 2227 String number = getRecipients().get(0).getNumber(); 2228 Intent dialIntent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + number)); 2229 startActivity(dialIntent); 2230 } 2231 2232 @Override 2233 public boolean onPrepareOptionsMenu(Menu menu) { 2234 menu.clear(); 2235 2236 if (isRecipientCallable()) { 2237 menu.add(0, MENU_CALL_RECIPIENT, 0, R.string.menu_call).setIcon( 2238 R.drawable.ic_menu_call); 2239 } 2240 2241 // Only add the "View contact" menu item when there's a single recipient and that 2242 // recipient is someone in contacts. 2243 ContactList recipients = getRecipients(); 2244 if (recipients.size() == 1 && recipients.get(0).existsInDatabase()) { 2245 menu.add(0, MENU_VIEW_CONTACT, 0, R.string.menu_view_contact).setIcon( 2246 R.drawable.ic_menu_contact); 2247 } 2248 2249 if (MmsConfig.getMmsEnabled()) { 2250 if (!isSubjectEditorVisible()) { 2251 menu.add(0, MENU_ADD_SUBJECT, 0, R.string.add_subject).setIcon( 2252 R.drawable.ic_menu_edit); 2253 } 2254 2255 if (!mWorkingMessage.hasAttachment()) { 2256 menu.add(0, MENU_ADD_ATTACHMENT, 0, R.string.add_attachment).setIcon( 2257 R.drawable.ic_menu_attachment); 2258 } 2259 } 2260 2261 if (isPreparedForSending()) { 2262 menu.add(0, MENU_SEND, 0, R.string.send).setIcon(android.R.drawable.ic_menu_send); 2263 } 2264 2265 menu.add(0, MENU_INSERT_SMILEY, 0, R.string.menu_insert_smiley).setIcon( 2266 R.drawable.ic_menu_emoticons); 2267 2268 if (mMsgListAdapter.getCount() > 0) { 2269 // Removed search as part of b/1205708 2270 //menu.add(0, MENU_SEARCH, 0, R.string.menu_search).setIcon( 2271 // R.drawable.ic_menu_search); 2272 Cursor cursor = mMsgListAdapter.getCursor(); 2273 if ((null != cursor) && (cursor.getCount() > 0)) { 2274 menu.add(0, MENU_DELETE_THREAD, 0, R.string.delete_thread).setIcon( 2275 android.R.drawable.ic_menu_delete); 2276 } 2277 } else { 2278 menu.add(0, MENU_DISCARD, 0, R.string.discard).setIcon(android.R.drawable.ic_menu_delete); 2279 } 2280 2281 menu.add(0, MENU_CONVERSATION_LIST, 0, R.string.all_threads).setIcon( 2282 R.drawable.ic_menu_friendslist); 2283 2284 buildAddAddressToContactMenuItem(menu); 2285 return true; 2286 } 2287 2288 private void buildAddAddressToContactMenuItem(Menu menu) { 2289 // Look for the first recipient we don't have a contact for and create a menu item to 2290 // add the number to contacts. 2291 for (Contact c : getRecipients()) { 2292 if (!c.existsInDatabase() && canAddToContacts(c)) { 2293 Intent intent = ConversationList.createAddContactIntent(c.getNumber()); 2294 menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, R.string.menu_add_to_contacts) 2295 .setIcon(android.R.drawable.ic_menu_add) 2296 .setIntent(intent); 2297 break; 2298 } 2299 } 2300 } 2301 2302 @Override 2303 public boolean onOptionsItemSelected(MenuItem item) { 2304 switch (item.getItemId()) { 2305 case MENU_ADD_SUBJECT: 2306 showSubjectEditor(true); 2307 mWorkingMessage.setSubject("", true); 2308 mSubjectTextEditor.requestFocus(); 2309 break; 2310 case MENU_ADD_ATTACHMENT: 2311 // Launch the add-attachment list dialog 2312 showAddAttachmentDialog(false); 2313 break; 2314 case MENU_DISCARD: 2315 mWorkingMessage.discard(); 2316 finish(); 2317 break; 2318 case MENU_SEND: 2319 if (isPreparedForSending()) { 2320 confirmSendMessageIfNeeded(); 2321 } 2322 break; 2323 case MENU_SEARCH: 2324 onSearchRequested(); 2325 break; 2326 case MENU_DELETE_THREAD: 2327 confirmDeleteThread(mConversation.getThreadId()); 2328 break; 2329 case MENU_CONVERSATION_LIST: 2330 exitComposeMessageActivity(new Runnable() { 2331 public void run() { 2332 goToConversationList(); 2333 } 2334 }); 2335 break; 2336 case MENU_CALL_RECIPIENT: 2337 dialRecipient(); 2338 break; 2339 case MENU_INSERT_SMILEY: 2340 showSmileyDialog(); 2341 break; 2342 case MENU_VIEW_CONTACT: { 2343 // View the contact for the first (and only) recipient. 2344 ContactList list = getRecipients(); 2345 if (list.size() == 1 && list.get(0).existsInDatabase()) { 2346 Uri contactUri = list.get(0).getUri(); 2347 Intent intent = new Intent(Intent.ACTION_VIEW, contactUri); 2348 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 2349 startActivity(intent); 2350 } 2351 break; 2352 } 2353 case MENU_ADD_ADDRESS_TO_CONTACTS: 2354 mAddContactIntent = item.getIntent(); 2355 startActivityForResult(mAddContactIntent, REQUEST_CODE_ADD_CONTACT); 2356 break; 2357 } 2358 2359 return true; 2360 } 2361 2362 private void confirmDeleteThread(long threadId) { 2363 Conversation.startQueryHaveLockedMessages(mBackgroundQueryHandler, 2364 threadId, ConversationList.HAVE_LOCKED_MESSAGES_TOKEN); 2365 } 2366 2367// static class SystemProperties { // TODO, temp class to get unbundling working 2368// static int getInt(String s, int value) { 2369// return value; // just return the default value or now 2370// } 2371// } 2372 2373 private void addAttachment(int type, boolean replace) { 2374 // Calculate the size of the current slide if we're doing a replace so the 2375 // slide size can optionally be used in computing how much room is left for an attachment. 2376 int currentSlideSize = 0; 2377 SlideshowModel slideShow = mWorkingMessage.getSlideshow(); 2378 if (replace && slideShow != null) { 2379 SlideModel slide = slideShow.get(0); 2380 currentSlideSize = slide.getSlideSize(); 2381 } 2382 switch (type) { 2383 case AttachmentTypeSelectorAdapter.ADD_IMAGE: 2384 MessageUtils.selectImage(this, REQUEST_CODE_ATTACH_IMAGE); 2385 break; 2386 2387 case AttachmentTypeSelectorAdapter.TAKE_PICTURE: { 2388 Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 2389 2390 intent.putExtra(MediaStore.EXTRA_OUTPUT, Mms.ScrapSpace.CONTENT_URI); 2391 startActivityForResult(intent, REQUEST_CODE_TAKE_PICTURE); 2392 break; 2393 } 2394 2395 case AttachmentTypeSelectorAdapter.ADD_VIDEO: 2396 MessageUtils.selectVideo(this, REQUEST_CODE_ATTACH_VIDEO); 2397 break; 2398 2399 case AttachmentTypeSelectorAdapter.RECORD_VIDEO: { 2400 long sizeLimit = computeAttachmentSizeLimit(slideShow, currentSlideSize); 2401 if (sizeLimit > 0) { 2402 MessageUtils.recordVideo(this, REQUEST_CODE_TAKE_VIDEO, sizeLimit); 2403 } else { 2404 Toast.makeText(this, 2405 getString(R.string.message_too_big_for_video), 2406 Toast.LENGTH_SHORT).show(); 2407 } 2408 } 2409 break; 2410 2411 case AttachmentTypeSelectorAdapter.ADD_SOUND: 2412 MessageUtils.selectAudio(this, REQUEST_CODE_ATTACH_SOUND); 2413 break; 2414 2415 case AttachmentTypeSelectorAdapter.RECORD_SOUND: 2416 long sizeLimit = computeAttachmentSizeLimit(slideShow, currentSlideSize); 2417 MessageUtils.recordSound(this, REQUEST_CODE_RECORD_SOUND, sizeLimit); 2418 break; 2419 2420 case AttachmentTypeSelectorAdapter.ADD_SLIDESHOW: 2421 editSlideshow(); 2422 break; 2423 2424 default: 2425 break; 2426 } 2427 } 2428 2429 public static long computeAttachmentSizeLimit(SlideshowModel slideShow, int currentSlideSize) { 2430 // Computer attachment size limit. Subtract 1K for some text. 2431 long sizeLimit = MmsConfig.getMaxMessageSize() - SlideshowModel.SLIDESHOW_SLOP; 2432 if (slideShow != null) { 2433 sizeLimit -= slideShow.getCurrentMessageSize(); 2434 2435 // We're about to ask the camera to capture some video (or the sound recorder 2436 // to record some audio) which will eventually replace the content on the current 2437 // slide. Since the current slide already has some content (which was subtracted 2438 // out just above) and that content is going to get replaced, we can add the size of the 2439 // current slide into the available space used to capture a video (or audio). 2440 sizeLimit += currentSlideSize; 2441 } 2442 return sizeLimit; 2443 } 2444 2445 private void showAddAttachmentDialog(final boolean replace) { 2446 AlertDialog.Builder builder = new AlertDialog.Builder(this); 2447 builder.setIcon(R.drawable.ic_dialog_attach); 2448 builder.setTitle(R.string.add_attachment); 2449 2450 if (mAttachmentTypeSelectorAdapter == null) { 2451 mAttachmentTypeSelectorAdapter = new AttachmentTypeSelectorAdapter( 2452 this, AttachmentTypeSelectorAdapter.MODE_WITH_SLIDESHOW); 2453 } 2454 builder.setAdapter(mAttachmentTypeSelectorAdapter, new DialogInterface.OnClickListener() { 2455 public void onClick(DialogInterface dialog, int which) { 2456 addAttachment(mAttachmentTypeSelectorAdapter.buttonToCommand(which), replace); 2457 } 2458 }); 2459 2460 builder.show(); 2461 } 2462 2463 @Override 2464 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 2465 if (DEBUG) { 2466 log("onActivityResult: requestCode=" + requestCode 2467 + ", resultCode=" + resultCode + ", data=" + data); 2468 } 2469 mWaitingForSubActivity = false; // We're back! 2470 if (mWorkingMessage.isFakeMmsForDraft()) { 2471 // We no longer have to fake the fact we're an Mms. At this point we are or we aren't, 2472 // based on attachments and other Mms attrs. 2473 mWorkingMessage.removeFakeMmsForDraft(); 2474 } 2475 2476 if (requestCode == REQUEST_CODE_PICK) { 2477 mWorkingMessage.asyncDeleteDraftSmsMessage(mConversation); 2478 } 2479 2480 // If there's no data (because the user didn't select a picture and 2481 // just hit BACK, for example), there's nothing to do. 2482 if (requestCode != REQUEST_CODE_TAKE_PICTURE) { 2483 if (data == null) { 2484 return; 2485 } 2486 } else if (resultCode != RESULT_OK){ 2487 if (DEBUG) log("onActivityResult: bail due to resultCode=" + resultCode); 2488 return; 2489 } 2490 2491 switch(requestCode) { 2492 case REQUEST_CODE_CREATE_SLIDESHOW: 2493 if (data != null) { 2494 WorkingMessage newMessage = WorkingMessage.load(this, data.getData()); 2495 if (newMessage != null) { 2496 mWorkingMessage = newMessage; 2497 mWorkingMessage.setConversation(mConversation); 2498 drawTopPanel(); 2499 updateSendButtonState(); 2500 } 2501 } 2502 break; 2503 2504 case REQUEST_CODE_TAKE_PICTURE: { 2505 // create a file based uri and pass to addImage(). We want to read the JPEG 2506 // data directly from file (using UriImage) instead of decoding it into a Bitmap, 2507 // which takes up too much memory and could easily lead to OOM. 2508 File file = new File(Mms.ScrapSpace.SCRAP_FILE_PATH); 2509 Uri uri = Uri.fromFile(file); 2510 addImage(uri, false); 2511 break; 2512 } 2513 2514 case REQUEST_CODE_ATTACH_IMAGE: { 2515 addImage(data.getData(), false); 2516 break; 2517 } 2518 2519 case REQUEST_CODE_TAKE_VIDEO: 2520 case REQUEST_CODE_ATTACH_VIDEO: 2521 addVideo(data.getData(), false); 2522 break; 2523 2524 case REQUEST_CODE_ATTACH_SOUND: { 2525 Uri uri = (Uri) data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); 2526 if (Settings.System.DEFAULT_RINGTONE_URI.equals(uri)) { 2527 break; 2528 } 2529 addAudio(uri); 2530 break; 2531 } 2532 2533 case REQUEST_CODE_RECORD_SOUND: 2534 addAudio(data.getData()); 2535 break; 2536 2537 case REQUEST_CODE_ECM_EXIT_DIALOG: 2538 boolean outOfEmergencyMode = data.getBooleanExtra(EXIT_ECM_RESULT, false); 2539 if (outOfEmergencyMode) { 2540 sendMessage(false); 2541 } 2542 break; 2543 2544 case REQUEST_CODE_ADD_CONTACT: 2545 // The user just added a new contact. We saved the contact info in 2546 // mAddContactIntent. Get the contact and force our cached contact to 2547 // get reloaded with the new info (such as contact name). After the 2548 // contact is reloaded, the function onUpdate() in this file will get called 2549 // and it will update the title bar, etc. 2550 if (mAddContactIntent != null) { 2551 String address = 2552 mAddContactIntent.getStringExtra(ContactsContract.Intents.Insert.EMAIL); 2553 if (address == null) { 2554 address = 2555 mAddContactIntent.getStringExtra(ContactsContract.Intents.Insert.PHONE); 2556 } 2557 if (address != null) { 2558 Contact contact = Contact.get(address, false); 2559 if (contact != null) { 2560 contact.reload(); 2561 } 2562 } 2563 } 2564 break; 2565 2566 case REQUEST_CODE_PICK: 2567 processPickResult(data); 2568 break; 2569 default: 2570 // TODO 2571 break; 2572 } 2573 } 2574 2575 private void processPickResult(final Intent data) { 2576 // The EXTRA_PHONE_URIS stores the phone's urls that were selected by user in the 2577 // multiple phone picker. 2578 final Parcelable[] uris = 2579 data.getParcelableArrayExtra(Intents.EXTRA_PHONE_URIS); 2580 2581 final int recipientCount = uris != null ? uris.length : 0; 2582 2583 final int recipientLimit = MmsConfig.getRecipientLimit(); 2584 if (recipientLimit != Integer.MAX_VALUE && recipientCount > recipientLimit) { 2585 new AlertDialog.Builder(this) 2586 .setTitle(R.string.pick_too_many_recipients) 2587 .setIcon(android.R.drawable.ic_dialog_alert) 2588 .setMessage(getString(R.string.too_many_recipients, recipientCount, recipientLimit)) 2589 .setPositiveButton(android.R.string.ok, null) 2590 .create().show(); 2591 return; 2592 } 2593 2594 final Handler handler = new Handler(); 2595 final ProgressDialog progressDialog = new ProgressDialog(this); 2596 progressDialog.setTitle(getText(R.string.pick_too_many_recipients)); 2597 progressDialog.setMessage(getText(R.string.adding_recipients)); 2598 progressDialog.setIndeterminate(true); 2599 progressDialog.setCancelable(false); 2600 2601 final Runnable showProgress = new Runnable() { 2602 public void run() { 2603 progressDialog.show(); 2604 } 2605 }; 2606 // Only show the progress dialog if we can not finish off parsing the return data in 1s, 2607 // otherwise the dialog could flicker. 2608 handler.postDelayed(showProgress, 1000); 2609 2610 new Thread(new Runnable() { 2611 public void run() { 2612 final ContactList list; 2613 try { 2614 list = ContactList.blockingGetByUris(uris); 2615 } finally { 2616 handler.removeCallbacks(showProgress); 2617 progressDialog.dismiss(); 2618 } 2619 // TODO: there is already code to update the contact header widget and recipients 2620 // editor if the contacts change. we can re-use that code. 2621 final Runnable populateWorker = new Runnable() { 2622 public void run() { 2623 mRecipientsEditor.populate(list); 2624 updateTitle(list); 2625 } 2626 }; 2627 handler.post(populateWorker); 2628 } 2629 }).start(); 2630 } 2631 2632 private final ResizeImageResultCallback mResizeImageCallback = new ResizeImageResultCallback() { 2633 // TODO: make this produce a Uri, that's what we want anyway 2634 public void onResizeResult(PduPart part, boolean append) { 2635 if (part == null) { 2636 handleAddAttachmentError(WorkingMessage.UNKNOWN_ERROR, R.string.type_picture); 2637 return; 2638 } 2639 2640 Context context = ComposeMessageActivity.this; 2641 PduPersister persister = PduPersister.getPduPersister(context); 2642 int result; 2643 2644 Uri messageUri = mWorkingMessage.saveAsMms(true); 2645 try { 2646 Uri dataUri = persister.persistPart(part, ContentUris.parseId(messageUri)); 2647 result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, dataUri, append); 2648 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 2649 log("ResizeImageResultCallback: dataUri=" + dataUri); 2650 } 2651 } catch (MmsException e) { 2652 result = WorkingMessage.UNKNOWN_ERROR; 2653 } 2654 2655 handleAddAttachmentError(result, R.string.type_picture); 2656 } 2657 }; 2658 2659 private void handleAddAttachmentError(final int error, final int mediaTypeStringId) { 2660 if (error == WorkingMessage.OK) { 2661 return; 2662 } 2663 2664 runOnUiThread(new Runnable() { 2665 public void run() { 2666 Resources res = getResources(); 2667 String mediaType = res.getString(mediaTypeStringId); 2668 String title, message; 2669 2670 switch(error) { 2671 case WorkingMessage.UNKNOWN_ERROR: 2672 message = res.getString(R.string.failed_to_add_media, mediaType); 2673 Toast.makeText(ComposeMessageActivity.this, message, Toast.LENGTH_SHORT).show(); 2674 return; 2675 case WorkingMessage.UNSUPPORTED_TYPE: 2676 title = res.getString(R.string.unsupported_media_format, mediaType); 2677 message = res.getString(R.string.select_different_media, mediaType); 2678 break; 2679 case WorkingMessage.MESSAGE_SIZE_EXCEEDED: 2680 title = res.getString(R.string.exceed_message_size_limitation, mediaType); 2681 message = res.getString(R.string.failed_to_add_media, mediaType); 2682 break; 2683 case WorkingMessage.IMAGE_TOO_LARGE: 2684 title = res.getString(R.string.failed_to_resize_image); 2685 message = res.getString(R.string.resize_image_error_information); 2686 break; 2687 default: 2688 throw new IllegalArgumentException("unknown error " + error); 2689 } 2690 2691 MessageUtils.showErrorDialog(ComposeMessageActivity.this, title, message); 2692 } 2693 }); 2694 } 2695 2696 private void addImage(Uri uri, boolean append) { 2697 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 2698 log("addImage: append=" + append + ", uri=" + uri); 2699 } 2700 2701 int result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, uri, append); 2702 2703 if (result == WorkingMessage.IMAGE_TOO_LARGE || 2704 result == WorkingMessage.MESSAGE_SIZE_EXCEEDED) { 2705 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 2706 log("addImage: resize image " + uri); 2707 } 2708 MessageUtils.resizeImageAsync(this, 2709 uri, mAttachmentEditorHandler, mResizeImageCallback, append); 2710 return; 2711 } 2712 handleAddAttachmentError(result, R.string.type_picture); 2713 } 2714 2715 private void addVideo(Uri uri, boolean append) { 2716 if (uri != null) { 2717 int result = mWorkingMessage.setAttachment(WorkingMessage.VIDEO, uri, append); 2718 handleAddAttachmentError(result, R.string.type_video); 2719 } 2720 } 2721 2722 private void addAudio(Uri uri) { 2723 int result = mWorkingMessage.setAttachment(WorkingMessage.AUDIO, uri, false); 2724 handleAddAttachmentError(result, R.string.type_audio); 2725 } 2726 2727 private boolean handleForwardedMessage() { 2728 Intent intent = getIntent(); 2729 2730 // If this is a forwarded message, it will have an Intent extra 2731 // indicating so. If not, bail out. 2732 if (intent.getBooleanExtra("forwarded_message", false) == false) { 2733 return false; 2734 } 2735 2736 Uri uri = intent.getParcelableExtra("msg_uri"); 2737 2738 if (Log.isLoggable(LogTag.APP, Log.DEBUG)) { 2739 log("handle forwarded message " + uri); 2740 } 2741 2742 if (uri != null) { 2743 mWorkingMessage = WorkingMessage.load(this, uri); 2744 mWorkingMessage.setSubject(intent.getStringExtra("subject"), false); 2745 } else { 2746 mWorkingMessage.setText(intent.getStringExtra("sms_body")); 2747 } 2748 2749 // let's clear the message thread for forwarded messages 2750 mMsgListAdapter.changeCursor(null); 2751 2752 return true; 2753 } 2754 2755 private boolean handleSendIntent(Intent intent) { 2756 Bundle extras = intent.getExtras(); 2757 if (extras == null) { 2758 return false; 2759 } 2760 2761 final String mimeType = intent.getType(); 2762 String action = intent.getAction(); 2763 if (Intent.ACTION_SEND.equals(action)) { 2764 if (extras.containsKey(Intent.EXTRA_STREAM)) { 2765 Uri uri = (Uri)extras.getParcelable(Intent.EXTRA_STREAM); 2766 addAttachment(mimeType, uri, false); 2767 return true; 2768 } else if (extras.containsKey(Intent.EXTRA_TEXT)) { 2769 mWorkingMessage.setText(extras.getString(Intent.EXTRA_TEXT)); 2770 return true; 2771 } 2772 } else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && 2773 extras.containsKey(Intent.EXTRA_STREAM)) { 2774 SlideshowModel slideShow = mWorkingMessage.getSlideshow(); 2775 final ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM); 2776 int currentSlideCount = slideShow != null ? slideShow.size() : 0; 2777 int importCount = uris.size(); 2778 if (importCount + currentSlideCount > SlideshowEditor.MAX_SLIDE_NUM) { 2779 importCount = Math.min(SlideshowEditor.MAX_SLIDE_NUM - currentSlideCount, 2780 importCount); 2781 Toast.makeText(ComposeMessageActivity.this, 2782 getString(R.string.too_many_attachments, 2783 SlideshowEditor.MAX_SLIDE_NUM, importCount), 2784 Toast.LENGTH_LONG).show(); 2785 } 2786 2787 // Attach all the pictures/videos off of the UI thread. 2788 // Show a progress alert if adding all the slides hasn't finished 2789 // within one second. 2790 // Stash the runnable for showing it away so we can cancel 2791 // it later if adding completes ahead of the deadline. 2792 final AlertDialog dialog = new AlertDialog.Builder(ComposeMessageActivity.this) 2793 .setIcon(android.R.drawable.ic_dialog_alert) 2794 .setTitle(R.string.adding_attachments_title) 2795 .setMessage(R.string.adding_attachments) 2796 .create(); 2797 final Runnable showProgress = new Runnable() { 2798 public void run() { 2799 dialog.show(); 2800 } 2801 }; 2802 // Schedule it for one second from now. 2803 mAttachmentEditorHandler.postDelayed(showProgress, 1000); 2804 2805 final int numberToImport = importCount; 2806 new Thread(new Runnable() { 2807 public void run() { 2808 for (int i = 0; i < numberToImport; i++) { 2809 Parcelable uri = uris.get(i); 2810 addAttachment(mimeType, (Uri) uri, true); 2811 } 2812 // Cancel pending show of the progress alert if necessary. 2813 mAttachmentEditorHandler.removeCallbacks(showProgress); 2814 dialog.dismiss(); 2815 } 2816 }).start(); 2817 return true; 2818 } 2819 2820 return false; 2821 } 2822 2823 // mVideoUri will look like this: content://media/external/video/media 2824 private static final String mVideoUri = Video.Media.getContentUri("external").toString(); 2825 // mImageUri will look like this: content://media/external/images/media 2826 private static final String mImageUri = Images.Media.getContentUri("external").toString(); 2827 2828 private void addAttachment(String type, Uri uri, boolean append) { 2829 if (uri != null) { 2830 // When we're handling Intent.ACTION_SEND_MULTIPLE, the passed in items can be 2831 // videos, and/or images, and/or some other unknown types we don't handle. When 2832 // a single attachment is "shared" the type will specify an image or video. When 2833 // there are multiple types, the type passed in is "*/*". In that case, we've got 2834 // to look at the uri to figure out if it is an image or video. 2835 boolean wildcard = "*/*".equals(type); 2836 if (type.startsWith("image/") || (wildcard && uri.toString().startsWith(mImageUri))) { 2837 addImage(uri, append); 2838 } else if (type.startsWith("video/") || 2839 (wildcard && uri.toString().startsWith(mVideoUri))) { 2840 addVideo(uri, append); 2841 } 2842 } 2843 } 2844 2845 private String getResourcesString(int id, String mediaName) { 2846 Resources r = getResources(); 2847 return r.getString(id, mediaName); 2848 } 2849 2850 private void drawBottomPanel() { 2851 // Reset the counter for text editor. 2852 resetCounter(); 2853 2854 if (mWorkingMessage.hasSlideshow()) { 2855 mBottomPanel.setVisibility(View.GONE); 2856 mAttachmentEditor.requestFocus(); 2857 return; 2858 } 2859 2860 mBottomPanel.setVisibility(View.VISIBLE); 2861 2862 CharSequence text = mWorkingMessage.getText(); 2863 2864 // TextView.setTextKeepState() doesn't like null input. 2865 if (text != null) { 2866 mTextEditor.setTextKeepState(text); 2867 } else { 2868 mTextEditor.setText(""); 2869 } 2870 } 2871 2872 private void drawTopPanel() { 2873 boolean showingAttachment = mAttachmentEditor.update(mWorkingMessage); 2874 mAttachmentEditorScrollView.setVisibility(showingAttachment ? View.VISIBLE : View.GONE); 2875 showSubjectEditor(mWorkingMessage.hasSubject()); 2876 } 2877 2878 //========================================================== 2879 // Interface methods 2880 //========================================================== 2881 2882 public void onClick(View v) { 2883 if ((v == mSendButton) && isPreparedForSending()) { 2884 confirmSendMessageIfNeeded(); 2885 } else if ((v == mRecipientsPicker)) { 2886 launchMultiplePhonePicker(); 2887 } 2888 } 2889 2890 private void launchMultiplePhonePicker() { 2891 Intent intent = new Intent(Intents.ACTION_GET_MULTIPLE_PHONES); 2892 intent.addCategory("android.intent.category.DEFAULT"); 2893 intent.setType(Phone.CONTENT_TYPE); 2894 // We have to wait for the constructing complete. 2895 ContactList contacts = mRecipientsEditor.constructContactsFromInput(true); 2896 int recipientsCount = 0; 2897 int urisCount = 0; 2898 Uri[] uris = new Uri[contacts.size()]; 2899 urisCount = 0; 2900 for (Contact contact : contacts) { 2901 if (Contact.CONTACT_METHOD_TYPE_PHONE == contact.getContactMethodType()) { 2902 uris[urisCount++] = contact.getPhoneUri(); 2903 } 2904 } 2905 if (urisCount > 0) { 2906 intent.putExtra(Intents.EXTRA_PHONE_URIS, uris); 2907 } 2908 startActivityForResult(intent, REQUEST_CODE_PICK); 2909 } 2910 2911 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 2912 if (event != null) { 2913 // if shift key is down, then we want to insert the '\n' char in the TextView; 2914 // otherwise, the default action is to send the message. 2915 if (!event.isShiftPressed()) { 2916 if (isPreparedForSending()) { 2917 confirmSendMessageIfNeeded(); 2918 } 2919 return true; 2920 } 2921 return false; 2922 } 2923 2924 if (isPreparedForSending()) { 2925 confirmSendMessageIfNeeded(); 2926 } 2927 return true; 2928 } 2929 2930 private final TextWatcher mTextEditorWatcher = new TextWatcher() { 2931 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 2932 } 2933 2934 public void onTextChanged(CharSequence s, int start, int before, int count) { 2935 // This is a workaround for bug 1609057. Since onUserInteraction() is 2936 // not called when the user touches the soft keyboard, we pretend it was 2937 // called when textfields changes. This should be removed when the bug 2938 // is fixed. 2939 onUserInteraction(); 2940 2941 mWorkingMessage.setText(s); 2942 2943 updateSendButtonState(); 2944 2945 updateCounter(s, start, before, count); 2946 2947 ensureCorrectButtonHeight(); 2948 } 2949 2950 public void afterTextChanged(Editable s) { 2951 } 2952 }; 2953 2954 /** 2955 * Ensures that if the text edit box extends past two lines then the 2956 * button will be shifted up to allow enough space for the character 2957 * counter string to be placed beneath it. 2958 */ 2959 private void ensureCorrectButtonHeight() { 2960 int currentTextLines = mTextEditor.getLineCount(); 2961 if (currentTextLines <= 2) { 2962 mTextCounter.setVisibility(View.GONE); 2963 } 2964 else if (currentTextLines > 2 && mTextCounter.getVisibility() == View.GONE) { 2965 // Making the counter invisible ensures that it is used to correctly 2966 // calculate the position of the send button even if we choose not to 2967 // display the text. 2968 mTextCounter.setVisibility(View.INVISIBLE); 2969 } 2970 } 2971 2972 private final TextWatcher mSubjectEditorWatcher = new TextWatcher() { 2973 public void beforeTextChanged(CharSequence s, int start, int count, int after) { } 2974 2975 public void onTextChanged(CharSequence s, int start, int before, int count) { 2976 mWorkingMessage.setSubject(s, true); 2977 } 2978 2979 public void afterTextChanged(Editable s) { } 2980 }; 2981 2982 //========================================================== 2983 // Private methods 2984 //========================================================== 2985 2986 /** 2987 * Initialize all UI elements from resources. 2988 */ 2989 private void initResourceRefs() { 2990 mMsgListView = (MessageListView) findViewById(R.id.history); 2991 mMsgListView.setDivider(null); // no divider so we look like IM conversation. 2992 mBottomPanel = findViewById(R.id.bottom_panel); 2993 mTextEditor = (EditText) findViewById(R.id.embedded_text_editor); 2994 mTextEditor.setOnEditorActionListener(this); 2995 mTextEditor.addTextChangedListener(mTextEditorWatcher); 2996 mTextCounter = (TextView) findViewById(R.id.text_counter); 2997 mSendButton = (Button) findViewById(R.id.send_button); 2998 mSendButton.setOnClickListener(this); 2999 mTopPanel = findViewById(R.id.recipients_subject_linear); 3000 mTopPanel.setFocusable(false); 3001 mAttachmentEditor = (AttachmentEditor) findViewById(R.id.attachment_editor); 3002 mAttachmentEditor.setHandler(mAttachmentEditorHandler); 3003 mAttachmentEditorScrollView = findViewById(R.id.attachment_editor_scroll_view); 3004 } 3005 3006 private void confirmDeleteDialog(OnClickListener listener, boolean locked) { 3007 AlertDialog.Builder builder = new AlertDialog.Builder(this); 3008 builder.setTitle(locked ? R.string.confirm_dialog_locked_title : 3009 R.string.confirm_dialog_title); 3010 builder.setIcon(android.R.drawable.ic_dialog_alert); 3011 builder.setCancelable(true); 3012 builder.setMessage(locked ? R.string.confirm_delete_locked_message : 3013 R.string.confirm_delete_message); 3014 builder.setPositiveButton(R.string.delete, listener); 3015 builder.setNegativeButton(R.string.no, null); 3016 builder.show(); 3017 } 3018 3019 void undeliveredMessageDialog(long date) { 3020 String body; 3021 LinearLayout dialog = (LinearLayout) LayoutInflater.from(this).inflate( 3022 R.layout.retry_sending_dialog, null); 3023 3024 if (date >= 0) { 3025 body = getString(R.string.undelivered_msg_dialog_body, 3026 MessageUtils.formatTimeStampString(this, date)); 3027 } else { 3028 // FIXME: we can not get sms retry time. 3029 body = getString(R.string.undelivered_sms_dialog_body); 3030 } 3031 3032 ((TextView) dialog.findViewById(R.id.body_text_view)).setText(body); 3033 3034 Toast undeliveredDialog = new Toast(this); 3035 undeliveredDialog.setView(dialog); 3036 undeliveredDialog.setDuration(Toast.LENGTH_LONG); 3037 undeliveredDialog.show(); 3038 } 3039 3040 private void startMsgListQuery() { 3041 Uri conversationUri = mConversation.getUri(); 3042 3043 if (conversationUri == null) { 3044 return; 3045 } 3046 3047 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3048 log("startMsgListQuery for " + conversationUri); 3049 } 3050 3051 // Cancel any pending queries 3052 mBackgroundQueryHandler.cancelOperation(MESSAGE_LIST_QUERY_TOKEN); 3053 try { 3054 // Kick off the new query 3055 mBackgroundQueryHandler.startQuery( 3056 MESSAGE_LIST_QUERY_TOKEN, null, conversationUri, 3057 PROJECTION, null, null, null); 3058 } catch (SQLiteException e) { 3059 SqliteWrapper.checkSQLiteException(this, e); 3060 } 3061 } 3062 3063 private void initMessageList() { 3064 if (mMsgListAdapter != null) { 3065 return; 3066 } 3067 3068 String highlightString = getIntent().getStringExtra("highlight"); 3069 Pattern highlight = highlightString == null 3070 ? null 3071 : Pattern.compile("\\b" + Pattern.quote(highlightString), Pattern.CASE_INSENSITIVE); 3072 3073 // Initialize the list adapter with a null cursor. 3074 mMsgListAdapter = new MessageListAdapter(this, null, mMsgListView, true, highlight); 3075 mMsgListAdapter.setOnDataSetChangedListener(mDataSetChangedListener); 3076 mMsgListAdapter.setMsgListItemHandler(mMessageListItemHandler); 3077 mMsgListView.setAdapter(mMsgListAdapter); 3078 mMsgListView.setItemsCanFocus(false); 3079 mMsgListView.setVisibility(View.VISIBLE); 3080 mMsgListView.setOnCreateContextMenuListener(mMsgListMenuCreateListener); 3081 mMsgListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 3082 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 3083 if (view != null) { 3084 ((MessageListItem) view).onMessageListItemClick(); 3085 } 3086 } 3087 }); 3088 } 3089 3090 private void loadDraft() { 3091 if (mWorkingMessage.isWorthSaving()) { 3092 Log.w(TAG, "loadDraft() called with non-empty working message"); 3093 return; 3094 } 3095 3096 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3097 log("loadDraft: call WorkingMessage.loadDraft"); 3098 } 3099 3100 mWorkingMessage = WorkingMessage.loadDraft(this, mConversation); 3101 } 3102 3103 private void saveDraft() { 3104 // TODO: Do something better here. Maybe make discard() legal 3105 // to call twice and make isEmpty() return true if discarded 3106 // so it is caught in the clause above this one? 3107 if (mWorkingMessage.isDiscarded()) { 3108 return; 3109 } 3110 3111 if (!mWaitingForSubActivity && !mWorkingMessage.isWorthSaving()) { 3112 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3113 log("saveDraft: not worth saving, discard WorkingMessage and bail"); 3114 } 3115 mWorkingMessage.discard(); 3116 return; 3117 } 3118 3119 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3120 log("saveDraft: call WorkingMessage.saveDraft"); 3121 } 3122 3123 mWorkingMessage.saveDraft(); 3124 3125 if (mToastForDraftSave) { 3126 Toast.makeText(this, R.string.message_saved_as_draft, 3127 Toast.LENGTH_SHORT).show(); 3128 } 3129 } 3130 3131 private boolean isPreparedForSending() { 3132 int recipientCount = recipientCount(); 3133 3134 return recipientCount > 0 && recipientCount <= MmsConfig.getRecipientLimit() && 3135 (mWorkingMessage.hasAttachment() || mWorkingMessage.hasText()); 3136 } 3137 3138 private int recipientCount() { 3139 int recipientCount; 3140 3141 // To avoid creating a bunch of invalid Contacts when the recipients 3142 // editor is in flux, we keep the recipients list empty. So if the 3143 // recipients editor is showing, see if there is anything in it rather 3144 // than consulting the empty recipient list. 3145 if (isRecipientsEditorVisible()) { 3146 recipientCount = mRecipientsEditor.getRecipientCount(); 3147 } else { 3148 recipientCount = getRecipients().size(); 3149 } 3150 return recipientCount; 3151 } 3152 3153 private void sendMessage(boolean bCheckEcmMode) { 3154 if (bCheckEcmMode) { 3155 // TODO: expose this in telephony layer for SDK build 3156 String inEcm = SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE); 3157 if (Boolean.parseBoolean(inEcm)) { 3158 try { 3159 startActivityForResult( 3160 new Intent(TelephonyIntents.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS, null), 3161 REQUEST_CODE_ECM_EXIT_DIALOG); 3162 return; 3163 } catch (ActivityNotFoundException e) { 3164 // continue to send message 3165 Log.e(TAG, "Cannot find EmergencyCallbackModeExitDialog", e); 3166 } 3167 } 3168 } 3169 3170 if (!mSendingMessage) { 3171 // send can change the recipients. Make sure we remove the listeners first and then add 3172 // them back once the recipient list has settled. 3173 removeRecipientsListeners(); 3174 mWorkingMessage.send(); 3175 mSentMessage = true; 3176 mSendingMessage = true; 3177 addRecipientsListeners(); 3178 } 3179 // But bail out if we are supposed to exit after the message is sent. 3180 if (mExitOnSent) { 3181 finish(); 3182 } 3183 } 3184 3185 private void resetMessage() { 3186 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3187 log("resetMessage"); 3188 } 3189 3190 // Make the attachment editor hide its view. 3191 mAttachmentEditor.hideView(); 3192 mAttachmentEditorScrollView.setVisibility(View.GONE); 3193 3194 // Hide the subject editor. 3195 showSubjectEditor(false); 3196 3197 // Focus to the text editor. 3198 mTextEditor.requestFocus(); 3199 3200 // We have to remove the text change listener while the text editor gets cleared and 3201 // we subsequently turn the message back into SMS. When the listener is listening while 3202 // doing the clearing, it's fighting to update its counts and itself try and turn 3203 // the message one way or the other. 3204 mTextEditor.removeTextChangedListener(mTextEditorWatcher); 3205 3206 // Clear the text box. 3207 TextKeyListener.clear(mTextEditor.getText()); 3208 3209 mWorkingMessage = WorkingMessage.createEmpty(this); 3210 mWorkingMessage.setConversation(mConversation); 3211 3212 hideRecipientEditor(); 3213 drawBottomPanel(); 3214 3215 // "Or not", in this case. 3216 updateSendButtonState(); 3217 3218 // Our changes are done. Let the listener respond to text changes once again. 3219 mTextEditor.addTextChangedListener(mTextEditorWatcher); 3220 3221 // Close the soft on-screen keyboard if we're in landscape mode so the user can see the 3222 // conversation. 3223 if (mIsLandscape) { 3224 InputMethodManager inputMethodManager = 3225 (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); 3226 3227 inputMethodManager.hideSoftInputFromWindow(mTextEditor.getWindowToken(), 0); 3228 } 3229 3230 mLastRecipientCount = 0; 3231 mSendingMessage = false; 3232 } 3233 3234 private void updateSendButtonState() { 3235 boolean enable = false; 3236 if (isPreparedForSending()) { 3237 // When the type of attachment is slideshow, we should 3238 // also hide the 'Send' button since the slideshow view 3239 // already has a 'Send' button embedded. 3240 if (!mWorkingMessage.hasSlideshow()) { 3241 enable = true; 3242 } else { 3243 mAttachmentEditor.setCanSend(true); 3244 } 3245 } else if (null != mAttachmentEditor){ 3246 mAttachmentEditor.setCanSend(false); 3247 } 3248 3249 setSendButtonText(mWorkingMessage.requiresMms()); 3250 mSendButton.setEnabled(enable); 3251 mSendButton.setFocusable(enable); 3252 } 3253 3254 private long getMessageDate(Uri uri) { 3255 if (uri != null) { 3256 Cursor cursor = SqliteWrapper.query(this, mContentResolver, 3257 uri, new String[] { Mms.DATE }, null, null, null); 3258 if (cursor != null) { 3259 try { 3260 if ((cursor.getCount() == 1) && cursor.moveToFirst()) { 3261 return cursor.getLong(0) * 1000L; 3262 } 3263 } finally { 3264 cursor.close(); 3265 } 3266 } 3267 } 3268 return NO_DATE_FOR_DIALOG; 3269 } 3270 3271 private void initActivityState(Bundle bundle, Intent intent) { 3272 if (bundle != null) { 3273 String recipients = bundle.getString("recipients"); 3274 mConversation = Conversation.get(this, 3275 ContactList.getByNumbers(recipients, 3276 false /* don't block */, true /* replace number */), false); 3277 addRecipientsListeners(); 3278 mExitOnSent = bundle.getBoolean("exit_on_sent", false); 3279 mWorkingMessage.readStateFromBundle(bundle); 3280 return; 3281 } 3282 3283 // If we have been passed a thread_id, use that to find our 3284 // conversation. 3285 long threadId = intent.getLongExtra("thread_id", 0); 3286 if (threadId > 0) { 3287 mConversation = Conversation.get(this, threadId, false); 3288 } else { 3289 Uri intentData = intent.getData(); 3290 3291 if (intentData != null) { 3292 // try to get a conversation based on the data URI passed to our intent. 3293 mConversation = Conversation.get(this, intentData, false); 3294 } else { 3295 // special intent extra parameter to specify the address 3296 String address = intent.getStringExtra("address"); 3297 if (!TextUtils.isEmpty(address)) { 3298 mConversation = Conversation.get(this, ContactList.getByNumbers(address, 3299 false /* don't block */, true /* replace number */), false); 3300 } else { 3301 mConversation = Conversation.createNew(this); 3302 } 3303 } 3304 } 3305 addRecipientsListeners(); 3306 3307 mExitOnSent = intent.getBooleanExtra("exit_on_sent", false); 3308 mWorkingMessage.setText(intent.getStringExtra("sms_body")); 3309 mWorkingMessage.setSubject(intent.getStringExtra("subject"), false); 3310 } 3311 3312 private void initFocus() { 3313 if (!mIsKeyboardOpen) { 3314 return; 3315 } 3316 3317 // If the recipients editor is visible, there is nothing in it, 3318 // and the text editor is not already focused, focus the 3319 // recipients editor. 3320 if (isRecipientsEditorVisible() 3321 && TextUtils.isEmpty(mRecipientsEditor.getText()) 3322 && !mTextEditor.isFocused()) { 3323 mRecipientsEditor.requestFocus(); 3324 return; 3325 } 3326 3327 // If we decided not to focus the recipients editor, focus the text editor. 3328 mTextEditor.requestFocus(); 3329 } 3330 3331 private final MessageListAdapter.OnDataSetChangedListener 3332 mDataSetChangedListener = new MessageListAdapter.OnDataSetChangedListener() { 3333 public void onDataSetChanged(MessageListAdapter adapter) { 3334 mPossiblePendingNotification = true; 3335 } 3336 3337 public void onContentChanged(MessageListAdapter adapter) { 3338 startMsgListQuery(); 3339 } 3340 }; 3341 3342 private void checkPendingNotification() { 3343 if (mPossiblePendingNotification && hasWindowFocus()) { 3344 mConversation.markAsRead(); 3345 mPossiblePendingNotification = false; 3346 } 3347 } 3348 3349 private final class BackgroundQueryHandler extends AsyncQueryHandler { 3350 public BackgroundQueryHandler(ContentResolver contentResolver) { 3351 super(contentResolver); 3352 } 3353 3354 @Override 3355 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 3356 switch(token) { 3357 case MESSAGE_LIST_QUERY_TOKEN: 3358 int newSelectionPos = -1; 3359 long targetMsgId = getIntent().getLongExtra("select_id", -1); 3360 if (targetMsgId != -1) { 3361 cursor.moveToPosition(-1); 3362 while (cursor.moveToNext()) { 3363 long msgId = cursor.getLong(COLUMN_ID); 3364 if (msgId == targetMsgId) { 3365 newSelectionPos = cursor.getPosition(); 3366 break; 3367 } 3368 } 3369 } 3370 3371 mMsgListAdapter.changeCursor(cursor); 3372 if (newSelectionPos != -1) { 3373 mMsgListView.setSelection(newSelectionPos); 3374 } 3375 3376 // Once we have completed the query for the message history, if 3377 // there is nothing in the cursor and we are not composing a new 3378 // message, we must be editing a draft in a new conversation (unless 3379 // mSentMessage is true). 3380 // Show the recipients editor to give the user a chance to add 3381 // more people before the conversation begins. 3382 if (cursor.getCount() == 0 && !isRecipientsEditorVisible() && !mSentMessage) { 3383 initRecipientsEditor(); 3384 } 3385 3386 // FIXME: freshing layout changes the focused view to an unexpected 3387 // one, set it back to TextEditor forcely. 3388 mTextEditor.requestFocus(); 3389 3390 mConversation.blockMarkAsRead(false); 3391 return; 3392 3393 case ConversationList.HAVE_LOCKED_MESSAGES_TOKEN: 3394 long threadId = (Long)cookie; 3395 ConversationList.confirmDeleteThreadDialog( 3396 new ConversationList.DeleteThreadListener(threadId, 3397 mBackgroundQueryHandler, ComposeMessageActivity.this), 3398 threadId == -1, 3399 cursor != null && cursor.getCount() > 0, 3400 ComposeMessageActivity.this); 3401 break; 3402 } 3403 } 3404 3405 @Override 3406 protected void onDeleteComplete(int token, Object cookie, int result) { 3407 switch(token) { 3408 case DELETE_MESSAGE_TOKEN: 3409 case ConversationList.DELETE_CONVERSATION_TOKEN: 3410 // Update the notification for new messages since they 3411 // may be deleted. 3412 MessagingNotification.nonBlockingUpdateNewMessageIndicator( 3413 ComposeMessageActivity.this, false, false); 3414 // Update the notification for failed messages since they 3415 // may be deleted. 3416 updateSendFailedNotification(); 3417 break; 3418 } 3419 3420 // If we're deleting the whole conversation, throw away 3421 // our current working message and bail. 3422 if (token == ConversationList.DELETE_CONVERSATION_TOKEN) { 3423 mWorkingMessage.discard(); 3424 Conversation.init(ComposeMessageActivity.this); 3425 finish(); 3426 } 3427 } 3428 } 3429 3430 private void showSmileyDialog() { 3431 if (mSmileyDialog == null) { 3432 int[] icons = SmileyParser.DEFAULT_SMILEY_RES_IDS; 3433 String[] names = getResources().getStringArray( 3434 SmileyParser.DEFAULT_SMILEY_NAMES); 3435 final String[] texts = getResources().getStringArray( 3436 SmileyParser.DEFAULT_SMILEY_TEXTS); 3437 3438 final int N = names.length; 3439 3440 List<Map<String, ?>> entries = new ArrayList<Map<String, ?>>(); 3441 for (int i = 0; i < N; i++) { 3442 // We might have different ASCII for the same icon, skip it if 3443 // the icon is already added. 3444 boolean added = false; 3445 for (int j = 0; j < i; j++) { 3446 if (icons[i] == icons[j]) { 3447 added = true; 3448 break; 3449 } 3450 } 3451 if (!added) { 3452 HashMap<String, Object> entry = new HashMap<String, Object>(); 3453 3454 entry. put("icon", icons[i]); 3455 entry. put("name", names[i]); 3456 entry.put("text", texts[i]); 3457 3458 entries.add(entry); 3459 } 3460 } 3461 3462 final SimpleAdapter a = new SimpleAdapter( 3463 this, 3464 entries, 3465 R.layout.smiley_menu_item, 3466 new String[] {"icon", "name", "text"}, 3467 new int[] {R.id.smiley_icon, R.id.smiley_name, R.id.smiley_text}); 3468 SimpleAdapter.ViewBinder viewBinder = new SimpleAdapter.ViewBinder() { 3469 public boolean setViewValue(View view, Object data, String textRepresentation) { 3470 if (view instanceof ImageView) { 3471 Drawable img = getResources().getDrawable((Integer)data); 3472 ((ImageView)view).setImageDrawable(img); 3473 return true; 3474 } 3475 return false; 3476 } 3477 }; 3478 a.setViewBinder(viewBinder); 3479 3480 AlertDialog.Builder b = new AlertDialog.Builder(this); 3481 3482 b.setTitle(getString(R.string.menu_insert_smiley)); 3483 3484 b.setCancelable(true); 3485 b.setAdapter(a, new DialogInterface.OnClickListener() { 3486 @SuppressWarnings("unchecked") 3487 public final void onClick(DialogInterface dialog, int which) { 3488 HashMap<String, Object> item = (HashMap<String, Object>) a.getItem(which); 3489 mTextEditor.append((String)item.get("text")); 3490 3491 dialog.dismiss(); 3492 } 3493 }); 3494 3495 mSmileyDialog = b.create(); 3496 } 3497 3498 mSmileyDialog.show(); 3499 } 3500 3501 public void onUpdate(final Contact updated) { 3502 // Using an existing handler for the post, rather than conjuring up a new one. 3503 mMessageListItemHandler.post(new Runnable() { 3504 public void run() { 3505 ContactList recipients = isRecipientsEditorVisible() ? 3506 mRecipientsEditor.constructContactsFromInput(false) : getRecipients(); 3507 if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) { 3508 log("[CMA] onUpdate contact updated: " + updated); 3509 log("[CMA] onUpdate recipients: " + recipients); 3510 } 3511 updateTitle(recipients); 3512 3513 // The contact information for one (or more) of the recipients has changed. 3514 // Rebuild the message list so each MessageItem will get the last contact info. 3515 ComposeMessageActivity.this.mMsgListAdapter.notifyDataSetChanged(); 3516 3517 if (mRecipientsEditor != null) { 3518 mRecipientsEditor.populate(recipients); 3519 } 3520 } 3521 }); 3522 } 3523 3524 private void addRecipientsListeners() { 3525 Contact.addListener(this); 3526 } 3527 3528 private void removeRecipientsListeners() { 3529 Contact.removeListener(this); 3530 } 3531 3532 public static Intent createIntent(Context context, long threadId) { 3533 Intent intent = new Intent(context, ComposeMessageActivity.class); 3534 3535 if (threadId > 0) { 3536 intent.setData(Conversation.getUri(threadId)); 3537 } 3538 3539 return intent; 3540 } 3541} 3542