1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.messaging.ui.conversation; 17 18 import android.content.Context; 19 import android.content.res.Resources; 20 import android.graphics.Rect; 21 import android.net.Uri; 22 import android.os.Bundle; 23 import android.support.v7.app.ActionBar; 24 import android.text.Editable; 25 import android.text.Html; 26 import android.text.InputFilter; 27 import android.text.InputFilter.LengthFilter; 28 import android.text.TextUtils; 29 import android.text.TextWatcher; 30 import android.util.AttributeSet; 31 import android.view.ContextThemeWrapper; 32 import android.view.KeyEvent; 33 import android.view.View; 34 import android.view.accessibility.AccessibilityEvent; 35 import android.view.inputmethod.EditorInfo; 36 import android.widget.ImageButton; 37 import android.widget.LinearLayout; 38 import android.widget.TextView; 39 40 import com.android.messaging.Factory; 41 import com.android.messaging.R; 42 import com.android.messaging.datamodel.binding.Binding; 43 import com.android.messaging.datamodel.binding.BindingBase; 44 import com.android.messaging.datamodel.binding.ImmutableBindingRef; 45 import com.android.messaging.datamodel.data.ConversationData; 46 import com.android.messaging.datamodel.data.ConversationData.ConversationDataListener; 47 import com.android.messaging.datamodel.data.ConversationData.SimpleConversationDataListener; 48 import com.android.messaging.datamodel.data.DraftMessageData; 49 import com.android.messaging.datamodel.data.DraftMessageData.CheckDraftForSendTask; 50 import com.android.messaging.datamodel.data.DraftMessageData.CheckDraftTaskCallback; 51 import com.android.messaging.datamodel.data.DraftMessageData.DraftMessageDataListener; 52 import com.android.messaging.datamodel.data.MessageData; 53 import com.android.messaging.datamodel.data.MessagePartData; 54 import com.android.messaging.datamodel.data.ParticipantData; 55 import com.android.messaging.datamodel.data.PendingAttachmentData; 56 import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry; 57 import com.android.messaging.sms.MmsConfig; 58 import com.android.messaging.ui.AttachmentPreview; 59 import com.android.messaging.ui.BugleActionBarActivity; 60 import com.android.messaging.ui.PlainTextEditText; 61 import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputSink; 62 import com.android.messaging.util.AccessibilityUtil; 63 import com.android.messaging.util.Assert; 64 import com.android.messaging.util.AvatarUriUtil; 65 import com.android.messaging.util.BuglePrefs; 66 import com.android.messaging.util.ContentType; 67 import com.android.messaging.util.LogUtil; 68 import com.android.messaging.util.MediaUtil; 69 import com.android.messaging.util.OsUtil; 70 import com.android.messaging.util.UiUtils; 71 72 import java.util.Collection; 73 import java.util.List; 74 75 /** 76 * This view contains the UI required to generate and send messages. 77 */ 78 public class ComposeMessageView extends LinearLayout 79 implements TextView.OnEditorActionListener, DraftMessageDataListener, TextWatcher, 80 ConversationInputSink { 81 82 public interface IComposeMessageViewHost extends 83 DraftMessageData.DraftMessageSubscriptionDataProvider { 84 void sendMessage(MessageData message); 85 void onComposeEditTextFocused(); 86 void onAttachmentsCleared(); 87 void onAttachmentsChanged(final boolean haveAttachments); 88 void displayPhoto(Uri photoUri, Rect imageBounds, boolean isDraft); 89 void promptForSelfPhoneNumber(); 90 boolean isReadyForAction(); 91 void warnOfMissingActionConditions(final boolean sending, 92 final Runnable commandToRunAfterActionConditionResolved); 93 void warnOfExceedingMessageLimit(final boolean showAttachmentChooser, 94 boolean tooManyVideos); 95 void notifyOfAttachmentLoadFailed(); 96 void showAttachmentChooser(); 97 boolean shouldShowSubjectEditor(); 98 boolean shouldHideAttachmentsWhenSimSelectorShown(); 99 Uri getSelfSendButtonIconUri(); 100 int overrideCounterColor(); 101 int getAttachmentsClearedFlags(); 102 } 103 104 public static final int CODEPOINTS_REMAINING_BEFORE_COUNTER_SHOWN = 10; 105 106 // There is no draft and there is no need for the SIM selector 107 private static final int SEND_WIDGET_MODE_SELF_AVATAR = 1; 108 // There is no draft but we need to show the SIM selector 109 private static final int SEND_WIDGET_MODE_SIM_SELECTOR = 2; 110 // There is a draft 111 private static final int SEND_WIDGET_MODE_SEND_BUTTON = 3; 112 113 private PlainTextEditText mComposeEditText; 114 private PlainTextEditText mComposeSubjectText; 115 private TextView mCharCounter; 116 private TextView mMmsIndicator; 117 private SimIconView mSelfSendIcon; 118 private ImageButton mSendButton; 119 private View mSubjectView; 120 private ImageButton mDeleteSubjectButton; 121 private AttachmentPreview mAttachmentPreview; 122 private ImageButton mAttachMediaButton; 123 124 private final Binding<DraftMessageData> mBinding; 125 private IComposeMessageViewHost mHost; 126 private final Context mOriginalContext; 127 private int mSendWidgetMode = SEND_WIDGET_MODE_SELF_AVATAR; 128 129 // Shared data model object binding from the conversation. 130 private ImmutableBindingRef<ConversationData> mConversationDataModel; 131 132 // Centrally manages all the mutual exclusive UI components accepting user input, i.e. 133 // media picker, IME keyboard and SIM selector. 134 private ConversationInputManager mInputManager; 135 136 private final ConversationDataListener mDataListener = new SimpleConversationDataListener() { 137 @Override 138 public void onConversationMetadataUpdated(ConversationData data) { 139 mConversationDataModel.ensureBound(data); 140 updateVisualsOnDraftChanged(); 141 } 142 143 @Override 144 public void onConversationParticipantDataLoaded(ConversationData data) { 145 mConversationDataModel.ensureBound(data); 146 updateVisualsOnDraftChanged(); 147 } 148 149 @Override 150 public void onSubscriptionListDataLoaded(ConversationData data) { 151 mConversationDataModel.ensureBound(data); 152 updateOnSelfSubscriptionChange(); 153 updateVisualsOnDraftChanged(); 154 } 155 }; 156 157 public ComposeMessageView(final Context context, final AttributeSet attrs) { 158 super(new ContextThemeWrapper(context, R.style.ColorAccentBlueOverrideStyle), attrs); 159 mOriginalContext = context; 160 mBinding = BindingBase.createBinding(this); 161 } 162 163 /** 164 * Host calls this to bind view to DraftMessageData object 165 */ 166 public void bind(final DraftMessageData data, final IComposeMessageViewHost host) { 167 mHost = host; 168 mBinding.bind(data); 169 data.addListener(this); 170 data.setSubscriptionDataProvider(host); 171 172 final int counterColor = mHost.overrideCounterColor(); 173 if (counterColor != -1) { 174 mCharCounter.setTextColor(counterColor); 175 } 176 } 177 178 /** 179 * Host calls this to unbind view 180 */ 181 public void unbind() { 182 mBinding.unbind(); 183 mHost = null; 184 mInputManager.onDetach(); 185 } 186 187 @Override 188 protected void onFinishInflate() { 189 mComposeEditText = (PlainTextEditText) findViewById( 190 R.id.compose_message_text); 191 mComposeEditText.setOnEditorActionListener(this); 192 mComposeEditText.addTextChangedListener(this); 193 mComposeEditText.setOnFocusChangeListener(new OnFocusChangeListener() { 194 @Override 195 public void onFocusChange(final View v, final boolean hasFocus) { 196 if (v == mComposeEditText && hasFocus) { 197 mHost.onComposeEditTextFocused(); 198 } 199 } 200 }); 201 mComposeEditText.setOnClickListener(new View.OnClickListener() { 202 @Override 203 public void onClick(View arg0) { 204 if (mHost.shouldHideAttachmentsWhenSimSelectorShown()) { 205 hideSimSelector(); 206 } 207 } 208 }); 209 210 // onFinishInflate() is called before self is loaded from db. We set the default text 211 // limit here, and apply the real limit later in updateOnSelfSubscriptionChange(). 212 mComposeEditText.setFilters(new InputFilter[] { 213 new LengthFilter(MmsConfig.get(ParticipantData.DEFAULT_SELF_SUB_ID) 214 .getMaxTextLimit()) }); 215 216 mSelfSendIcon = (SimIconView) findViewById(R.id.self_send_icon); 217 mSelfSendIcon.setOnClickListener(new OnClickListener() { 218 @Override 219 public void onClick(View v) { 220 boolean shown = mInputManager.toggleSimSelector(true /* animate */, 221 getSelfSubscriptionListEntry()); 222 hideAttachmentsWhenShowingSims(shown); 223 } 224 }); 225 mSelfSendIcon.setOnLongClickListener(new OnLongClickListener() { 226 @Override 227 public boolean onLongClick(final View v) { 228 if (mHost.shouldShowSubjectEditor()) { 229 showSubjectEditor(); 230 } else { 231 boolean shown = mInputManager.toggleSimSelector(true /* animate */, 232 getSelfSubscriptionListEntry()); 233 hideAttachmentsWhenShowingSims(shown); 234 } 235 return true; 236 } 237 }); 238 239 mComposeSubjectText = (PlainTextEditText) findViewById( 240 R.id.compose_subject_text); 241 // We need the listener to change the avatar to the send button when the user starts 242 // typing a subject without a message. 243 mComposeSubjectText.addTextChangedListener(this); 244 // onFinishInflate() is called before self is loaded from db. We set the default text 245 // limit here, and apply the real limit later in updateOnSelfSubscriptionChange(). 246 mComposeSubjectText.setFilters(new InputFilter[] { 247 new LengthFilter(MmsConfig.get(ParticipantData.DEFAULT_SELF_SUB_ID) 248 .getMaxSubjectLength())}); 249 250 mDeleteSubjectButton = (ImageButton) findViewById(R.id.delete_subject_button); 251 mDeleteSubjectButton.setOnClickListener(new OnClickListener() { 252 @Override 253 public void onClick(final View clickView) { 254 hideSubjectEditor(); 255 mComposeSubjectText.setText(null); 256 mBinding.getData().setMessageSubject(null); 257 } 258 }); 259 260 mSubjectView = findViewById(R.id.subject_view); 261 262 mSendButton = (ImageButton) findViewById(R.id.send_message_button); 263 mSendButton.setOnClickListener(new OnClickListener() { 264 @Override 265 public void onClick(final View clickView) { 266 sendMessageInternal(true /* checkMessageSize */); 267 } 268 }); 269 mSendButton.setOnLongClickListener(new OnLongClickListener() { 270 @Override 271 public boolean onLongClick(final View arg0) { 272 boolean shown = mInputManager.toggleSimSelector(true /* animate */, 273 getSelfSubscriptionListEntry()); 274 hideAttachmentsWhenShowingSims(shown); 275 if (mHost.shouldShowSubjectEditor()) { 276 showSubjectEditor(); 277 } 278 return true; 279 } 280 }); 281 mSendButton.setAccessibilityDelegate(new AccessibilityDelegate() { 282 @Override 283 public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { 284 super.onPopulateAccessibilityEvent(host, event); 285 // When the send button is long clicked, we want TalkBack to announce the real 286 // action (select SIM or edit subject), as opposed to "long press send button." 287 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_LONG_CLICKED) { 288 event.getText().clear(); 289 event.getText().add(getResources() 290 .getText(shouldShowSimSelector(mConversationDataModel.getData()) ? 291 R.string.send_button_long_click_description_with_sim_selector : 292 R.string.send_button_long_click_description_no_sim_selector)); 293 // Make this an announcement so TalkBack will read our custom message. 294 event.setEventType(AccessibilityEvent.TYPE_ANNOUNCEMENT); 295 } 296 } 297 }); 298 299 mAttachMediaButton = 300 (ImageButton) findViewById(R.id.attach_media_button); 301 mAttachMediaButton.setOnClickListener(new View.OnClickListener() { 302 @Override 303 public void onClick(final View clickView) { 304 // Showing the media picker is treated as starting to compose the message. 305 mInputManager.showHideMediaPicker(true /* show */, true /* animate */); 306 } 307 }); 308 309 mAttachmentPreview = (AttachmentPreview) findViewById(R.id.attachment_draft_view); 310 mAttachmentPreview.setComposeMessageView(this); 311 312 mCharCounter = (TextView) findViewById(R.id.char_counter); 313 mMmsIndicator = (TextView) findViewById(R.id.mms_indicator); 314 } 315 316 private void hideAttachmentsWhenShowingSims(final boolean simPickerVisible) { 317 if (!mHost.shouldHideAttachmentsWhenSimSelectorShown()) { 318 return; 319 } 320 final boolean haveAttachments = mBinding.getData().hasAttachments(); 321 if (simPickerVisible && haveAttachments) { 322 mHost.onAttachmentsChanged(false); 323 mAttachmentPreview.hideAttachmentPreview(); 324 } else { 325 mHost.onAttachmentsChanged(haveAttachments); 326 mAttachmentPreview.onAttachmentsChanged(mBinding.getData()); 327 } 328 } 329 330 public void setInputManager(final ConversationInputManager inputManager) { 331 mInputManager = inputManager; 332 } 333 334 public void setConversationDataModel(final ImmutableBindingRef<ConversationData> refDataModel) { 335 mConversationDataModel = refDataModel; 336 mConversationDataModel.getData().addConversationDataListener(mDataListener); 337 } 338 339 ImmutableBindingRef<DraftMessageData> getDraftDataModel() { 340 return BindingBase.createBindingReference(mBinding); 341 } 342 343 // returns true if it actually shows the subject editor and false if already showing 344 private boolean showSubjectEditor() { 345 // show the subject editor 346 if (mSubjectView.getVisibility() == View.GONE) { 347 mSubjectView.setVisibility(View.VISIBLE); 348 mSubjectView.requestFocus(); 349 return true; 350 } 351 return false; 352 } 353 354 private void hideSubjectEditor() { 355 mSubjectView.setVisibility(View.GONE); 356 mComposeEditText.requestFocus(); 357 } 358 359 /** 360 * {@inheritDoc} from TextView.OnEditorActionListener 361 */ 362 @Override // TextView.OnEditorActionListener.onEditorAction 363 public boolean onEditorAction(final TextView view, final int actionId, final KeyEvent event) { 364 if (actionId == EditorInfo.IME_ACTION_SEND) { 365 sendMessageInternal(true /* checkMessageSize */); 366 return true; 367 } 368 return false; 369 } 370 371 private void sendMessageInternal(final boolean checkMessageSize) { 372 LogUtil.i(LogUtil.BUGLE_TAG, "UI initiated message sending in conversation " + 373 mBinding.getData().getConversationId()); 374 if (mBinding.getData().isCheckingDraft()) { 375 // Don't send message if we are currently checking draft for sending. 376 LogUtil.w(LogUtil.BUGLE_TAG, "Message can't be sent: still checking draft"); 377 return; 378 } 379 // Check the host for pre-conditions about any action. 380 if (mHost.isReadyForAction()) { 381 mInputManager.showHideSimSelector(false /* show */, true /* animate */); 382 final String messageToSend = mComposeEditText.getText().toString(); 383 mBinding.getData().setMessageText(messageToSend); 384 final String subject = mComposeSubjectText.getText().toString(); 385 mBinding.getData().setMessageSubject(subject); 386 // Asynchronously check the draft against various requirements before sending. 387 mBinding.getData().checkDraftForAction(checkMessageSize, 388 mHost.getConversationSelfSubId(), new CheckDraftTaskCallback() { 389 @Override 390 public void onDraftChecked(DraftMessageData data, int result) { 391 mBinding.ensureBound(data); 392 switch (result) { 393 case CheckDraftForSendTask.RESULT_PASSED: 394 // Continue sending after check succeeded. 395 final MessageData message = mBinding.getData() 396 .prepareMessageForSending(mBinding); 397 if (message != null && message.hasContent()) { 398 playSentSound(); 399 mHost.sendMessage(message); 400 hideSubjectEditor(); 401 if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) { 402 AccessibilityUtil.announceForAccessibilityCompat( 403 ComposeMessageView.this, null, 404 R.string.sending_message); 405 } 406 } 407 break; 408 409 case CheckDraftForSendTask.RESULT_HAS_PENDING_ATTACHMENTS: 410 // Cannot send while there's still attachment(s) being loaded. 411 UiUtils.showToastAtBottom( 412 R.string.cant_send_message_while_loading_attachments); 413 break; 414 415 case CheckDraftForSendTask.RESULT_NO_SELF_PHONE_NUMBER_IN_GROUP_MMS: 416 mHost.promptForSelfPhoneNumber(); 417 break; 418 419 case CheckDraftForSendTask.RESULT_MESSAGE_OVER_LIMIT: 420 Assert.isTrue(checkMessageSize); 421 mHost.warnOfExceedingMessageLimit( 422 true /*sending*/, false /* tooManyVideos */); 423 break; 424 425 case CheckDraftForSendTask.RESULT_VIDEO_ATTACHMENT_LIMIT_EXCEEDED: 426 Assert.isTrue(checkMessageSize); 427 mHost.warnOfExceedingMessageLimit( 428 true /*sending*/, true /* tooManyVideos */); 429 break; 430 431 case CheckDraftForSendTask.RESULT_SIM_NOT_READY: 432 // Cannot send if there is no active subscription 433 UiUtils.showToastAtBottom( 434 R.string.cant_send_message_without_active_subscription); 435 break; 436 437 default: 438 break; 439 } 440 } 441 }, mBinding); 442 } else { 443 mHost.warnOfMissingActionConditions(true /*sending*/, 444 new Runnable() { 445 @Override 446 public void run() { 447 sendMessageInternal(checkMessageSize); 448 } 449 450 }); 451 } 452 } 453 454 public static void playSentSound() { 455 // Check if this setting is enabled before playing 456 final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); 457 final Context context = Factory.get().getApplicationContext(); 458 final String prefKey = context.getString(R.string.send_sound_pref_key); 459 final boolean defaultValue = context.getResources().getBoolean( 460 R.bool.send_sound_pref_default); 461 if (!prefs.getBoolean(prefKey, defaultValue)) { 462 return; 463 } 464 MediaUtil.get().playSound(context, R.raw.message_sent, null /* completionListener */); 465 } 466 467 /** 468 * {@inheritDoc} from DraftMessageDataListener 469 */ 470 @Override // From DraftMessageDataListener 471 public void onDraftChanged(final DraftMessageData data, final int changeFlags) { 472 // As this is called asynchronously when message read check bound before updating text 473 mBinding.ensureBound(data); 474 475 // We have to cache the values of the DraftMessageData because when we set 476 // mComposeEditText, its onTextChanged calls updateVisualsOnDraftChanged, 477 // which immediately reloads the text from the subject and message fields and replaces 478 // what's in the DraftMessageData. 479 480 final String subject = data.getMessageSubject(); 481 final String message = data.getMessageText(); 482 483 if ((changeFlags & DraftMessageData.MESSAGE_SUBJECT_CHANGED) == 484 DraftMessageData.MESSAGE_SUBJECT_CHANGED) { 485 mComposeSubjectText.setText(subject); 486 487 // Set the cursor selection to the end since setText resets it to the start 488 mComposeSubjectText.setSelection(mComposeSubjectText.getText().length()); 489 } 490 491 if ((changeFlags & DraftMessageData.MESSAGE_TEXT_CHANGED) == 492 DraftMessageData.MESSAGE_TEXT_CHANGED) { 493 mComposeEditText.setText(message); 494 495 // Set the cursor selection to the end since setText resets it to the start 496 mComposeEditText.setSelection(mComposeEditText.getText().length()); 497 } 498 499 if ((changeFlags & DraftMessageData.ATTACHMENTS_CHANGED) == 500 DraftMessageData.ATTACHMENTS_CHANGED) { 501 final boolean haveAttachments = mAttachmentPreview.onAttachmentsChanged(data); 502 mHost.onAttachmentsChanged(haveAttachments); 503 } 504 505 if ((changeFlags & DraftMessageData.SELF_CHANGED) == DraftMessageData.SELF_CHANGED) { 506 updateOnSelfSubscriptionChange(); 507 } 508 updateVisualsOnDraftChanged(); 509 } 510 511 @Override // From DraftMessageDataListener 512 public void onDraftAttachmentLimitReached(final DraftMessageData data) { 513 mBinding.ensureBound(data); 514 mHost.warnOfExceedingMessageLimit(false /* sending */, false /* tooManyVideos */); 515 } 516 517 private void updateOnSelfSubscriptionChange() { 518 // Refresh the length filters according to the selected self's MmsConfig. 519 mComposeEditText.setFilters(new InputFilter[] { 520 new LengthFilter(MmsConfig.get(mBinding.getData().getSelfSubId()) 521 .getMaxTextLimit()) }); 522 mComposeSubjectText.setFilters(new InputFilter[] { 523 new LengthFilter(MmsConfig.get(mBinding.getData().getSelfSubId()) 524 .getMaxSubjectLength())}); 525 } 526 527 @Override 528 public void onMediaItemsSelected(final Collection<MessagePartData> items) { 529 mBinding.getData().addAttachments(items); 530 announceMediaItemState(true /*isSelected*/); 531 } 532 533 @Override 534 public void onMediaItemsUnselected(final MessagePartData item) { 535 mBinding.getData().removeAttachment(item); 536 announceMediaItemState(false /*isSelected*/); 537 } 538 539 @Override 540 public void onPendingAttachmentAdded(final PendingAttachmentData pendingItem) { 541 mBinding.getData().addPendingAttachment(pendingItem, mBinding); 542 resumeComposeMessage(); 543 } 544 545 private void announceMediaItemState(final boolean isSelected) { 546 final Resources res = getContext().getResources(); 547 final String announcement = isSelected ? res.getString( 548 R.string.mediapicker_gallery_item_selected_content_description) : 549 res.getString(R.string.mediapicker_gallery_item_unselected_content_description); 550 AccessibilityUtil.announceForAccessibilityCompat( 551 this, null, announcement); 552 } 553 554 private void announceAttachmentState() { 555 if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) { 556 int attachmentCount = mBinding.getData().getReadOnlyAttachments().size() 557 + mBinding.getData().getReadOnlyPendingAttachments().size(); 558 final String announcement = getContext().getResources().getQuantityString( 559 R.plurals.attachment_changed_accessibility_announcement, 560 attachmentCount, attachmentCount); 561 AccessibilityUtil.announceForAccessibilityCompat( 562 this, null, announcement); 563 } 564 } 565 566 @Override 567 public void resumeComposeMessage() { 568 mComposeEditText.requestFocus(); 569 mInputManager.showHideImeKeyboard(true, true); 570 announceAttachmentState(); 571 } 572 573 public void clearAttachments() { 574 mBinding.getData().clearAttachments(mHost.getAttachmentsClearedFlags()); 575 mHost.onAttachmentsCleared(); 576 } 577 578 public void requestDraftMessage(boolean clearLocalDraft) { 579 mBinding.getData().loadFromStorage(mBinding, null, clearLocalDraft); 580 } 581 582 public void setDraftMessage(final MessageData message) { 583 mBinding.getData().loadFromStorage(mBinding, message, false); 584 } 585 586 public void writeDraftMessage() { 587 final String messageText = mComposeEditText.getText().toString(); 588 mBinding.getData().setMessageText(messageText); 589 590 final String subject = mComposeSubjectText.getText().toString(); 591 mBinding.getData().setMessageSubject(subject); 592 593 mBinding.getData().saveToStorage(mBinding); 594 } 595 596 private void updateConversationSelfId(final String selfId, final boolean notify) { 597 mBinding.getData().setSelfId(selfId, notify); 598 } 599 600 private Uri getSelfSendButtonIconUri() { 601 final Uri overridenSelfUri = mHost.getSelfSendButtonIconUri(); 602 if (overridenSelfUri != null) { 603 return overridenSelfUri; 604 } 605 final SubscriptionListEntry subscriptionListEntry = getSelfSubscriptionListEntry(); 606 607 if (subscriptionListEntry != null) { 608 return subscriptionListEntry.selectedIconUri; 609 } 610 611 // Fall back to default self-avatar in the base case. 612 final ParticipantData self = mConversationDataModel.getData().getDefaultSelfParticipant(); 613 return self == null ? null : AvatarUriUtil.createAvatarUri(self); 614 } 615 616 private SubscriptionListEntry getSelfSubscriptionListEntry() { 617 return mConversationDataModel.getData().getSubscriptionEntryForSelfParticipant( 618 mBinding.getData().getSelfId(), false /* excludeDefault */); 619 } 620 621 private boolean isDataLoadedForMessageSend() { 622 // Check data loading prerequisites for sending a message. 623 return mConversationDataModel != null && mConversationDataModel.isBound() && 624 mConversationDataModel.getData().getParticipantsLoaded(); 625 } 626 627 private void updateVisualsOnDraftChanged() { 628 final String messageText = mComposeEditText.getText().toString(); 629 final DraftMessageData draftMessageData = mBinding.getData(); 630 draftMessageData.setMessageText(messageText); 631 632 final String subject = mComposeSubjectText.getText().toString(); 633 draftMessageData.setMessageSubject(subject); 634 if (!TextUtils.isEmpty(subject)) { 635 mSubjectView.setVisibility(View.VISIBLE); 636 } 637 638 final boolean hasMessageText = (TextUtils.getTrimmedLength(messageText) > 0); 639 final boolean hasSubject = (TextUtils.getTrimmedLength(subject) > 0); 640 final boolean hasWorkingDraft = hasMessageText || hasSubject || 641 mBinding.getData().hasAttachments(); 642 643 // Update the SMS text counter. 644 final int messageCount = draftMessageData.getNumMessagesToBeSent(); 645 final int codePointsRemaining = draftMessageData.getCodePointsRemainingInCurrentMessage(); 646 // Show the counter only if: 647 // - We are not in MMS mode 648 // - We are going to send more than one message OR we are getting close 649 boolean showCounter = false; 650 if (!draftMessageData.getIsMms() && (messageCount > 1 || 651 codePointsRemaining <= CODEPOINTS_REMAINING_BEFORE_COUNTER_SHOWN)) { 652 showCounter = true; 653 } 654 655 if (showCounter) { 656 // Update the remaining characters and number of messages required. 657 final String counterText = messageCount > 1 ? codePointsRemaining + " / " + 658 messageCount : String.valueOf(codePointsRemaining); 659 mCharCounter.setText(counterText); 660 mCharCounter.setVisibility(View.VISIBLE); 661 } else { 662 mCharCounter.setVisibility(View.INVISIBLE); 663 } 664 665 // Update the send message button. Self icon uri might be null if self participant data 666 // and/or conversation metadata hasn't been loaded by the host. 667 final Uri selfSendButtonUri = getSelfSendButtonIconUri(); 668 int sendWidgetMode = SEND_WIDGET_MODE_SELF_AVATAR; 669 if (selfSendButtonUri != null) { 670 if (hasWorkingDraft && isDataLoadedForMessageSend()) { 671 UiUtils.revealOrHideViewWithAnimation(mSendButton, VISIBLE, null); 672 if (isOverriddenAvatarAGroup()) { 673 // If the host has overriden the avatar to show a group avatar where the 674 // send button sits, we have to hide the group avatar because it can be larger 675 // than the send button and pieces of the avatar will stick out from behind 676 // the send button. 677 UiUtils.revealOrHideViewWithAnimation(mSelfSendIcon, GONE, null); 678 } 679 mMmsIndicator.setVisibility(draftMessageData.getIsMms() ? VISIBLE : INVISIBLE); 680 sendWidgetMode = SEND_WIDGET_MODE_SEND_BUTTON; 681 } else { 682 mSelfSendIcon.setImageResourceUri(selfSendButtonUri); 683 if (isOverriddenAvatarAGroup()) { 684 UiUtils.revealOrHideViewWithAnimation(mSelfSendIcon, VISIBLE, null); 685 } 686 UiUtils.revealOrHideViewWithAnimation(mSendButton, GONE, null); 687 mMmsIndicator.setVisibility(INVISIBLE); 688 if (shouldShowSimSelector(mConversationDataModel.getData())) { 689 sendWidgetMode = SEND_WIDGET_MODE_SIM_SELECTOR; 690 } 691 } 692 } else { 693 mSelfSendIcon.setImageResourceUri(null); 694 } 695 696 if (mSendWidgetMode != sendWidgetMode || sendWidgetMode == SEND_WIDGET_MODE_SIM_SELECTOR) { 697 setSendButtonAccessibility(sendWidgetMode); 698 mSendWidgetMode = sendWidgetMode; 699 } 700 701 // Update the text hint on the message box depending on the attachment type. 702 final List<MessagePartData> attachments = draftMessageData.getReadOnlyAttachments(); 703 final int attachmentCount = attachments.size(); 704 if (attachmentCount == 0) { 705 final SubscriptionListEntry subscriptionListEntry = 706 mConversationDataModel.getData().getSubscriptionEntryForSelfParticipant( 707 mBinding.getData().getSelfId(), false /* excludeDefault */); 708 if (subscriptionListEntry == null) { 709 mComposeEditText.setHint(R.string.compose_message_view_hint_text); 710 } else { 711 mComposeEditText.setHint(Html.fromHtml(getResources().getString( 712 R.string.compose_message_view_hint_text_multi_sim, 713 subscriptionListEntry.displayName))); 714 } 715 } else { 716 int type = -1; 717 for (final MessagePartData attachment : attachments) { 718 int newType; 719 if (attachment.isImage()) { 720 newType = ContentType.TYPE_IMAGE; 721 } else if (attachment.isAudio()) { 722 newType = ContentType.TYPE_AUDIO; 723 } else if (attachment.isVideo()) { 724 newType = ContentType.TYPE_VIDEO; 725 } else if (attachment.isVCard()) { 726 newType = ContentType.TYPE_VCARD; 727 } else { 728 newType = ContentType.TYPE_OTHER; 729 } 730 731 if (type == -1) { 732 type = newType; 733 } else if (type != newType || type == ContentType.TYPE_OTHER) { 734 type = ContentType.TYPE_OTHER; 735 break; 736 } 737 } 738 739 switch (type) { 740 case ContentType.TYPE_IMAGE: 741 mComposeEditText.setHint(getResources().getQuantityString( 742 R.plurals.compose_message_view_hint_text_photo, attachmentCount)); 743 break; 744 745 case ContentType.TYPE_AUDIO: 746 mComposeEditText.setHint(getResources().getQuantityString( 747 R.plurals.compose_message_view_hint_text_audio, attachmentCount)); 748 break; 749 750 case ContentType.TYPE_VIDEO: 751 mComposeEditText.setHint(getResources().getQuantityString( 752 R.plurals.compose_message_view_hint_text_video, attachmentCount)); 753 break; 754 755 case ContentType.TYPE_VCARD: 756 mComposeEditText.setHint(getResources().getQuantityString( 757 R.plurals.compose_message_view_hint_text_vcard, attachmentCount)); 758 break; 759 760 case ContentType.TYPE_OTHER: 761 mComposeEditText.setHint(getResources().getQuantityString( 762 R.plurals.compose_message_view_hint_text_attachments, attachmentCount)); 763 break; 764 765 default: 766 Assert.fail("Unsupported attachment type!"); 767 break; 768 } 769 } 770 } 771 772 private void setSendButtonAccessibility(final int sendWidgetMode) { 773 switch (sendWidgetMode) { 774 case SEND_WIDGET_MODE_SELF_AVATAR: 775 // No send button and no SIM selector; the self send button is no longer 776 // important for accessibility. 777 mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 778 mSelfSendIcon.setContentDescription(null); 779 mSendButton.setVisibility(View.GONE); 780 setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SELF_AVATAR); 781 break; 782 783 case SEND_WIDGET_MODE_SIM_SELECTOR: 784 mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 785 mSelfSendIcon.setContentDescription(getSimContentDescription()); 786 setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SIM_SELECTOR); 787 break; 788 789 case SEND_WIDGET_MODE_SEND_BUTTON: 790 mMmsIndicator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 791 mMmsIndicator.setContentDescription(null); 792 setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SEND_BUTTON); 793 break; 794 } 795 } 796 797 private String getSimContentDescription() { 798 final SubscriptionListEntry sub = getSelfSubscriptionListEntry(); 799 if (sub != null) { 800 return getResources().getString( 801 R.string.sim_selector_button_content_description_with_selection, 802 sub.displayName); 803 } else { 804 return getResources().getString( 805 R.string.sim_selector_button_content_description); 806 } 807 } 808 809 // Set accessibility traversal order of the components in the send widget. 810 private void setSendWidgetAccessibilityTraversalOrder(final int mode) { 811 if (OsUtil.isAtLeastL_MR1()) { 812 mAttachMediaButton.setAccessibilityTraversalBefore(R.id.compose_message_text); 813 switch (mode) { 814 case SEND_WIDGET_MODE_SIM_SELECTOR: 815 mComposeEditText.setAccessibilityTraversalBefore(R.id.self_send_icon); 816 break; 817 case SEND_WIDGET_MODE_SEND_BUTTON: 818 mComposeEditText.setAccessibilityTraversalBefore(R.id.send_message_button); 819 break; 820 default: 821 break; 822 } 823 } 824 } 825 826 @Override 827 public void afterTextChanged(final Editable editable) { 828 } 829 830 @Override 831 public void beforeTextChanged(final CharSequence s, final int start, final int count, 832 final int after) { 833 if (mHost.shouldHideAttachmentsWhenSimSelectorShown()) { 834 hideSimSelector(); 835 } 836 } 837 838 private void hideSimSelector() { 839 if (mInputManager.showHideSimSelector(false /* show */, true /* animate */)) { 840 // Now that the sim selector has been hidden, reshow the attachments if they 841 // have been hidden. 842 hideAttachmentsWhenShowingSims(false /*simPickerVisible*/); 843 } 844 } 845 846 @Override 847 public void onTextChanged(final CharSequence s, final int start, final int before, 848 final int count) { 849 final BugleActionBarActivity activity = (mOriginalContext instanceof BugleActionBarActivity) 850 ? (BugleActionBarActivity) mOriginalContext : null; 851 if (activity != null && activity.getIsDestroyed()) { 852 LogUtil.v(LogUtil.BUGLE_TAG, "got onTextChanged after onDestroy"); 853 854 // if we get onTextChanged after the activity is destroyed then, ah, wtf 855 // b/18176615 856 // This appears to have occurred as the result of orientation change. 857 return; 858 } 859 860 mBinding.ensureBound(); 861 updateVisualsOnDraftChanged(); 862 } 863 864 @Override 865 public PlainTextEditText getComposeEditText() { 866 return mComposeEditText; 867 } 868 869 public void displayPhoto(final Uri photoUri, final Rect imageBounds) { 870 mHost.displayPhoto(photoUri, imageBounds, true /* isDraft */); 871 } 872 873 public void updateConversationSelfIdOnExternalChange(final String selfId) { 874 updateConversationSelfId(selfId, true /* notify */); 875 } 876 877 /** 878 * The selfId of the conversation. As soon as the DraftMessageData successfully loads (i.e. 879 * getSelfId() is non-null), the selfId in DraftMessageData is treated as the sole source 880 * of truth for conversation self id since it reflects any pending self id change the user 881 * makes in the UI. 882 */ 883 public String getConversationSelfId() { 884 return mBinding.getData().getSelfId(); 885 } 886 887 public void selectSim(SubscriptionListEntry subscriptionData) { 888 final String oldSelfId = getConversationSelfId(); 889 final String newSelfId = subscriptionData.selfParticipantId; 890 Assert.notNull(newSelfId); 891 // Don't attempt to change self if self hasn't been loaded, or if self hasn't changed. 892 if (oldSelfId == null || TextUtils.equals(oldSelfId, newSelfId)) { 893 return; 894 } 895 updateConversationSelfId(newSelfId, true /* notify */); 896 } 897 898 public void hideAllComposeInputs(final boolean animate) { 899 mInputManager.hideAllInputs(animate); 900 } 901 902 public void saveInputState(final Bundle outState) { 903 mInputManager.onSaveInputState(outState); 904 } 905 906 public void resetMediaPickerState() { 907 mInputManager.resetMediaPickerState(); 908 } 909 910 public boolean onBackPressed() { 911 return mInputManager.onBackPressed(); 912 } 913 914 public boolean onNavigationUpPressed() { 915 return mInputManager.onNavigationUpPressed(); 916 } 917 918 public boolean updateActionBar(final ActionBar actionBar) { 919 return mInputManager != null ? mInputManager.updateActionBar(actionBar) : false; 920 } 921 922 public static boolean shouldShowSimSelector(final ConversationData convData) { 923 return OsUtil.isAtLeastL_MR1() && 924 convData.getSelfParticipantsCountExcludingDefault(true /* activeOnly */) > 1; 925 } 926 927 public void sendMessageIgnoreMessageSizeLimit() { 928 sendMessageInternal(false /* checkMessageSize */); 929 } 930 931 public void onAttachmentPreviewLongClicked() { 932 mHost.showAttachmentChooser(); 933 } 934 935 @Override 936 public void onDraftAttachmentLoadFailed() { 937 mHost.notifyOfAttachmentLoadFailed(); 938 } 939 940 private boolean isOverriddenAvatarAGroup() { 941 final Uri overridenSelfUri = mHost.getSelfSendButtonIconUri(); 942 if (overridenSelfUri == null) { 943 return false; 944 } 945 return AvatarUriUtil.TYPE_GROUP_URI.equals(AvatarUriUtil.getAvatarType(overridenSelfUri)); 946 } 947 948 @Override 949 public void setAccessibility(boolean enabled) { 950 if (enabled) { 951 mAttachMediaButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 952 mComposeEditText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 953 mSendButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 954 setSendButtonAccessibility(mSendWidgetMode); 955 } else { 956 mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 957 mComposeEditText.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 958 mSendButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 959 mAttachMediaButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 960 } 961 } 962 } 963