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