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