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