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