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