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