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