• 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 android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.app.ActivityManager;
24 import android.app.Notification;
25 import android.app.PendingIntent;
26 import android.app.RemoteInput;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.pm.PackageManager;
30 import android.content.pm.ShortcutManager;
31 import android.content.res.ColorStateList;
32 import android.content.res.TypedArray;
33 import android.graphics.BlendMode;
34 import android.graphics.Color;
35 import android.graphics.PorterDuff;
36 import android.graphics.Rect;
37 import android.graphics.drawable.GradientDrawable;
38 import android.net.Uri;
39 import android.os.Bundle;
40 import android.os.SystemClock;
41 import android.os.UserHandle;
42 import android.text.Editable;
43 import android.text.SpannedString;
44 import android.text.TextUtils;
45 import android.text.TextWatcher;
46 import android.util.ArraySet;
47 import android.util.AttributeSet;
48 import android.util.Log;
49 import android.util.Pair;
50 import android.view.ContentInfo;
51 import android.view.KeyEvent;
52 import android.view.LayoutInflater;
53 import android.view.MotionEvent;
54 import android.view.OnReceiveContentListener;
55 import android.view.View;
56 import android.view.ViewAnimationUtils;
57 import android.view.ViewGroup;
58 import android.view.WindowInsets;
59 import android.view.WindowInsetsAnimation;
60 import android.view.accessibility.AccessibilityEvent;
61 import android.view.inputmethod.CompletionInfo;
62 import android.view.inputmethod.EditorInfo;
63 import android.view.inputmethod.InputConnection;
64 import android.view.inputmethod.InputMethodManager;
65 import android.widget.EditText;
66 import android.widget.ImageButton;
67 import android.widget.ImageView;
68 import android.widget.LinearLayout;
69 import android.widget.ProgressBar;
70 import android.widget.TextView;
71 
72 import androidx.annotation.NonNull;
73 import androidx.annotation.Nullable;
74 
75 import com.android.internal.graphics.ColorUtils;
76 import com.android.internal.logging.MetricsLogger;
77 import com.android.internal.logging.UiEvent;
78 import com.android.internal.logging.UiEventLogger;
79 import com.android.internal.logging.nano.MetricsProto;
80 import com.android.internal.util.ContrastColorUtil;
81 import com.android.systemui.Dependency;
82 import com.android.systemui.R;
83 import com.android.systemui.statusbar.NotificationRemoteInputManager;
84 import com.android.systemui.statusbar.RemoteInputController;
85 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
86 import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo;
87 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
88 import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
89 import com.android.systemui.statusbar.phone.LightBarController;
90 import com.android.wm.shell.animation.Interpolators;
91 
92 import java.util.ArrayList;
93 import java.util.Collection;
94 import java.util.HashMap;
95 import java.util.List;
96 import java.util.function.Consumer;
97 
98 /**
99  * Host for the remote input.
100  */
101 public class RemoteInputView extends LinearLayout implements View.OnClickListener {
102 
103     private static final String TAG = "RemoteInput";
104 
105     // A marker object that let's us easily find views of this class.
106     public static final Object VIEW_TAG = new Object();
107 
108     public final Object mToken = new Object();
109 
110     private final SendButtonTextWatcher mTextWatcher;
111     private final TextView.OnEditorActionListener mEditorActionHandler;
112     private final UiEventLogger mUiEventLogger;
113     private final RemoteInputQuickSettingsDisabler mRemoteInputQuickSettingsDisabler;
114     private final List<OnFocusChangeListener> mEditTextFocusChangeListeners = new ArrayList<>();
115     private final List<OnSendRemoteInputListener> mOnSendListeners = new ArrayList<>();
116     private RemoteEditText mEditText;
117     private ImageButton mSendButton;
118     private GradientDrawable mContentBackground;
119     private ProgressBar mProgressBar;
120     private PendingIntent mPendingIntent;
121     private RemoteInput[] mRemoteInputs;
122     private RemoteInput mRemoteInput;
123     private RemoteInputController mController;
124 
125     private NotificationEntry mEntry;
126 
127     private boolean mRemoved;
128 
129     private int mRevealCx;
130     private int mRevealCy;
131     private int mRevealR;
132 
133     private boolean mColorized;
134     private int mTint;
135 
136     private boolean mResetting;
137     private NotificationViewWrapper mWrapper;
138     private Consumer<Boolean> mOnVisibilityChangedListener;
139     private NotificationRemoteInputManager.BouncerChecker mBouncerChecker;
140     private ImageView mDelete;
141     private ImageView mDeleteBg;
142 
143     /**
144      * Enum for logged notification remote input UiEvents.
145      */
146     enum NotificationRemoteInputEvent implements UiEventLogger.UiEventEnum {
147         @UiEvent(doc = "Notification remote input view was displayed")
148         NOTIFICATION_REMOTE_INPUT_OPEN(795),
149         @UiEvent(doc = "Notification remote input view was closed")
150         NOTIFICATION_REMOTE_INPUT_CLOSE(796),
151         @UiEvent(doc = "User sent data through the notification remote input view")
152         NOTIFICATION_REMOTE_INPUT_SEND(797),
153         @UiEvent(doc = "Failed attempt to send data through the notification remote input view")
154         NOTIFICATION_REMOTE_INPUT_FAILURE(798);
155 
156         private final int mId;
NotificationRemoteInputEvent(int id)157         NotificationRemoteInputEvent(int id) {
158             mId = id;
159         }
getId()160         @Override public int getId() {
161             return mId;
162         }
163     }
164 
RemoteInputView(Context context, AttributeSet attrs)165     public RemoteInputView(Context context, AttributeSet attrs) {
166         super(context, attrs);
167         mTextWatcher = new SendButtonTextWatcher();
168         mEditorActionHandler = new EditorActionHandler();
169         mRemoteInputQuickSettingsDisabler = Dependency.get(RemoteInputQuickSettingsDisabler.class);
170         mUiEventLogger = Dependency.get(UiEventLogger.class);
171         TypedArray ta = getContext().getTheme().obtainStyledAttributes(new int[]{
172                 com.android.internal.R.attr.colorAccent,
173                 com.android.internal.R.attr.colorSurface,
174         });
175         mTint = ta.getColor(0, 0);
176         ta.recycle();
177     }
178 
colorStateListWithDisabledAlpha(int color, int disabledAlpha)179     private ColorStateList colorStateListWithDisabledAlpha(int color, int disabledAlpha) {
180         return new ColorStateList(new int[][]{
181                 new int[]{-com.android.internal.R.attr.state_enabled}, // disabled
182                 new int[]{},
183         }, new int[]{
184                 ColorUtils.setAlphaComponent(color, disabledAlpha),
185                 color
186         });
187     }
188 
189     /**
190      * The remote view needs to adapt to colorized notifications when set
191      * It overrides the background of itself as well as all of its childern
192      * @param backgroundColor colorized notification color
193      */
setBackgroundTintColor(final int backgroundColor, boolean colorized)194     public void setBackgroundTintColor(final int backgroundColor, boolean colorized) {
195         if (colorized == mColorized && backgroundColor == mTint) return;
196         mColorized = colorized;
197         mTint = backgroundColor;
198         final int editBgColor;
199         final int deleteBgColor;
200         final int deleteFgColor;
201         final ColorStateList accentColor;
202         final ColorStateList textColor;
203         final int hintColor;
204         final int stroke = colorized ? mContext.getResources().getDimensionPixelSize(
205                 R.dimen.remote_input_view_text_stroke) : 0;
206         if (colorized) {
207             final boolean dark = !ContrastColorUtil.isColorLight(backgroundColor);
208             final int foregroundColor = dark ? Color.WHITE : Color.BLACK;
209             final int inverseColor = dark ? Color.BLACK : Color.WHITE;
210             editBgColor = backgroundColor;
211             deleteBgColor = foregroundColor;
212             deleteFgColor = inverseColor;
213             accentColor = colorStateListWithDisabledAlpha(foregroundColor, 0x4D); // 30%
214             textColor = colorStateListWithDisabledAlpha(foregroundColor, 0x99); // 60%
215             hintColor = ColorUtils.setAlphaComponent(foregroundColor, 0x99);
216         } else {
217             accentColor = mContext.getColorStateList(R.color.remote_input_send);
218             textColor = mContext.getColorStateList(R.color.remote_input_text);
219             hintColor = mContext.getColor(R.color.remote_input_hint);
220             deleteFgColor = textColor.getDefaultColor();
221             try (TypedArray ta = getContext().getTheme().obtainStyledAttributes(new int[]{
222                     com.android.internal.R.attr.colorSurfaceHighlight,
223                     com.android.internal.R.attr.colorSurfaceVariant
224             })) {
225                 editBgColor = ta.getColor(0, backgroundColor);
226                 deleteBgColor = ta.getColor(1, Color.GRAY);
227             }
228         }
229 
230         mEditText.setTextColor(textColor);
231         mEditText.setHintTextColor(hintColor);
232         mEditText.getTextCursorDrawable().setColorFilter(
233                 accentColor.getDefaultColor(), PorterDuff.Mode.SRC_IN);
234         mContentBackground.setColor(editBgColor);
235         mContentBackground.setStroke(stroke, accentColor);
236         mDelete.setImageTintList(ColorStateList.valueOf(deleteFgColor));
237         mDeleteBg.setImageTintList(ColorStateList.valueOf(deleteBgColor));
238         mSendButton.setImageTintList(accentColor);
239         mProgressBar.setProgressTintList(accentColor);
240         mProgressBar.setIndeterminateTintList(accentColor);
241         mProgressBar.setSecondaryProgressTintList(accentColor);
242         setBackgroundColor(backgroundColor);
243     }
244 
245     @Override
onFinishInflate()246     protected void onFinishInflate() {
247         super.onFinishInflate();
248 
249         mProgressBar = findViewById(R.id.remote_input_progress);
250         mSendButton = findViewById(R.id.remote_input_send);
251         mSendButton.setOnClickListener(this);
252         mContentBackground = (GradientDrawable)
253                 mContext.getDrawable(R.drawable.remote_input_view_text_bg).mutate();
254         mDelete = findViewById(R.id.remote_input_delete);
255         mDeleteBg = findViewById(R.id.remote_input_delete_bg);
256         mDeleteBg.setImageTintBlendMode(BlendMode.SRC_IN);
257         mDelete.setImageTintBlendMode(BlendMode.SRC_IN);
258         mDelete.setOnClickListener(v -> setAttachment(null));
259         LinearLayout contentView = findViewById(R.id.remote_input_content);
260         contentView.setBackground(mContentBackground);
261         mEditText = findViewById(R.id.remote_input_text);
262         mEditText.setInnerFocusable(false);
263         mEditText.setWindowInsetsAnimationCallback(
264                 new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
265             @NonNull
266             @Override
267             public WindowInsets onProgress(@NonNull WindowInsets insets,
268                     @NonNull List<WindowInsetsAnimation> runningAnimations) {
269                 return insets;
270             }
271             @Override
272             public void onEnd(@NonNull WindowInsetsAnimation animation) {
273                 super.onEnd(animation);
274                 if (animation.getTypeMask() == WindowInsets.Type.ime()) {
275                     mEntry.mRemoteEditImeAnimatingAway = false;
276                     mEntry.mRemoteEditImeVisible =
277                             mEditText.getRootWindowInsets().isVisible(WindowInsets.Type.ime());
278                     if (!mEntry.mRemoteEditImeVisible && !mEditText.mShowImeOnInputConnection) {
279                         mController.removeRemoteInput(mEntry, mToken);
280                     }
281                 }
282             }
283         });
284     }
285 
setAttachment(ContentInfo item)286     private void setAttachment(ContentInfo item) {
287         if (mEntry.remoteInputAttachment != null && mEntry.remoteInputAttachment != item) {
288             // We need to release permissions when sending the attachment to the target
289             // app or if it is deleted by the user. When sending to the target app, we
290             // can safely release permissions as soon as the call to
291             // `mController.grantInlineReplyUriPermission` is made (ie, after the grant
292             // to the target app has been created).
293             mEntry.remoteInputAttachment.releasePermissions();
294         }
295         mEntry.remoteInputAttachment = item;
296         if (item != null) {
297             mEntry.remoteInputUri = item.getClip().getItemAt(0).getUri();
298             mEntry.remoteInputMimeType = item.getClip().getDescription().getMimeType(0);
299         }
300         View attachment = findViewById(R.id.remote_input_content_container);
301         ImageView iconView = findViewById(R.id.remote_input_attachment_image);
302         iconView.setImageDrawable(null);
303         if (item == null) {
304             attachment.setVisibility(GONE);
305             return;
306         }
307         iconView.setImageURI(item.getClip().getItemAt(0).getUri());
308         if (iconView.getDrawable() == null) {
309             attachment.setVisibility(GONE);
310         } else {
311             attachment.setVisibility(VISIBLE);
312         }
313         updateSendButton();
314     }
315 
316     /**
317      * Reply intent
318      * @return returns intent with granted URI permissions that should be used immediately
319      */
prepareRemoteInput()320     private Intent prepareRemoteInput() {
321         return mEntry.remoteInputAttachment == null
322                 ? prepareRemoteInputFromText()
323                 : prepareRemoteInputFromData(mEntry.remoteInputMimeType, mEntry.remoteInputUri);
324     }
325 
prepareRemoteInputFromText()326     private Intent prepareRemoteInputFromText() {
327         Bundle results = new Bundle();
328         results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString());
329         Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
330         RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent,
331                 results);
332 
333         mEntry.remoteInputText = mEditText.getText().toString();
334         setAttachment(null);
335         mEntry.remoteInputUri = null;
336         mEntry.remoteInputMimeType = null;
337 
338         if (mEntry.editedSuggestionInfo == null) {
339             RemoteInput.setResultsSource(fillInIntent, RemoteInput.SOURCE_FREE_FORM_INPUT);
340         } else {
341             RemoteInput.setResultsSource(fillInIntent, RemoteInput.SOURCE_CHOICE);
342         }
343 
344         return fillInIntent;
345     }
346 
prepareRemoteInputFromData(String contentType, Uri data)347     private Intent prepareRemoteInputFromData(String contentType, Uri data) {
348         HashMap<String, Uri> results = new HashMap<>();
349         results.put(contentType, data);
350         // grant for the target app.
351         mController.grantInlineReplyUriPermission(mEntry.getSbn(), data);
352         Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
353         RemoteInput.addDataResultToIntent(mRemoteInput, fillInIntent, results);
354 
355         Bundle bundle = new Bundle();
356         bundle.putString(mRemoteInput.getResultKey(), mEditText.getText().toString());
357         RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent,
358                 bundle);
359 
360         CharSequence attachmentText =
361                 mEntry.remoteInputAttachment.getClip().getDescription().getLabel();
362 
363         CharSequence attachmentLabel = TextUtils.isEmpty(attachmentText)
364                 ? mContext.getString(R.string.remote_input_image_insertion_text)
365                 : attachmentText;
366         // add content description to reply text for context
367         CharSequence fullText = TextUtils.isEmpty(mEditText.getText())
368                 ? attachmentLabel
369                 : "\"" + attachmentLabel + "\" " + mEditText.getText();
370 
371         mEntry.remoteInputText = fullText;
372 
373         // mirror prepareRemoteInputFromText for text input
374         if (mEntry.editedSuggestionInfo == null) {
375             RemoteInput.setResultsSource(fillInIntent, RemoteInput.SOURCE_FREE_FORM_INPUT);
376         } else if (mEntry.remoteInputAttachment == null) {
377             RemoteInput.setResultsSource(fillInIntent, RemoteInput.SOURCE_CHOICE);
378         }
379 
380         return fillInIntent;
381     }
382 
sendRemoteInput(Intent intent)383     private void sendRemoteInput(Intent intent) {
384         if (mBouncerChecker != null && mBouncerChecker.showBouncerIfNecessary()) {
385             mEditText.hideIme();
386             for (OnSendRemoteInputListener listener : mOnSendListeners) {
387                 listener.onSendRequestBounced();
388             }
389             return;
390         }
391 
392         mEditText.setEnabled(false);
393         mSendButton.setVisibility(INVISIBLE);
394         mProgressBar.setVisibility(VISIBLE);
395         mEntry.lastRemoteInputSent = SystemClock.elapsedRealtime();
396         mEntry.mRemoteEditImeAnimatingAway = true;
397         mController.addSpinning(mEntry.getKey(), mToken);
398         mController.removeRemoteInput(mEntry, mToken);
399         mEditText.mShowImeOnInputConnection = false;
400         mController.remoteInputSent(mEntry);
401         mEntry.setHasSentReply();
402 
403         for (OnSendRemoteInputListener listener : mOnSendListeners) {
404             listener.onSendRemoteInput();
405         }
406 
407         // Tell ShortcutManager that this package has been "activated".  ShortcutManager
408         // will reset the throttling for this package.
409         // Strictly speaking, the intent receiver may be different from the notification publisher,
410         // but that's an edge case, and also because we can't always know which package will receive
411         // an intent, so we just reset for the publisher.
412         getContext().getSystemService(ShortcutManager.class).onApplicationActive(
413                 mEntry.getSbn().getPackageName(),
414                 mEntry.getSbn().getUser().getIdentifier());
415 
416         MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_SEND,
417                 mEntry.getSbn().getPackageName());
418         mUiEventLogger.logWithInstanceId(
419                 NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_SEND,
420                 mEntry.getSbn().getUid(), mEntry.getSbn().getPackageName(),
421                 mEntry.getSbn().getInstanceId());
422         try {
423             mPendingIntent.send(mContext, 0, intent);
424         } catch (PendingIntent.CanceledException e) {
425             Log.i(TAG, "Unable to send remote input result", e);
426             MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_FAIL,
427                     mEntry.getSbn().getPackageName());
428             mUiEventLogger.logWithInstanceId(
429                     NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_FAILURE,
430                     mEntry.getSbn().getUid(), mEntry.getSbn().getPackageName(),
431                     mEntry.getSbn().getInstanceId());
432         }
433 
434         setAttachment(null);
435     }
436 
getText()437     public CharSequence getText() {
438         return mEditText.getText();
439     }
440 
inflate(Context context, ViewGroup root, NotificationEntry entry, RemoteInputController controller)441     public static RemoteInputView inflate(Context context, ViewGroup root,
442             NotificationEntry entry,
443             RemoteInputController controller) {
444         RemoteInputView v = (RemoteInputView)
445                 LayoutInflater.from(context).inflate(R.layout.remote_input, root, false);
446         v.mController = controller;
447         v.mEntry = entry;
448         UserHandle user = computeTextOperationUser(entry.getSbn().getUser());
449         v.mEditText.mUser = user;
450         v.mEditText.setTextOperationUser(user);
451         v.setTag(VIEW_TAG);
452 
453         return v;
454     }
455 
456     @Override
onClick(View v)457     public void onClick(View v) {
458         if (v == mSendButton) {
459             sendRemoteInput(prepareRemoteInput());
460         }
461     }
462 
463     @Override
onTouchEvent(MotionEvent event)464     public boolean onTouchEvent(MotionEvent event) {
465         super.onTouchEvent(event);
466 
467         // We never want for a touch to escape to an outer view or one we covered.
468         return true;
469     }
470 
onDefocus(boolean animate, boolean logClose)471     private void onDefocus(boolean animate, boolean logClose) {
472         mController.removeRemoteInput(mEntry, mToken);
473         mEntry.remoteInputText = mEditText.getText();
474 
475         // During removal, we get reattached and lose focus. Not hiding in that
476         // case to prevent flicker.
477         if (!mRemoved) {
478             if (animate && mRevealR > 0) {
479                 Animator reveal = ViewAnimationUtils.createCircularReveal(
480                         this, mRevealCx, mRevealCy, mRevealR, 0);
481                 reveal.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
482                 reveal.setDuration(StackStateAnimator.ANIMATION_DURATION_CLOSE_REMOTE_INPUT);
483                 reveal.addListener(new AnimatorListenerAdapter() {
484                     @Override
485                     public void onAnimationEnd(Animator animation) {
486                         setVisibility(GONE);
487                         if (mWrapper != null) {
488                             mWrapper.setRemoteInputVisible(false);
489                         }
490                     }
491                 });
492                 reveal.start();
493             } else {
494                 setVisibility(GONE);
495                 if (mWrapper != null) {
496                     mWrapper.setRemoteInputVisible(false);
497                 }
498             }
499         }
500 
501         mRemoteInputQuickSettingsDisabler.setRemoteInputActive(false);
502 
503         if (logClose) {
504             MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_CLOSE,
505                     mEntry.getSbn().getPackageName());
506             mUiEventLogger.logWithInstanceId(
507                     NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_CLOSE,
508                     mEntry.getSbn().getUid(), mEntry.getSbn().getPackageName(),
509                     mEntry.getSbn().getInstanceId());
510         }
511     }
512 
513     @Override
onAttachedToWindow()514     protected void onAttachedToWindow() {
515         super.onAttachedToWindow();
516         mEditText.mRemoteInputView = this;
517         mEditText.setOnEditorActionListener(mEditorActionHandler);
518         mEditText.addTextChangedListener(mTextWatcher);
519         if (mEntry.getRow().isChangingPosition()) {
520             if (getVisibility() == VISIBLE && mEditText.isFocusable()) {
521                 mEditText.requestFocus();
522             }
523         }
524     }
525 
526     @Override
onDetachedFromWindow()527     protected void onDetachedFromWindow() {
528         super.onDetachedFromWindow();
529         mEditText.removeTextChangedListener(mTextWatcher);
530         mEditText.setOnEditorActionListener(null);
531         mEditText.mRemoteInputView = null;
532         if (mEntry.getRow().isChangingPosition() || isTemporarilyDetached()) {
533             return;
534         }
535         mController.removeRemoteInput(mEntry, mToken);
536         mController.removeSpinning(mEntry.getKey(), mToken);
537     }
538 
setPendingIntent(PendingIntent pendingIntent)539     public void setPendingIntent(PendingIntent pendingIntent) {
540         mPendingIntent = pendingIntent;
541     }
542 
543     /**
544      * Sets the remote input for this view.
545      *
546      * @param remoteInputs The remote inputs that need to be sent to the app.
547      * @param remoteInput The remote input that needs to be activated.
548      * @param editedSuggestionInfo The smart reply that should be inserted in the remote input, or
549      *         {@code null} if the user is not editing a smart reply.
550      */
setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput, @Nullable EditedSuggestionInfo editedSuggestionInfo)551     public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput,
552             @Nullable EditedSuggestionInfo editedSuggestionInfo) {
553         mRemoteInputs = remoteInputs;
554         mRemoteInput = remoteInput;
555         mEditText.setHint(mRemoteInput.getLabel());
556         mEditText.setSupportedMimeTypes(remoteInput.getAllowedDataTypes());
557 
558         mEntry.editedSuggestionInfo = editedSuggestionInfo;
559         if (editedSuggestionInfo != null) {
560             mEntry.remoteInputText = editedSuggestionInfo.originalText;
561             mEntry.remoteInputAttachment = null;
562         }
563     }
564 
565     /** Populates the text field of the remote input with the given content. */
setEditTextContent(@ullable CharSequence editTextContent)566     public void setEditTextContent(@Nullable CharSequence editTextContent) {
567         mEditText.setText(editTextContent);
568     }
569 
focusAnimated()570     public void focusAnimated() {
571         if (getVisibility() != VISIBLE) {
572             Animator animator = ViewAnimationUtils.createCircularReveal(
573                     this, mRevealCx, mRevealCy, 0, mRevealR);
574             animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
575             animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
576             animator.start();
577         }
578         focus();
579     }
580 
computeTextOperationUser(UserHandle notificationUser)581     private static UserHandle computeTextOperationUser(UserHandle notificationUser) {
582         return UserHandle.ALL.equals(notificationUser)
583                 ? UserHandle.of(ActivityManager.getCurrentUser()) : notificationUser;
584     }
585 
focus()586     public void focus() {
587         MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_OPEN,
588                 mEntry.getSbn().getPackageName());
589         mUiEventLogger.logWithInstanceId(
590                 NotificationRemoteInputEvent.NOTIFICATION_REMOTE_INPUT_OPEN,
591                 mEntry.getSbn().getUid(), mEntry.getSbn().getPackageName(),
592                 mEntry.getSbn().getInstanceId());
593 
594         setVisibility(VISIBLE);
595         if (mWrapper != null) {
596             mWrapper.setRemoteInputVisible(true);
597         }
598         mEditText.setInnerFocusable(true);
599         mEditText.mShowImeOnInputConnection = true;
600         mEditText.setText(mEntry.remoteInputText);
601         mEditText.setSelection(mEditText.length());
602         mEditText.requestFocus();
603         mController.addRemoteInput(mEntry, mToken);
604         setAttachment(mEntry.remoteInputAttachment);
605 
606         mRemoteInputQuickSettingsDisabler.setRemoteInputActive(true);
607 
608         updateSendButton();
609     }
610 
onNotificationUpdateOrReset()611     public void onNotificationUpdateOrReset() {
612         boolean sending = mProgressBar.getVisibility() == VISIBLE;
613 
614         if (sending) {
615             // Update came in after we sent the reply, time to reset.
616             reset();
617         }
618 
619         if (isActive() && mWrapper != null) {
620             mWrapper.setRemoteInputVisible(true);
621         }
622     }
623 
reset()624     private void reset() {
625         mResetting = true;
626         mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText());
627 
628         mEditText.getText().clear();
629         mEditText.setEnabled(true);
630         mSendButton.setVisibility(VISIBLE);
631         mProgressBar.setVisibility(INVISIBLE);
632         mController.removeSpinning(mEntry.getKey(), mToken);
633         updateSendButton();
634         onDefocus(false /* animate */, false /* logClose */);
635         setAttachment(null);
636 
637         mResetting = false;
638     }
639 
640     @Override
onRequestSendAccessibilityEvent(View child, AccessibilityEvent event)641     public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
642         if (mResetting && child == mEditText) {
643             // Suppress text events if it happens during resetting. Ideally this would be
644             // suppressed by the text view not being shown, but that doesn't work here because it
645             // needs to stay visible for the animation.
646             return false;
647         }
648         return super.onRequestSendAccessibilityEvent(child, event);
649     }
650 
updateSendButton()651     private void updateSendButton() {
652         mSendButton.setEnabled(mEditText.length() != 0 || mEntry.remoteInputAttachment != null);
653     }
654 
close()655     public void close() {
656         mEditText.defocusIfNeeded(false /* animated */);
657     }
658 
659     @Override
onInterceptTouchEvent(MotionEvent ev)660     public boolean onInterceptTouchEvent(MotionEvent ev) {
661         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
662             mController.requestDisallowLongPressAndDismiss();
663         }
664         return super.onInterceptTouchEvent(ev);
665     }
666 
requestScrollTo()667     public boolean requestScrollTo() {
668         mController.lockScrollTo(mEntry);
669         return true;
670     }
671 
isActive()672     public boolean isActive() {
673         return mEditText.isFocused() && mEditText.isEnabled();
674     }
675 
stealFocusFrom(RemoteInputView other)676     public void stealFocusFrom(RemoteInputView other) {
677         other.close();
678         setPendingIntent(other.mPendingIntent);
679         setRemoteInput(other.mRemoteInputs, other.mRemoteInput, mEntry.editedSuggestionInfo);
680         setRevealParameters(other.mRevealCx, other.mRevealCy, other.mRevealR);
681         focus();
682     }
683 
684     /**
685      * Tries to find an action in {@param actions} that matches the current pending intent
686      * of this view and updates its state to that of the found action
687      *
688      * @return true if a matching action was found, false otherwise
689      */
updatePendingIntentFromActions(Notification.Action[] actions)690     public boolean updatePendingIntentFromActions(Notification.Action[] actions) {
691         if (mPendingIntent == null || actions == null) {
692             return false;
693         }
694         Intent current = mPendingIntent.getIntent();
695         if (current == null) {
696             return false;
697         }
698 
699         for (Notification.Action a : actions) {
700             RemoteInput[] inputs = a.getRemoteInputs();
701             if (a.actionIntent == null || inputs == null) {
702                 continue;
703             }
704             Intent candidate = a.actionIntent.getIntent();
705             if (!current.filterEquals(candidate)) {
706                 continue;
707             }
708 
709             RemoteInput input = null;
710             for (RemoteInput i : inputs) {
711                 if (i.getAllowFreeFormInput()) {
712                     input = i;
713                 }
714             }
715             if (input == null) {
716                 continue;
717             }
718             setPendingIntent(a.actionIntent);
719             setRemoteInput(inputs, input, null /* editedSuggestionInfo*/);
720             return true;
721         }
722         return false;
723     }
724 
getPendingIntent()725     public PendingIntent getPendingIntent() {
726         return mPendingIntent;
727     }
728 
setRemoved()729     public void setRemoved() {
730         mRemoved = true;
731     }
732 
setRevealParameters(int cx, int cy, int r)733     public void setRevealParameters(int cx, int cy, int r) {
734         mRevealCx = cx;
735         mRevealCy = cy;
736         mRevealR = r;
737     }
738 
739     @Override
dispatchStartTemporaryDetach()740     public void dispatchStartTemporaryDetach() {
741         super.dispatchStartTemporaryDetach();
742         // Detach the EditText temporarily such that it doesn't get onDetachedFromWindow and
743         // won't lose IME focus.
744         final int iEditText = indexOfChild(mEditText);
745         if (iEditText != -1) {
746             detachViewFromParent(iEditText);
747         }
748     }
749 
750     @Override
dispatchFinishTemporaryDetach()751     public void dispatchFinishTemporaryDetach() {
752         if (isAttachedToWindow()) {
753             attachViewToParent(mEditText, 0, mEditText.getLayoutParams());
754         } else {
755             removeDetachedView(mEditText, false /* animate */);
756         }
757         super.dispatchFinishTemporaryDetach();
758     }
759 
setWrapper(NotificationViewWrapper wrapper)760     public void setWrapper(NotificationViewWrapper wrapper) {
761         mWrapper = wrapper;
762     }
763 
setOnVisibilityChangedListener(Consumer<Boolean> visibilityChangedListener)764     public void setOnVisibilityChangedListener(Consumer<Boolean> visibilityChangedListener) {
765         mOnVisibilityChangedListener = visibilityChangedListener;
766     }
767 
768     @Override
onVisibilityChanged(View changedView, int visibility)769     protected void onVisibilityChanged(View changedView, int visibility) {
770         super.onVisibilityChanged(changedView, visibility);
771         if (changedView == this && mOnVisibilityChangedListener != null) {
772             mOnVisibilityChangedListener.accept(visibility == VISIBLE);
773             // Hide soft-keyboard when the input view became invisible
774             // (i.e. The notification shade collapsed by pressing the home key)
775             if (visibility != VISIBLE && !mEditText.isVisibleToUser()
776                     && !mController.isRemoteInputActive()) {
777                 mEditText.hideIme();
778             }
779         }
780     }
781 
isSending()782     public boolean isSending() {
783         return getVisibility() == VISIBLE && mController.isSpinning(mEntry.getKey(), mToken);
784     }
785 
786     /**
787      * Sets a {@link com.android.systemui.statusbar.NotificationRemoteInputManager.BouncerChecker}
788      * that will be used to determine if the device needs to be unlocked before sending the
789      * RemoteInput.
790      */
setBouncerChecker( @ullable NotificationRemoteInputManager.BouncerChecker bouncerChecker)791     public void setBouncerChecker(
792             @Nullable NotificationRemoteInputManager.BouncerChecker bouncerChecker) {
793         mBouncerChecker = bouncerChecker;
794     }
795 
796     /** Registers a listener for focus-change events on the EditText */
addOnEditTextFocusChangedListener(View.OnFocusChangeListener listener)797     public void addOnEditTextFocusChangedListener(View.OnFocusChangeListener listener) {
798         mEditTextFocusChangeListeners.add(listener);
799     }
800 
801     /** Removes a previously-added listener for focus-change events on the EditText */
removeOnEditTextFocusChangedListener(View.OnFocusChangeListener listener)802     public void removeOnEditTextFocusChangedListener(View.OnFocusChangeListener listener) {
803         mEditTextFocusChangeListeners.remove(listener);
804     }
805 
806     /** Determines if the EditText has focus. */
editTextHasFocus()807     public boolean editTextHasFocus() {
808         return mEditText != null && mEditText.hasFocus();
809     }
810 
onEditTextFocusChanged(RemoteEditText remoteEditText, boolean focused)811     private void onEditTextFocusChanged(RemoteEditText remoteEditText, boolean focused) {
812         for (View.OnFocusChangeListener listener : new ArrayList<>(mEditTextFocusChangeListeners)) {
813             listener.onFocusChange(remoteEditText, focused);
814         }
815     }
816 
817     /** Registers a listener for send events on this RemoteInputView */
addOnSendRemoteInputListener(OnSendRemoteInputListener listener)818     public void addOnSendRemoteInputListener(OnSendRemoteInputListener listener) {
819         mOnSendListeners.add(listener);
820     }
821 
822     /** Removes a previously-added listener for send events on this RemoteInputView */
removeOnSendRemoteInputListener(OnSendRemoteInputListener listener)823     public void removeOnSendRemoteInputListener(OnSendRemoteInputListener listener) {
824         mOnSendListeners.remove(listener);
825     }
826 
827     /** Listener for send events */
828     public interface OnSendRemoteInputListener {
829         /** Invoked when the remote input has been sent successfully. */
onSendRemoteInput()830         void onSendRemoteInput();
831         /**
832          * Invoked when the user had requested to send the remote input, but authentication was
833          * required and the bouncer was shown instead.
834          */
onSendRequestBounced()835         void onSendRequestBounced();
836     }
837 
838     /** Handler for button click on send action in IME. */
839     private class EditorActionHandler implements TextView.OnEditorActionListener {
840 
841         @Override
onEditorAction(TextView v, int actionId, KeyEvent event)842         public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
843             final boolean isSoftImeEvent = event == null
844                     && (actionId == EditorInfo.IME_ACTION_DONE
845                     || actionId == EditorInfo.IME_ACTION_NEXT
846                     || actionId == EditorInfo.IME_ACTION_SEND);
847             final boolean isKeyboardEnterKey = event != null
848                     && KeyEvent.isConfirmKey(event.getKeyCode())
849                     && event.getAction() == KeyEvent.ACTION_DOWN;
850 
851             if (isSoftImeEvent || isKeyboardEnterKey) {
852                 if (mEditText.length() > 0 || mEntry.remoteInputAttachment != null) {
853                     sendRemoteInput(prepareRemoteInput());
854                 }
855                 // Consume action to prevent IME from closing.
856                 return true;
857             }
858             return false;
859         }
860     }
861 
862     /** Observes text change events and updates the visibility of the send button accordingly. */
863     private class SendButtonTextWatcher implements TextWatcher {
864 
865         @Override
beforeTextChanged(CharSequence s, int start, int count, int after)866         public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
867 
868         @Override
onTextChanged(CharSequence s, int start, int before, int count)869         public void onTextChanged(CharSequence s, int start, int before, int count) {}
870 
871         @Override
afterTextChanged(Editable s)872         public void afterTextChanged(Editable s) {
873             updateSendButton();
874         }
875     }
876 
877     /**
878      * An EditText that changes appearance based on whether it's focusable and becomes
879      * un-focusable whenever the user navigates away from it or it becomes invisible.
880      */
881     public static class RemoteEditText extends EditText {
882 
883         private final OnReceiveContentListener mOnReceiveContentListener = this::onReceiveContent;
884 
885         private RemoteInputView mRemoteInputView;
886         boolean mShowImeOnInputConnection;
887         private LightBarController mLightBarController;
888         private InputMethodManager mInputMethodManager;
889         private ArraySet<String> mSupportedMimes = new ArraySet<>();
890         UserHandle mUser;
891 
RemoteEditText(Context context, AttributeSet attrs)892         public RemoteEditText(Context context, AttributeSet attrs) {
893             super(context, attrs);
894             mLightBarController = Dependency.get(LightBarController.class);
895         }
896 
setSupportedMimeTypes(@ullable Collection<String> mimeTypes)897         void setSupportedMimeTypes(@Nullable Collection<String> mimeTypes) {
898             String[] types = null;
899             OnReceiveContentListener listener = null;
900             if (mimeTypes != null && !mimeTypes.isEmpty()) {
901                 types = mimeTypes.toArray(new String[0]);
902                 listener = mOnReceiveContentListener;
903             }
904             setOnReceiveContentListener(types, listener);
905             mSupportedMimes.clear();
906             mSupportedMimes.addAll(mimeTypes);
907         }
908 
hideIme()909         private void hideIme() {
910             if (mInputMethodManager != null) {
911                 mInputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
912             }
913         }
914 
defocusIfNeeded(boolean animate)915         private void defocusIfNeeded(boolean animate) {
916             if (mRemoteInputView != null && mRemoteInputView.mEntry.getRow().isChangingPosition()
917                     || isTemporarilyDetached()) {
918                 if (isTemporarilyDetached()) {
919                     // We might get reattached but then the other one of HUN / expanded might steal
920                     // our focus, so we'll need to save our text here.
921                     if (mRemoteInputView != null) {
922                         mRemoteInputView.mEntry.remoteInputText = getText();
923                     }
924                 }
925                 return;
926             }
927             if (isFocusable() && isEnabled()) {
928                 setInnerFocusable(false);
929                 if (mRemoteInputView != null) {
930                     mRemoteInputView.onDefocus(animate, true /* logClose */);
931                 }
932                 mShowImeOnInputConnection = false;
933             }
934         }
935 
936         @Override
onVisibilityChanged(View changedView, int visibility)937         protected void onVisibilityChanged(View changedView, int visibility) {
938             super.onVisibilityChanged(changedView, visibility);
939 
940             if (!isShown()) {
941                 defocusIfNeeded(false /* animate */);
942             }
943         }
944 
945         @Override
onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)946         protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
947             super.onFocusChanged(focused, direction, previouslyFocusedRect);
948             if (mRemoteInputView != null) {
949                 mRemoteInputView.onEditTextFocusChanged(this, focused);
950             }
951             if (!focused) {
952                 defocusIfNeeded(true /* animate */);
953             }
954             if (mRemoteInputView != null && !mRemoteInputView.mRemoved) {
955                 mLightBarController.setDirectReplying(focused);
956             }
957         }
958 
959         @Override
getFocusedRect(Rect r)960         public void getFocusedRect(Rect r) {
961             super.getFocusedRect(r);
962             r.top = mScrollY;
963             r.bottom = mScrollY + (mBottom - mTop);
964         }
965 
966         @Override
requestRectangleOnScreen(Rect rectangle)967         public boolean requestRectangleOnScreen(Rect rectangle) {
968             return mRemoteInputView.requestScrollTo();
969         }
970 
971         @Override
onKeyDown(int keyCode, KeyEvent event)972         public boolean onKeyDown(int keyCode, KeyEvent event) {
973             if (keyCode == KeyEvent.KEYCODE_BACK) {
974                 // Eat the DOWN event here to prevent any default behavior.
975                 return true;
976             }
977             return super.onKeyDown(keyCode, event);
978         }
979 
980         @Override
onKeyUp(int keyCode, KeyEvent event)981         public boolean onKeyUp(int keyCode, KeyEvent event) {
982             if (keyCode == KeyEvent.KEYCODE_BACK) {
983                 defocusIfNeeded(true /* animate */);
984                 return true;
985             }
986             return super.onKeyUp(keyCode, event);
987         }
988 
989         @Override
onKeyPreIme(int keyCode, KeyEvent event)990         public boolean onKeyPreIme(int keyCode, KeyEvent event) {
991             // When BACK key is pressed, this method would be invoked twice.
992             if (event.getKeyCode() == KeyEvent.KEYCODE_BACK &&
993                     event.getAction() == KeyEvent.ACTION_UP) {
994                 defocusIfNeeded(true /* animate */);
995             }
996             return super.onKeyPreIme(keyCode, event);
997         }
998 
999         @Override
onCheckIsTextEditor()1000         public boolean onCheckIsTextEditor() {
1001             // Stop being editable while we're being removed. During removal, we get reattached,
1002             // and editable views get their spellchecking state re-evaluated which is too costly
1003             // during the removal animation.
1004             boolean flyingOut = mRemoteInputView != null && mRemoteInputView.mRemoved;
1005             return !flyingOut && super.onCheckIsTextEditor();
1006         }
1007 
1008         @Override
onCreateInputConnection(EditorInfo outAttrs)1009         public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
1010             final InputConnection ic = super.onCreateInputConnection(outAttrs);
1011             Context userContext = null;
1012             try {
1013                 userContext = mContext.createPackageContextAsUser(
1014                         mContext.getPackageName(), 0, mUser);
1015             } catch (PackageManager.NameNotFoundException e) {
1016                 Log.e(TAG, "Unable to create user context:" + e.getMessage(), e);
1017             }
1018 
1019             if (mShowImeOnInputConnection && ic != null) {
1020                 Context targetContext = userContext != null ? userContext : getContext();
1021                 mInputMethodManager = targetContext.getSystemService(InputMethodManager.class);
1022                 if (mInputMethodManager != null) {
1023                     // onCreateInputConnection is called by InputMethodManager in the middle of
1024                     // setting up the connection to the IME; wait with requesting the IME until that
1025                     // work has completed.
1026                     post(new Runnable() {
1027                         @Override
1028                         public void run() {
1029                             mInputMethodManager.viewClicked(RemoteEditText.this);
1030                             mInputMethodManager.showSoftInput(RemoteEditText.this, 0);
1031                         }
1032                     });
1033                 }
1034             }
1035 
1036             return ic;
1037         }
1038 
1039         @Override
onCommitCompletion(CompletionInfo text)1040         public void onCommitCompletion(CompletionInfo text) {
1041             clearComposingText();
1042             setText(text.getText());
1043             setSelection(getText().length());
1044         }
1045 
setInnerFocusable(boolean focusable)1046         void setInnerFocusable(boolean focusable) {
1047             setFocusableInTouchMode(focusable);
1048             setFocusable(focusable);
1049             setCursorVisible(focusable);
1050 
1051             if (focusable) {
1052                 requestFocus();
1053             }
1054         }
1055 
onReceiveContent(View view, ContentInfo payload)1056         private ContentInfo onReceiveContent(View view, ContentInfo payload) {
1057             Pair<ContentInfo, ContentInfo> split =
1058                     payload.partition(item -> item.getUri() != null);
1059             ContentInfo uriItems = split.first;
1060             ContentInfo remainingItems = split.second;
1061             if (uriItems != null) {
1062                 mRemoteInputView.setAttachment(uriItems);
1063             }
1064             return remainingItems;
1065         }
1066 
1067     }
1068 }
1069