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