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