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 17 package com.android.systemui.statusbar.policy; 18 19 import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; 20 21 import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_STANDARD; 22 23 import android.app.ActivityManager; 24 import android.content.Context; 25 import android.content.pm.PackageManager; 26 import android.content.res.ColorStateList; 27 import android.graphics.BlendMode; 28 import android.graphics.Color; 29 import android.graphics.PorterDuff; 30 import android.graphics.Rect; 31 import android.graphics.drawable.GradientDrawable; 32 import android.os.Trace; 33 import android.os.UserHandle; 34 import android.text.Editable; 35 import android.text.SpannedString; 36 import android.text.TextWatcher; 37 import android.util.ArraySet; 38 import android.util.AttributeSet; 39 import android.util.Log; 40 import android.util.Pair; 41 import android.view.ContentInfo; 42 import android.view.KeyEvent; 43 import android.view.LayoutInflater; 44 import android.view.MotionEvent; 45 import android.view.OnReceiveContentListener; 46 import android.view.View; 47 import android.view.ViewGroup; 48 import android.view.ViewRootImpl; 49 import android.view.WindowInsets; 50 import android.view.WindowInsetsAnimation; 51 import android.view.WindowInsetsController; 52 import android.view.accessibility.AccessibilityEvent; 53 import android.view.inputmethod.CompletionInfo; 54 import android.view.inputmethod.EditorInfo; 55 import android.view.inputmethod.InputConnection; 56 import android.view.inputmethod.InputMethodManager; 57 import android.widget.EditText; 58 import android.widget.FrameLayout; 59 import android.widget.ImageButton; 60 import android.widget.ImageView; 61 import android.widget.LinearLayout; 62 import android.widget.ProgressBar; 63 import android.widget.TextView; 64 import android.window.OnBackInvokedCallback; 65 import android.window.OnBackInvokedDispatcher; 66 67 import androidx.annotation.NonNull; 68 import androidx.annotation.Nullable; 69 import androidx.core.animation.Animator; 70 import androidx.core.animation.AnimatorListenerAdapter; 71 import androidx.core.animation.AnimatorSet; 72 import androidx.core.animation.ObjectAnimator; 73 import androidx.core.animation.ValueAnimator; 74 75 import com.android.app.animation.InterpolatorsAndroidX; 76 import com.android.internal.annotations.VisibleForTesting; 77 import com.android.internal.graphics.ColorUtils; 78 import com.android.internal.logging.UiEvent; 79 import com.android.internal.logging.UiEventLogger; 80 import com.android.internal.util.ContrastColorUtil; 81 import com.android.systemui.Dependency; 82 import com.android.systemui.Flags; 83 import com.android.systemui.res.R; 84 import com.android.systemui.statusbar.RemoteInputController; 85 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 86 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; 87 import com.android.systemui.statusbar.phone.LightBarController; 88 89 import java.util.ArrayList; 90 import java.util.Collection; 91 import java.util.List; 92 import java.util.function.Consumer; 93 94 /** 95 * Host for the remote input. 96 */ 97 public class RemoteInputView extends LinearLayout implements View.OnClickListener { 98 99 private static final boolean DEBUG = false; 100 private static final String TAG = "RemoteInput"; 101 102 // A marker object that let's us easily find views of this class. 103 public static final Object VIEW_TAG = new Object(); 104 105 private static final long FOCUS_ANIMATION_TOTAL_DURATION = ANIMATION_DURATION_STANDARD; 106 private static final long FOCUS_ANIMATION_CROSSFADE_DURATION = 50; 107 private static final long FOCUS_ANIMATION_FADE_IN_DELAY = 33; 108 private static final long FOCUS_ANIMATION_FADE_IN_DURATION = 83; 109 public static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f; 110 private static final long DEFOCUS_ANIMATION_FADE_OUT_DELAY = 120; 111 private static final long DEFOCUS_ANIMATION_CROSSFADE_DELAY = 180; 112 113 public final Object mToken = new Object(); 114 115 private final SendButtonTextWatcher mTextWatcher; 116 private final TextView.OnEditorActionListener mEditorActionHandler; 117 private final ArrayList<Runnable> mOnSendListeners = new ArrayList<>(); 118 private Consumer<Boolean> mOnVisibilityChangedListener = null; 119 private final ArrayList<OnFocusChangeListener> mEditTextFocusChangeListeners = 120 new ArrayList<>(); 121 122 private RemoteEditText mEditText; 123 private ImageButton mSendButton; 124 private LinearLayout mContentView; 125 private GradientDrawable mContentBackground; 126 private ProgressBar mProgressBar; 127 private ImageView mDelete; 128 private ImageView mDeleteBg; 129 private boolean mColorized; 130 private int mLastBackgroundColor; 131 private boolean mResetting; 132 private Rect mContentBackgroundBounds; 133 private boolean mIsAnimatingAppearance = false; 134 135 // TODO(b/193539698): move these to a Controller 136 private RemoteInputController mController; 137 private final UiEventLogger mUiEventLogger; 138 private NotificationEntry mEntry; 139 private boolean mRemoved; 140 private boolean mSending; 141 private NotificationViewWrapper mWrapper; 142 143 // TODO(b/193539698): remove this; views shouldn't have access to their controller, and places 144 // that need the controller shouldn't have access to the view 145 private RemoteInputViewController mViewController; 146 private ViewRootImpl mTestableViewRootImpl; 147 148 /** 149 * Enum for logged notification remote input UiEvents. 150 */ 151 enum NotificationRemoteInputEvent implements UiEventLogger.UiEventEnum { 152 @UiEvent(doc = "Notification remote input view was displayed") 153 NOTIFICATION_REMOTE_INPUT_OPEN(795), 154 @UiEvent(doc = "Notification remote input view was closed") 155 NOTIFICATION_REMOTE_INPUT_CLOSE(796), 156 @UiEvent(doc = "User sent data through the notification remote input view") 157 NOTIFICATION_REMOTE_INPUT_SEND(797), 158 @UiEvent(doc = "Failed attempt to send data through the notification remote input view") 159 NOTIFICATION_REMOTE_INPUT_FAILURE(798), 160 @UiEvent(doc = "User attached an image to the remote input view") 161 NOTIFICATION_REMOTE_INPUT_ATTACH_IMAGE(825); 162 163 private final int mId; NotificationRemoteInputEvent(int id)164 NotificationRemoteInputEvent(int id) { 165 mId = id; 166 } getId()167 @Override public int getId() { 168 return mId; 169 } 170 } 171 RemoteInputView(Context context, AttributeSet attrs)172 public RemoteInputView(Context context, AttributeSet attrs) { 173 super(context, attrs); 174 mTextWatcher = new SendButtonTextWatcher(); 175 mEditorActionHandler = new EditorActionHandler(); 176 mUiEventLogger = Dependency.get(UiEventLogger.class); 177 mLastBackgroundColor = getContext().getColor( 178 com.android.internal.R.color.materialColorSurfaceDim); 179 } 180 181 // TODO(b/193539698): move to Controller, since we're just directly accessing a system service 182 /** Hide the IME, if visible. */ hideIme()183 public void hideIme() { 184 mEditText.hideIme(); 185 } 186 colorStateListWithDisabledAlpha(int color, int disabledAlpha)187 private ColorStateList colorStateListWithDisabledAlpha(int color, int disabledAlpha) { 188 return new ColorStateList(new int[][]{ 189 new int[]{-com.android.internal.R.attr.state_enabled}, // disabled 190 new int[]{}, 191 }, new int[]{ 192 ColorUtils.setAlphaComponent(color, disabledAlpha), 193 color 194 }); 195 } 196 197 /** 198 * The remote view needs to adapt to colorized notifications when set 199 * It overrides the background of itself as well as all of its childern 200 * @param backgroundColor colorized notification color 201 */ setBackgroundTintColor(final int backgroundColor, boolean colorized)202 public void setBackgroundTintColor(final int backgroundColor, boolean colorized) { 203 if (colorized == mColorized && backgroundColor == mLastBackgroundColor) return; 204 mColorized = colorized; 205 mLastBackgroundColor = backgroundColor; 206 final int editBgColor; 207 final int deleteBgColor; 208 final int deleteFgColor; 209 final ColorStateList accentColor; 210 final ColorStateList textColor; 211 final int hintColor; 212 final int stroke = colorized ? mContext.getResources().getDimensionPixelSize( 213 R.dimen.remote_input_view_text_stroke) : 0; 214 if (colorized) { 215 final boolean dark = ContrastColorUtil.isColorDark(backgroundColor); 216 final int foregroundColor = dark ? Color.WHITE : Color.BLACK; 217 final int inverseColor = dark ? Color.BLACK : Color.WHITE; 218 editBgColor = backgroundColor; 219 deleteBgColor = foregroundColor; 220 deleteFgColor = inverseColor; 221 accentColor = colorStateListWithDisabledAlpha(foregroundColor, 0x4D); // 30% 222 textColor = colorStateListWithDisabledAlpha(foregroundColor, 0x99); // 60% 223 hintColor = ColorUtils.setAlphaComponent(foregroundColor, 0x99); 224 } else { 225 accentColor = mContext.getColorStateList(R.color.remote_input_send); 226 textColor = mContext.getColorStateList(R.color.remote_input_text); 227 hintColor = mContext.getColor(R.color.remote_input_hint); 228 deleteFgColor = textColor.getDefaultColor(); 229 editBgColor = getContext().getColor( 230 com.android.internal.R.color.materialColorSurfaceDim); 231 deleteBgColor = getContext().getColor( 232 com.android.internal.R.color.materialColorSurfaceVariant); 233 } 234 235 mEditText.setTextColor(textColor); 236 mEditText.setHintTextColor(hintColor); 237 if (mEditText.getTextCursorDrawable() != null) { 238 mEditText.getTextCursorDrawable().setColorFilter( 239 accentColor.getDefaultColor(), PorterDuff.Mode.SRC_IN); 240 } 241 mContentBackground.setColor(editBgColor); 242 mContentBackground.setStroke(stroke, accentColor); 243 mDelete.setImageTintList(ColorStateList.valueOf(deleteFgColor)); 244 mDeleteBg.setImageTintList(ColorStateList.valueOf(deleteBgColor)); 245 mSendButton.setImageTintList(accentColor); 246 mProgressBar.setProgressTintList(accentColor); 247 mProgressBar.setIndeterminateTintList(accentColor); 248 mProgressBar.setSecondaryProgressTintList(accentColor); 249 if (!Flags.notificationRowTransparency()) { 250 setBackgroundColor(backgroundColor); 251 } 252 } 253 254 @Override onFinishInflate()255 protected void onFinishInflate() { 256 super.onFinishInflate(); 257 258 mProgressBar = findViewById(R.id.remote_input_progress); 259 mSendButton = findViewById(R.id.remote_input_send); 260 mSendButton.setOnClickListener(this); 261 mContentBackground = (GradientDrawable) 262 mContext.getDrawable(R.drawable.remote_input_view_text_bg).mutate(); 263 mDelete = findViewById(R.id.remote_input_delete); 264 mDeleteBg = findViewById(R.id.remote_input_delete_bg); 265 mDeleteBg.setImageTintBlendMode(BlendMode.SRC_IN); 266 mDelete.setImageTintBlendMode(BlendMode.SRC_IN); 267 mDelete.setOnClickListener(v -> setAttachment(null)); 268 mContentView = findViewById(R.id.remote_input_content); 269 mContentView.setBackground(mContentBackground); 270 mEditText = findViewById(R.id.remote_input_text); 271 mEditText.setInnerFocusable(false); 272 // TextView initializes the spell checked when the view is attached to a window. 273 // This causes a couple of IPCs that can jank, especially during animations. 274 // By default the text view should be disabled, to avoid the unnecessary initialization. 275 mEditText.setEnabled(false); 276 mEditText.setWindowInsetsAnimationCallback( 277 new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) { 278 @NonNull 279 @Override 280 public WindowInsets onProgress(@NonNull WindowInsets insets, 281 @NonNull List<WindowInsetsAnimation> runningAnimations) { 282 return insets; 283 } 284 @Override 285 public void onEnd(@NonNull WindowInsetsAnimation animation) { 286 super.onEnd(animation); 287 if (animation.getTypeMask() == WindowInsets.Type.ime()) { 288 mEntry.mRemoteEditImeAnimatingAway = false; 289 WindowInsets editTextRootWindowInsets = mEditText.getRootWindowInsets(); 290 if (editTextRootWindowInsets == null) { 291 Log.w(TAG, "onEnd called on detached view", new Exception()); 292 } 293 mEntry.mRemoteEditImeVisible = editTextRootWindowInsets != null 294 && editTextRootWindowInsets.isVisible(WindowInsets.Type.ime()); 295 if (!mEntry.mRemoteEditImeVisible && !mEditText.mShowImeOnInputConnection) { 296 mController.removeRemoteInput(mEntry, mToken, 297 /* reason= */"RemoteInputView$WindowInsetAnimation#onEnd"); 298 } 299 } 300 } 301 }); 302 } 303 304 /** 305 * @deprecated TODO(b/193539698): views shouldn't have access to their controller, and places 306 * that need the controller shouldn't have access to the view 307 */ 308 @Deprecated setController(RemoteInputViewController controller)309 public void setController(RemoteInputViewController controller) { 310 mViewController = controller; 311 } 312 313 /** 314 * @deprecated TODO(b/193539698): views shouldn't have access to their controller, and places 315 * that need the controller shouldn't have access to the view 316 */ 317 @Deprecated getController()318 public RemoteInputViewController getController() { 319 return mViewController; 320 } 321 322 /** Clear the attachment, if present. */ clearAttachment()323 public void clearAttachment() { 324 setAttachment(null); 325 } 326 327 @VisibleForTesting setAttachment(ContentInfo item)328 protected void setAttachment(ContentInfo item) { 329 if (mEntry.remoteInputAttachment != null && mEntry.remoteInputAttachment != item) { 330 // We need to release permissions when sending the attachment to the target 331 // app or if it is deleted by the user. When sending to the target app, we 332 // can safely release permissions as soon as the call to 333 // `mController.grantInlineReplyUriPermission` is made (ie, after the grant 334 // to the target app has been created). 335 mEntry.remoteInputAttachment.releasePermissions(); 336 } 337 mEntry.remoteInputAttachment = item; 338 if (item != null) { 339 mEntry.remoteInputUri = item.getClip().getItemAt(0).getUri(); 340 mEntry.remoteInputMimeType = item.getClip().getDescription().getMimeType(0); 341 } 342 343 View attachment = findViewById(R.id.remote_input_content_container); 344 ImageView iconView = findViewById(R.id.remote_input_attachment_image); 345 iconView.setImageDrawable(null); 346 if (item == null) { 347 attachment.setVisibility(GONE); 348 return; 349 } 350 iconView.setImageURI(item.getClip().getItemAt(0).getUri()); 351 if (iconView.getDrawable() == null) { 352 attachment.setVisibility(GONE); 353 } else { 354 attachment.setVisibility(VISIBLE); 355 mUiEventLogger.logWithInstanceId( 356 NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_ATTACH_IMAGE, 357 mEntry.getSbn().getUid(), mEntry.getSbn().getPackageName(), 358 mEntry.getSbn().getInstanceId()); 359 } 360 updateSendButton(); 361 } 362 363 /** Show the "sending in-progress" UI. */ startSending()364 public void startSending() { 365 mEditText.setEnabled(false); 366 mSending = true; 367 mSendButton.setVisibility(INVISIBLE); 368 mProgressBar.setVisibility(VISIBLE); 369 mEditText.mShowImeOnInputConnection = false; 370 } 371 sendRemoteInput()372 private void sendRemoteInput() { 373 for (Runnable listener : new ArrayList<>(mOnSendListeners)) { 374 listener.run(); 375 } 376 } 377 getText()378 public CharSequence getText() { 379 return mEditText.getText(); 380 } 381 inflate(Context context, ViewGroup root, NotificationEntry entry, RemoteInputController controller)382 public static RemoteInputView inflate(Context context, ViewGroup root, 383 NotificationEntry entry, 384 RemoteInputController controller) { 385 RemoteInputView v = (RemoteInputView) 386 LayoutInflater.from(context).inflate(R.layout.remote_input, root, false); 387 v.mController = controller; 388 v.mEntry = entry; 389 UserHandle user = computeTextOperationUser(entry.getSbn().getUser()); 390 v.mEditText.mUser = user; 391 v.mEditText.setTextOperationUser(user); 392 v.setTag(VIEW_TAG); 393 394 return v; 395 } 396 397 @Override onClick(View v)398 public void onClick(View v) { 399 if (v == mSendButton) { 400 sendRemoteInput(); 401 } 402 } 403 404 @Override onTouchEvent(MotionEvent event)405 public boolean onTouchEvent(MotionEvent event) { 406 super.onTouchEvent(event); 407 408 // We never want for a touch to escape to an outer view or one we covered. 409 return true; 410 } 411 isAnimatingAppearance()412 public boolean isAnimatingAppearance() { 413 return mIsAnimatingAppearance; 414 } 415 416 @VisibleForTesting onDefocus(boolean animate, boolean logClose, @Nullable Runnable doAfterDefocus)417 void onDefocus(boolean animate, boolean logClose, @Nullable Runnable doAfterDefocus) { 418 mController.removeRemoteInput(mEntry, mToken, /* reason= */"RemoteInputView#onDefocus"); 419 mEntry.remoteInputText = mEditText.getText(); 420 421 // During removal, we get reattached and lose focus. Not hiding in that 422 // case to prevent flicker. 423 if (!mRemoved) { 424 ViewGroup parent = (ViewGroup) getParent(); 425 View actionsContainer = getActionsContainerLayout(); 426 if (animate && parent != null) { 427 428 ViewGroup grandParent = (ViewGroup) parent.getParent(); 429 int actionsContainerHeight = 430 actionsContainer != null ? actionsContainer.getHeight() : 0; 431 432 // When defocusing, the notification needs to shrink. Therefore, we need to free 433 // up the space that was needed for the RemoteInputView. This is done by setting 434 // a negative top margin of the height difference of the RemoteInputView and its 435 // sibling (the actions_container_layout containing the Reply button etc.) 436 final int heightToShrink = actionsContainerHeight - getHeight(); 437 setTopMargin(heightToShrink); 438 if (grandParent != null) grandParent.setClipChildren(false); 439 440 final Animator animator = getDefocusAnimator(actionsContainer); 441 animator.addListener(new AnimatorListenerAdapter() { 442 @Override 443 public void onAnimationEnd(Animator animation) { 444 setTopMargin(0); 445 if (grandParent != null) grandParent.setClipChildren(true); 446 setVisibility(GONE); 447 setAlpha(1f); 448 if (mWrapper != null) { 449 mWrapper.setRemoteInputVisible(false); 450 } 451 if (doAfterDefocus != null) { 452 doAfterDefocus.run(); 453 } 454 } 455 }); 456 if (actionsContainer != null) actionsContainer.setAlpha(0f); 457 animator.start(); 458 459 } else { 460 setVisibility(GONE); 461 if (doAfterDefocus != null) doAfterDefocus.run(); 462 if (mWrapper != null) { 463 mWrapper.setRemoteInputVisible(false); 464 } 465 if (Flags.notificationRowTransparency()) { 466 if (actionsContainer != null) actionsContainer.setAlpha(1); 467 } 468 } 469 } 470 unregisterBackCallback(); 471 472 if (logClose) { 473 mUiEventLogger.logWithInstanceId( 474 NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_CLOSE, 475 mEntry.getSbn().getUid(), mEntry.getSbn().getPackageName(), 476 mEntry.getSbn().getInstanceId()); 477 } 478 } 479 setTopMargin(int topMargin)480 private void setTopMargin(int topMargin) { 481 if (!(getLayoutParams() instanceof FrameLayout.LayoutParams layoutParams)) return; 482 layoutParams.topMargin = topMargin; 483 setLayoutParams(layoutParams); 484 } 485 486 @VisibleForTesting setViewRootImpl(ViewRootImpl viewRoot)487 protected void setViewRootImpl(ViewRootImpl viewRoot) { 488 mTestableViewRootImpl = viewRoot; 489 } 490 491 @VisibleForTesting setEditTextReferenceToSelf()492 protected void setEditTextReferenceToSelf() { 493 mEditText.mRemoteInputView = this; 494 } 495 496 @Override onAttachedToWindow()497 protected void onAttachedToWindow() { 498 super.onAttachedToWindow(); 499 setEditTextReferenceToSelf(); 500 mEditText.setOnEditorActionListener(mEditorActionHandler); 501 mEditText.addTextChangedListener(mTextWatcher); 502 if (mEntry.getRow().isChangingPosition()) { 503 if (getVisibility() == VISIBLE && mEditText.isFocusable()) { 504 mEditText.requestFocus(); 505 } 506 } 507 } 508 509 @Override onDetachedFromWindow()510 protected void onDetachedFromWindow() { 511 super.onDetachedFromWindow(); 512 mEditText.removeTextChangedListener(mTextWatcher); 513 mEditText.setOnEditorActionListener(null); 514 mEditText.mRemoteInputView = null; 515 if (mEntry.getRow().isChangingPosition() || isTemporarilyDetached()) { 516 return; 517 } 518 // RemoteInputView can be detached from window before IME close event in some cases like 519 // remote input view removal with notification update. As a result of this, RemoteInputView 520 // will stop ime animation updates, which results in never removing remote input. That's why 521 // we have to set mRemoteEditImeAnimatingAway false on detach to remove remote input. 522 mEntry.mRemoteEditImeAnimatingAway = false; 523 mController.removeRemoteInput(mEntry, mToken, 524 /* reason= */"RemoteInputView#onDetachedFromWindow"); 525 mController.removeSpinning(mEntry.getKey(), mToken); 526 } 527 528 @Override getViewRootImpl()529 public ViewRootImpl getViewRootImpl() { 530 if (mTestableViewRootImpl != null) { 531 return mTestableViewRootImpl; 532 } 533 return super.getViewRootImpl(); 534 } 535 registerBackCallback()536 private void registerBackCallback() { 537 ViewRootImpl viewRoot = getViewRootImpl(); 538 if (viewRoot == null) { 539 if (DEBUG) { 540 Log.d(TAG, "ViewRoot was null, NOT registering Predictive Back callback"); 541 } 542 return; 543 } 544 if (DEBUG) { 545 Log.d(TAG, "registering Predictive Back callback"); 546 } 547 viewRoot.getOnBackInvokedDispatcher().registerOnBackInvokedCallback( 548 OnBackInvokedDispatcher.PRIORITY_OVERLAY, mEditText.mOnBackInvokedCallback); 549 } 550 unregisterBackCallback()551 private void unregisterBackCallback() { 552 ViewRootImpl viewRoot = getViewRootImpl(); 553 if (viewRoot == null) { 554 if (DEBUG) { 555 Log.d(TAG, "ViewRoot was null, NOT unregistering Predictive Back callback"); 556 } 557 return; 558 } 559 if (DEBUG) { 560 Log.d(TAG, "unregistering Predictive Back callback"); 561 } 562 viewRoot.getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback( 563 mEditText.mOnBackInvokedCallback); 564 } 565 566 @Override onVisibilityAggregated(boolean isVisible)567 public void onVisibilityAggregated(boolean isVisible) { 568 super.onVisibilityAggregated(isVisible); 569 mEditText.setEnabled(isVisible && !mSending); 570 } 571 setHintText(CharSequence hintText)572 public void setHintText(CharSequence hintText) { 573 mEditText.setHint(hintText); 574 } 575 setSupportedMimeTypes(Collection<String> mimeTypes)576 public void setSupportedMimeTypes(Collection<String> mimeTypes) { 577 mEditText.setSupportedMimeTypes(mimeTypes); 578 } 579 580 /** Populates the text field of the remote input with the given content. */ setEditTextContent(@ullable CharSequence editTextContent)581 public void setEditTextContent(@Nullable CharSequence editTextContent) { 582 mEditText.setText(editTextContent); 583 } 584 585 /** 586 * Focuses the RemoteInputView and animates its appearance 587 */ focusAnimated()588 public void focusAnimated() { 589 if (getVisibility() != VISIBLE) { 590 mIsAnimatingAppearance = true; 591 setAlpha(0f); 592 Animator focusAnimator = getFocusAnimator(getActionsContainerLayout()); 593 focusAnimator.addListener(new AnimatorListenerAdapter() { 594 @Override 595 public void onAnimationEnd(Animator animation, boolean isReverse) { 596 mIsAnimatingAppearance = false; 597 } 598 }); 599 focusAnimator.start(); 600 } 601 focus(); 602 } 603 computeTextOperationUser(UserHandle notificationUser)604 private static UserHandle computeTextOperationUser(UserHandle notificationUser) { 605 return UserHandle.ALL.equals(notificationUser) 606 ? UserHandle.of(ActivityManager.getCurrentUser()) : notificationUser; 607 } 608 focus()609 public void focus() { 610 mUiEventLogger.logWithInstanceId( 611 NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_OPEN, 612 mEntry.getSbn().getUid(), mEntry.getSbn().getPackageName(), 613 mEntry.getSbn().getInstanceId()); 614 615 setVisibility(VISIBLE); 616 if (mWrapper != null) { 617 mWrapper.setRemoteInputVisible(true); 618 } 619 mEditText.setInnerFocusable(true); 620 mEditText.mShowImeOnInputConnection = true; 621 mEditText.setText(mEntry.remoteInputText); 622 mEditText.setSelection(mEditText.length()); 623 mEditText.requestFocus(); 624 mController.addRemoteInput(mEntry, mToken, "RemoteInputView#focus"); 625 setAttachment(mEntry.remoteInputAttachment); 626 627 updateSendButton(); 628 registerBackCallback(); 629 } 630 onNotificationUpdateOrReset()631 public void onNotificationUpdateOrReset() { 632 boolean sending = mProgressBar.getVisibility() == VISIBLE; 633 634 if (sending) { 635 // Update came in after we sent the reply, time to reset. 636 reset(); 637 } 638 639 if (isActive() && mWrapper != null) { 640 mWrapper.setRemoteInputVisible(true); 641 } 642 } 643 reset()644 private void reset() { 645 mProgressBar.setVisibility(INVISIBLE); 646 mResetting = true; 647 mSending = false; 648 mController.removeSpinning(mEntry.getKey(), mToken); 649 onDefocus(true /* animate */, false /* logClose */, () -> { 650 mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); 651 mEditText.getText().clear(); 652 mEditText.setEnabled(isAggregatedVisible()); 653 mSendButton.setVisibility(VISIBLE); 654 updateSendButton(); 655 setAttachment(null); 656 mResetting = false; 657 }); 658 } 659 660 @Override onRequestSendAccessibilityEvent(View child, AccessibilityEvent event)661 public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { 662 if (mResetting && child == mEditText) { 663 // Suppress text events if it happens during resetting. Ideally this would be 664 // suppressed by the text view not being shown, but that doesn't work here because it 665 // needs to stay visible for the animation. 666 return false; 667 } 668 return super.onRequestSendAccessibilityEvent(child, event); 669 } 670 updateSendButton()671 private void updateSendButton() { 672 mSendButton.setEnabled(mEditText.length() != 0 || mEntry.remoteInputAttachment != null); 673 } 674 close()675 public void close() { 676 mEditText.defocusIfNeeded(false /* animated */); 677 } 678 679 @Override onInterceptTouchEvent(MotionEvent ev)680 public boolean onInterceptTouchEvent(MotionEvent ev) { 681 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 682 mController.requestDisallowLongPressAndDismiss(); 683 } 684 return super.onInterceptTouchEvent(ev); 685 } 686 requestScrollTo()687 public boolean requestScrollTo() { 688 mController.lockScrollTo(mEntry); 689 return true; 690 } 691 isActive()692 public boolean isActive() { 693 return mEditText.isFocused() && mEditText.isEnabled(); 694 } 695 setRemoved()696 public void setRemoved() { 697 mRemoved = true; 698 } 699 700 @Override dispatchStartTemporaryDetach()701 public void dispatchStartTemporaryDetach() { 702 super.dispatchStartTemporaryDetach(); 703 // Detach the EditText temporarily such that it doesn't get onDetachedFromWindow and 704 // won't lose IME focus. 705 final int iEditText = indexOfChild(mEditText); 706 if (iEditText != -1) { 707 detachViewFromParent(iEditText); 708 } 709 } 710 711 @Override dispatchFinishTemporaryDetach()712 public void dispatchFinishTemporaryDetach() { 713 if (isAttachedToWindow()) { 714 attachViewToParent(mEditText, 0, mEditText.getLayoutParams()); 715 } else { 716 removeDetachedView(mEditText, false /* animate */); 717 } 718 super.dispatchFinishTemporaryDetach(); 719 } 720 setWrapper(NotificationViewWrapper wrapper)721 public void setWrapper(NotificationViewWrapper wrapper) { 722 mWrapper = wrapper; 723 } 724 725 /** 726 * Register a listener to be notified when this view's visibility changes. 727 * 728 * Specifically, the passed {@link Consumer} will receive {@code true} when 729 * {@link #getVisibility()} would return {@link View#VISIBLE}, and {@code false} it would return 730 * any other value. 731 */ setOnVisibilityChangedListener(Consumer<Boolean> listener)732 public void setOnVisibilityChangedListener(Consumer<Boolean> listener) { 733 mOnVisibilityChangedListener = listener; 734 } 735 736 @Override onVisibilityChanged(View changedView, int visibility)737 protected void onVisibilityChanged(View changedView, int visibility) { 738 super.onVisibilityChanged(changedView, visibility); 739 if (changedView == this) { 740 final Consumer<Boolean> visibilityChangedListener = mOnVisibilityChangedListener; 741 if (visibilityChangedListener != null) { 742 visibilityChangedListener.accept(visibility == VISIBLE); 743 } 744 // Hide soft-keyboard when the input view became invisible 745 // (i.e. The notification shade collapsed by pressing the home key) 746 if (visibility != VISIBLE && !mController.isRemoteInputActive()) { 747 mEditText.hideIme(); 748 } 749 } 750 } 751 isSending()752 public boolean isSending() { 753 return getVisibility() == VISIBLE && mController.isSpinning(mEntry.getKey(), mToken); 754 } 755 756 /** Registers a listener for focus-change events on the EditText */ addOnEditTextFocusChangedListener(View.OnFocusChangeListener listener)757 public void addOnEditTextFocusChangedListener(View.OnFocusChangeListener listener) { 758 mEditTextFocusChangeListeners.add(listener); 759 } 760 761 /** Removes a previously-added listener for focus-change events on the EditText */ removeOnEditTextFocusChangedListener(View.OnFocusChangeListener listener)762 public void removeOnEditTextFocusChangedListener(View.OnFocusChangeListener listener) { 763 mEditTextFocusChangeListeners.remove(listener); 764 } 765 766 /** Determines if the EditText has focus. */ editTextHasFocus()767 public boolean editTextHasFocus() { 768 return mEditText != null && mEditText.hasFocus(); 769 } 770 onEditTextFocusChanged(RemoteEditText remoteEditText, boolean focused)771 private void onEditTextFocusChanged(RemoteEditText remoteEditText, boolean focused) { 772 for (View.OnFocusChangeListener listener : new ArrayList<>(mEditTextFocusChangeListeners)) { 773 listener.onFocusChange(remoteEditText, focused); 774 } 775 } 776 777 /** Registers a listener for send events on this RemoteInputView */ addOnSendRemoteInputListener(Runnable listener)778 public void addOnSendRemoteInputListener(Runnable listener) { 779 mOnSendListeners.add(listener); 780 } 781 782 /** Removes a previously-added listener for send events on this RemoteInputView */ removeOnSendRemoteInputListener(Runnable listener)783 public void removeOnSendRemoteInputListener(Runnable listener) { 784 mOnSendListeners.remove(listener); 785 } 786 787 @Override onLayout(boolean changed, int l, int t, int r, int b)788 protected void onLayout(boolean changed, int l, int t, int r, int b) { 789 super.onLayout(changed, l, t, r, b); 790 setPivotY(getMeasuredHeight()); 791 if (mContentBackgroundBounds != null) { 792 mContentBackground.setBounds(mContentBackgroundBounds); 793 } 794 } 795 796 /** 797 * @return action button container view (i.e. ViewGroup containing Reply button etc.) 798 */ getActionsContainerLayout()799 public View getActionsContainerLayout() { 800 ViewGroup parentView = (ViewGroup) getParent(); 801 if (parentView == null) return null; 802 return parentView.findViewById(com.android.internal.R.id.actions_container_layout); 803 } 804 805 /** 806 * Creates an animator for the focus animation. 807 * 808 * @param fadeOutView View that will be faded out during the focus animation. 809 */ getFocusAnimator(@ullable View fadeOutView)810 private Animator getFocusAnimator(@Nullable View fadeOutView) { 811 final AnimatorSet animatorSet = new AnimatorSet(); 812 813 final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f); 814 alphaAnimator.setStartDelay(FOCUS_ANIMATION_FADE_IN_DELAY); 815 alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); 816 alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); 817 818 ValueAnimator scaleAnimator = ValueAnimator.ofFloat(FOCUS_ANIMATION_MIN_SCALE, 1f); 819 scaleAnimator.addUpdateListener(valueAnimator -> { 820 setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue()); 821 }); 822 scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION); 823 scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN); 824 825 if (fadeOutView == null) { 826 animatorSet.playTogether(alphaAnimator, scaleAnimator); 827 } else { 828 final Animator fadeOutViewAlphaAnimator = 829 ObjectAnimator.ofFloat(fadeOutView, View.ALPHA, 1f, 0f); 830 fadeOutViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION); 831 fadeOutViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); 832 if (!Flags.notificationRowTransparency()) { 833 animatorSet.addListener(new AnimatorListenerAdapter() { 834 @Override 835 public void onAnimationEnd(Animator animation, boolean isReverse) { 836 fadeOutView.setAlpha(1f); 837 } 838 }); 839 } 840 animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeOutViewAlphaAnimator); 841 } 842 return animatorSet; 843 } 844 845 /** 846 * Creates an animator for the defocus animation. 847 * 848 * @param fadeInView View that will be faded in during the defocus animation. 849 */ getDefocusAnimator(@ullable View fadeInView)850 private Animator getDefocusAnimator(@Nullable View fadeInView) { 851 final AnimatorSet animatorSet = new AnimatorSet(); 852 853 final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f); 854 alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); 855 alphaAnimator.setStartDelay(DEFOCUS_ANIMATION_FADE_OUT_DELAY); 856 alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); 857 858 ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, FOCUS_ANIMATION_MIN_SCALE); 859 scaleAnimator.addUpdateListener(valueAnimator -> { 860 setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue()); 861 }); 862 scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION); 863 scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN); 864 scaleAnimator.addListener(new AnimatorListenerAdapter() { 865 @Override 866 public void onAnimationEnd(Animator animation, boolean isReverse) { 867 setFocusAnimationScaleY(1f /* scaleY */); 868 } 869 }); 870 871 if (fadeInView == null) { 872 animatorSet.playTogether(alphaAnimator, scaleAnimator); 873 } else { 874 fadeInView.forceHasOverlappingRendering(false); 875 Animator fadeInViewAlphaAnimator = 876 ObjectAnimator.ofFloat(fadeInView, View.ALPHA, 0f, 1f); 877 fadeInViewAlphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); 878 fadeInViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); 879 fadeInViewAlphaAnimator.setStartDelay(DEFOCUS_ANIMATION_CROSSFADE_DELAY); 880 animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeInViewAlphaAnimator); 881 } 882 return animatorSet; 883 } 884 885 /** 886 * Sets affected view properties for a vertical scale animation 887 * 888 * @param scaleY desired vertical view scale 889 */ setFocusAnimationScaleY(float scaleY)890 private void setFocusAnimationScaleY(float scaleY) { 891 int verticalBoundOffset = (int) ((1f - scaleY) * 0.5f * mContentView.getHeight()); 892 Rect contentBackgroundBounds = new Rect(0, verticalBoundOffset, mContentView.getWidth(), 893 mContentView.getHeight() - verticalBoundOffset); 894 mContentBackground.setBounds(contentBackgroundBounds); 895 mContentView.setBackground(mContentBackground); 896 if (scaleY == 1f) { 897 mContentBackgroundBounds = null; 898 } else { 899 mContentBackgroundBounds = contentBackgroundBounds; 900 } 901 setTranslationY(verticalBoundOffset); 902 } 903 904 /** Handler for button click on send action in IME. */ 905 private class EditorActionHandler implements TextView.OnEditorActionListener { 906 907 @Override onEditorAction(TextView v, int actionId, KeyEvent event)908 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 909 final boolean isSoftImeEvent = event == null 910 && (actionId == EditorInfo.IME_ACTION_DONE 911 || actionId == EditorInfo.IME_ACTION_NEXT 912 || actionId == EditorInfo.IME_ACTION_SEND); 913 final boolean isKeyboardEnterKey = event != null 914 && KeyEvent.isConfirmKey(event.getKeyCode()) 915 && event.getAction() == KeyEvent.ACTION_DOWN; 916 917 if (isSoftImeEvent || isKeyboardEnterKey) { 918 if (mEditText.length() > 0 || mEntry.remoteInputAttachment != null) { 919 sendRemoteInput(); 920 } 921 // Consume action to prevent IME from closing. 922 return true; 923 } 924 return false; 925 } 926 } 927 928 /** Observes text change events and updates the visibility of the send button accordingly. */ 929 private class SendButtonTextWatcher implements TextWatcher { 930 931 @Override beforeTextChanged(CharSequence s, int start, int count, int after)932 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 933 934 @Override onTextChanged(CharSequence s, int start, int before, int count)935 public void onTextChanged(CharSequence s, int start, int before, int count) {} 936 937 @Override afterTextChanged(Editable s)938 public void afterTextChanged(Editable s) { 939 updateSendButton(); 940 } 941 } 942 943 /** 944 * An EditText that changes appearance based on whether it's focusable and becomes 945 * un-focusable whenever the user navigates away from it or it becomes invisible. 946 */ 947 public static class RemoteEditText extends EditText { 948 949 private final OnReceiveContentListener mOnReceiveContentListener = this::onReceiveContent; 950 951 private RemoteInputView mRemoteInputView; 952 boolean mShowImeOnInputConnection; 953 private final LightBarController mLightBarController; 954 private InputMethodManager mInputMethodManager; 955 private final ArraySet<String> mSupportedMimes = new ArraySet<>(); 956 UserHandle mUser; 957 RemoteEditText(Context context, AttributeSet attrs)958 public RemoteEditText(Context context, AttributeSet attrs) { 959 super(context, attrs); 960 mLightBarController = Dependency.get(LightBarController.class); 961 } 962 setSupportedMimeTypes(@ullable Collection<String> mimeTypes)963 void setSupportedMimeTypes(@Nullable Collection<String> mimeTypes) { 964 String[] types = null; 965 OnReceiveContentListener listener = null; 966 if (mimeTypes != null && !mimeTypes.isEmpty()) { 967 types = mimeTypes.toArray(new String[0]); 968 listener = mOnReceiveContentListener; 969 } 970 setOnReceiveContentListener(types, listener); 971 mSupportedMimes.clear(); 972 mSupportedMimes.addAll(mimeTypes); 973 } 974 hideIme()975 private void hideIme() { 976 Trace.beginSection("RemoteEditText#hideIme"); 977 final WindowInsetsController insetsController = getWindowInsetsController(); 978 if (insetsController != null) { 979 insetsController.hide(WindowInsets.Type.ime()); 980 } 981 Trace.endSection(); 982 } 983 defocusIfNeeded(boolean animate)984 private void defocusIfNeeded(boolean animate) { 985 if (mRemoteInputView != null && mRemoteInputView.mEntry.getRow().isChangingPosition() 986 || isTemporarilyDetached()) { 987 if (isTemporarilyDetached()) { 988 // We might get reattached but then the other one of HUN / expanded might steal 989 // our focus, so we'll need to save our text here. 990 if (mRemoteInputView != null) { 991 mRemoteInputView.mEntry.remoteInputText = getText(); 992 } 993 } 994 return; 995 } 996 if (isFocusable() && isEnabled()) { 997 setInnerFocusable(false); 998 if (mRemoteInputView != null) { 999 mRemoteInputView 1000 .onDefocus(animate, true /* logClose */, null /* doAfterDefocus */); 1001 } 1002 mShowImeOnInputConnection = false; 1003 } 1004 } 1005 1006 @Override onVisibilityChanged(View changedView, int visibility)1007 protected void onVisibilityChanged(View changedView, int visibility) { 1008 super.onVisibilityChanged(changedView, visibility); 1009 1010 if (!isShown()) { 1011 defocusIfNeeded(false /* animate */); 1012 } 1013 } 1014 1015 @Override onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)1016 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 1017 super.onFocusChanged(focused, direction, previouslyFocusedRect); 1018 if (mRemoteInputView != null) { 1019 mRemoteInputView.onEditTextFocusChanged(this, focused); 1020 } 1021 if (!focused) { 1022 defocusIfNeeded(true /* animate */); 1023 } 1024 if (mRemoteInputView != null && !mRemoteInputView.mRemoved) { 1025 mLightBarController.setDirectReplying(focused); 1026 } 1027 } 1028 1029 @Override getFocusedRect(Rect r)1030 public void getFocusedRect(Rect r) { 1031 super.getFocusedRect(r); 1032 r.top = mScrollY; 1033 r.bottom = mScrollY + (mBottom - mTop); 1034 } 1035 1036 @Override requestRectangleOnScreen(Rect rectangle)1037 public boolean requestRectangleOnScreen(Rect rectangle) { 1038 return mRemoteInputView.requestScrollTo(); 1039 } 1040 1041 @Override onKeyDown(int keyCode, KeyEvent event)1042 public boolean onKeyDown(int keyCode, KeyEvent event) { 1043 if (keyCode == KeyEvent.KEYCODE_BACK) { 1044 // Eat the DOWN event here to prevent any default behavior. 1045 return true; 1046 } 1047 return super.onKeyDown(keyCode, event); 1048 } 1049 1050 private final OnBackInvokedCallback mOnBackInvokedCallback = () -> { 1051 if (DEBUG) { 1052 Log.d(TAG, "Predictive Back Callback dispatched"); 1053 } 1054 respondToKeycodeBack(); 1055 }; 1056 respondToKeycodeBack()1057 private void respondToKeycodeBack() { 1058 defocusIfNeeded(true /* animate */); 1059 } 1060 1061 @Override onKeyUp(int keyCode, KeyEvent event)1062 public boolean onKeyUp(int keyCode, KeyEvent event) { 1063 if (keyCode == KeyEvent.KEYCODE_BACK) { 1064 respondToKeycodeBack(); 1065 return true; 1066 } 1067 return super.onKeyUp(keyCode, event); 1068 } 1069 1070 @Override onKeyPreIme(int keyCode, KeyEvent event)1071 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 1072 // When BACK key is pressed, this method would be invoked twice. 1073 if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && 1074 event.getAction() == KeyEvent.ACTION_UP) { 1075 defocusIfNeeded(true /* animate */); 1076 } 1077 return super.onKeyPreIme(keyCode, event); 1078 } 1079 1080 @Override onCheckIsTextEditor()1081 public boolean onCheckIsTextEditor() { 1082 // Stop being editable while we're being removed. During removal, we get reattached, 1083 // and editable views get their spellchecking state re-evaluated which is too costly 1084 // during the removal animation. 1085 boolean flyingOut = mRemoteInputView != null && mRemoteInputView.mRemoved; 1086 return !flyingOut && super.onCheckIsTextEditor(); 1087 } 1088 1089 @Override onCreateInputConnection(EditorInfo outAttrs)1090 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 1091 final InputConnection ic = super.onCreateInputConnection(outAttrs); 1092 Context userContext = null; 1093 try { 1094 userContext = mContext.createPackageContextAsUser( 1095 mContext.getPackageName(), 0, mUser); 1096 } catch (PackageManager.NameNotFoundException e) { 1097 Log.e(TAG, "Unable to create user context:" + e.getMessage(), e); 1098 } 1099 1100 if (mShowImeOnInputConnection && ic != null) { 1101 Context targetContext = userContext != null ? userContext : getContext(); 1102 mInputMethodManager = targetContext.getSystemService(InputMethodManager.class); 1103 if (mInputMethodManager != null) { 1104 // onCreateInputConnection is called by InputMethodManager in the middle of 1105 // setting up the connection to the IME; wait with requesting the IME until that 1106 // work has completed. 1107 post(new Runnable() { 1108 @Override 1109 public void run() { 1110 mInputMethodManager.viewClicked(RemoteEditText.this); 1111 mInputMethodManager.showSoftInput(RemoteEditText.this, 0); 1112 } 1113 }); 1114 } 1115 } 1116 1117 return ic; 1118 } 1119 1120 @Override onCommitCompletion(CompletionInfo text)1121 public void onCommitCompletion(CompletionInfo text) { 1122 clearComposingText(); 1123 setText(text.getText()); 1124 setSelection(getText().length()); 1125 } 1126 setInnerFocusable(boolean focusable)1127 void setInnerFocusable(boolean focusable) { 1128 setFocusableInTouchMode(focusable); 1129 setFocusable(focusable); 1130 setCursorVisible(focusable); 1131 1132 if (focusable) { 1133 requestFocus(); 1134 } 1135 } 1136 onReceiveContent(View view, ContentInfo payload)1137 private ContentInfo onReceiveContent(View view, ContentInfo payload) { 1138 Pair<ContentInfo, ContentInfo> split = 1139 payload.partition(item -> item.getUri() != null); 1140 ContentInfo uriItems = split.first; 1141 ContentInfo remainingItems = split.second; 1142 if (uriItems != null) { 1143 mRemoteInputView.setAttachment(uriItems); 1144 } 1145 return remainingItems; 1146 } 1147 1148 } 1149 } 1150