1 /*
2  * Copyright (C) 2017 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 package com.android.systemui.statusbar;
17 
18 import android.app.ActivityManager;
19 import android.app.ActivityOptions;
20 import android.app.KeyguardManager;
21 import android.app.Notification;
22 import android.app.PendingIntent;
23 import android.app.RemoteInput;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.pm.UserInfo;
27 import android.os.PowerManager;
28 import android.os.RemoteException;
29 import android.os.ServiceManager;
30 import android.os.SystemProperties;
31 import android.os.UserManager;
32 import android.service.notification.StatusBarNotification;
33 import android.text.TextUtils;
34 import android.util.IndentingPrintWriter;
35 import android.util.Log;
36 import android.util.Pair;
37 import android.view.MotionEvent;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.view.ViewParent;
41 import android.widget.RemoteViews;
42 import android.widget.RemoteViews.InteractionHandler;
43 import android.widget.TextView;
44 
45 import androidx.annotation.NonNull;
46 import androidx.annotation.Nullable;
47 
48 import com.android.internal.statusbar.IStatusBarService;
49 import com.android.internal.statusbar.NotificationVisibility;
50 import com.android.systemui.Dumpable;
51 import com.android.systemui.R;
52 import com.android.systemui.dump.DumpManager;
53 import com.android.systemui.plugins.statusbar.StatusBarStateController;
54 import com.android.systemui.power.domain.interactor.PowerInteractor;
55 import com.android.systemui.statusbar.dagger.CentralSurfacesDependenciesModule;
56 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
57 import com.android.systemui.statusbar.notification.RemoteInputControllerLogger;
58 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
59 import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo;
60 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
61 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
62 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
63 import com.android.systemui.statusbar.policy.RemoteInputUriController;
64 import com.android.systemui.statusbar.policy.RemoteInputView;
65 import com.android.systemui.util.DumpUtilsKt;
66 import com.android.systemui.util.ListenerSet;
67 
68 import java.io.PrintWriter;
69 import java.util.ArrayList;
70 import java.util.List;
71 import java.util.Objects;
72 import java.util.function.Consumer;
73 
74 /**
75  * Class for handling remote input state over a set of notifications. This class handles things
76  * like keeping notifications temporarily that were cancelled as a response to a remote input
77  * interaction, keeping track of notifications to remove when NotificationPresenter is collapsed,
78  * and handling clicks on remote views.
79  */
80 public class NotificationRemoteInputManager implements Dumpable {
81     public static final boolean ENABLE_REMOTE_INPUT =
82             SystemProperties.getBoolean("debug.enable_remote_input", true);
83     public static boolean FORCE_REMOTE_INPUT_HISTORY =
84             SystemProperties.getBoolean("debug.force_remoteinput_history", true);
85     private static final boolean DEBUG = false;
86     private static final String TAG = "NotifRemoteInputManager";
87 
88     private RemoteInputListener mRemoteInputListener;
89 
90     // Dependencies:
91     private final NotificationLockscreenUserManager mLockscreenUserManager;
92     private final SmartReplyController mSmartReplyController;
93     private final NotificationVisibilityProvider mVisibilityProvider;
94     private final PowerInteractor mPowerInteractor;
95     private final ActionClickLogger mLogger;
96     protected final Context mContext;
97     protected final NotifPipelineFlags mNotifPipelineFlags;
98     private final UserManager mUserManager;
99     private final KeyguardManager mKeyguardManager;
100     private final StatusBarStateController mStatusBarStateController;
101     private final RemoteInputUriController mRemoteInputUriController;
102 
103     private final RemoteInputControllerLogger mRemoteInputControllerLogger;
104     private final NotificationClickNotifier mClickNotifier;
105 
106     protected RemoteInputController mRemoteInputController;
107     protected IStatusBarService mBarService;
108     protected Callback mCallback;
109 
110     private final List<RemoteInputController.Callback> mControllerCallbacks = new ArrayList<>();
111     private final ListenerSet<Consumer<NotificationEntry>> mActionPressListeners =
112             new ListenerSet<>();
113 
114     private final InteractionHandler mInteractionHandler = new InteractionHandler() {
115 
116         @Override
117         public boolean onInteraction(
118                 View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) {
119             mPowerInteractor.wakeUpIfDozing(
120                     "NOTIFICATION_CLICK", PowerManager.WAKE_REASON_GESTURE);
121 
122             Integer actionIndex = (Integer)
123                     view.getTag(com.android.internal.R.id.notification_action_index_tag);
124 
125             final NotificationEntry entry = getNotificationForParent(view.getParent());
126             mLogger.logInitialClick(entry, actionIndex, pendingIntent);
127 
128             if (handleRemoteInput(view, pendingIntent)) {
129                 mLogger.logRemoteInputWasHandled(entry, actionIndex);
130                 return true;
131             }
132 
133             if (DEBUG) {
134                 Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
135             }
136             logActionClick(view, entry, pendingIntent);
137             // The intent we are sending is for the application, which
138             // won't have permission to immediately start an activity after
139             // the user switches to home.  We know it is safe to do at this
140             // point, so make sure new activity switches are now allowed.
141             try {
142                 ActivityManager.getService().resumeAppSwitches();
143             } catch (RemoteException e) {
144             }
145             Notification.Action action = getActionFromView(view, entry, pendingIntent);
146             return mCallback.handleRemoteViewClick(view, pendingIntent,
147                     action == null ? false : action.isAuthenticationRequired(), actionIndex, () -> {
148                     Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
149                     mLogger.logStartingIntentWithDefaultHandler(entry, pendingIntent, actionIndex);
150                     boolean started = RemoteViews.startPendingIntent(view, pendingIntent, options);
151                     if (started) releaseNotificationIfKeptForRemoteInputHistory(entry);
152                     return started;
153             });
154         }
155 
156         private @Nullable Notification.Action getActionFromView(View view,
157                 NotificationEntry entry, PendingIntent actionIntent) {
158             Integer actionIndex = (Integer)
159                     view.getTag(com.android.internal.R.id.notification_action_index_tag);
160             if (actionIndex == null) {
161                 return null;
162             }
163             if (entry == null) {
164                 Log.w(TAG, "Couldn't determine notification for click.");
165                 return null;
166             }
167 
168             // Notification may be updated before this function is executed, and thus play safe
169             // here and verify that the action object is still the one that where the click happens.
170             StatusBarNotification statusBarNotification = entry.getSbn();
171             Notification.Action[] actions = statusBarNotification.getNotification().actions;
172             if (actions == null || actionIndex >= actions.length) {
173                 Log.w(TAG, "statusBarNotification.getNotification().actions is null or invalid");
174                 return null ;
175             }
176             final Notification.Action action =
177                     statusBarNotification.getNotification().actions[actionIndex];
178             if (!Objects.equals(action.actionIntent, actionIntent)) {
179                 Log.w(TAG, "actionIntent does not match");
180                 return null;
181             }
182             return action;
183         }
184 
185         private void logActionClick(
186                 View view,
187                 NotificationEntry entry,
188                 PendingIntent actionIntent) {
189             Notification.Action action = getActionFromView(view, entry, actionIntent);
190             if (action == null) {
191                 return;
192             }
193             ViewParent parent = view.getParent();
194             String key = entry.getSbn().getKey();
195             int buttonIndex = -1;
196             // If this is a default template, determine the index of the button.
197             if (view.getId() == com.android.internal.R.id.action0 &&
198                     parent != null && parent instanceof ViewGroup) {
199                 ViewGroup actionGroup = (ViewGroup) parent;
200                 buttonIndex = actionGroup.indexOfChild(view);
201             }
202             final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true);
203             mClickNotifier.onNotificationActionClick(key, buttonIndex, action, nv, false);
204         }
205 
206         private NotificationEntry getNotificationForParent(ViewParent parent) {
207             while (parent != null) {
208                 if (parent instanceof ExpandableNotificationRow) {
209                     return ((ExpandableNotificationRow) parent).getEntry();
210                 }
211                 parent = parent.getParent();
212             }
213             return null;
214         }
215 
216         private boolean handleRemoteInput(View view, PendingIntent pendingIntent) {
217             if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) {
218                 return true;
219             }
220 
221             Object tag = view.getTag(com.android.internal.R.id.remote_input_tag);
222             RemoteInput[] inputs = null;
223             if (tag instanceof RemoteInput[]) {
224                 inputs = (RemoteInput[]) tag;
225             }
226 
227             if (inputs == null) {
228                 return false;
229             }
230 
231             RemoteInput input = null;
232 
233             for (RemoteInput i : inputs) {
234                 if (i.getAllowFreeFormInput()) {
235                     input = i;
236                 }
237             }
238 
239             if (input == null) {
240                 return false;
241             }
242 
243             return activateRemoteInput(view, inputs, input, pendingIntent,
244                     null /* editedSuggestionInfo */);
245         }
246     };
247 
248     /**
249      * Injected constructor. See {@link CentralSurfacesDependenciesModule}.
250      */
NotificationRemoteInputManager( Context context, NotifPipelineFlags notifPipelineFlags, NotificationLockscreenUserManager lockscreenUserManager, SmartReplyController smartReplyController, NotificationVisibilityProvider visibilityProvider, PowerInteractor powerInteractor, StatusBarStateController statusBarStateController, RemoteInputUriController remoteInputUriController, RemoteInputControllerLogger remoteInputControllerLogger, NotificationClickNotifier clickNotifier, ActionClickLogger logger, DumpManager dumpManager)251     public NotificationRemoteInputManager(
252             Context context,
253             NotifPipelineFlags notifPipelineFlags,
254             NotificationLockscreenUserManager lockscreenUserManager,
255             SmartReplyController smartReplyController,
256             NotificationVisibilityProvider visibilityProvider,
257             PowerInteractor powerInteractor,
258             StatusBarStateController statusBarStateController,
259             RemoteInputUriController remoteInputUriController,
260             RemoteInputControllerLogger remoteInputControllerLogger,
261             NotificationClickNotifier clickNotifier,
262             ActionClickLogger logger,
263             DumpManager dumpManager) {
264         mContext = context;
265         mNotifPipelineFlags = notifPipelineFlags;
266         mLockscreenUserManager = lockscreenUserManager;
267         mSmartReplyController = smartReplyController;
268         mVisibilityProvider = visibilityProvider;
269         mPowerInteractor = powerInteractor;
270         mLogger = logger;
271         mBarService = IStatusBarService.Stub.asInterface(
272                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
273         mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
274         mKeyguardManager = context.getSystemService(KeyguardManager.class);
275         mStatusBarStateController = statusBarStateController;
276         mRemoteInputUriController = remoteInputUriController;
277         mRemoteInputControllerLogger = remoteInputControllerLogger;
278         mClickNotifier = clickNotifier;
279 
280         dumpManager.registerDumpable(this);
281     }
282 
283     /** Add a listener for various remote input events.  Works with NEW pipeline only. */
setRemoteInputListener(@onNull RemoteInputListener remoteInputListener)284     public void setRemoteInputListener(@NonNull RemoteInputListener remoteInputListener) {
285         if (mRemoteInputListener != null) {
286             throw new IllegalStateException("mRemoteInputListener is already set");
287         }
288         mRemoteInputListener = remoteInputListener;
289         if (mRemoteInputController != null) {
290             mRemoteInputListener.setRemoteInputController(mRemoteInputController);
291         }
292     }
293 
294     /** Initializes this component with the provided dependencies. */
setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate)295     public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) {
296         mCallback = callback;
297         mRemoteInputController = new RemoteInputController(delegate,
298                 mRemoteInputUriController, mRemoteInputControllerLogger);
299         if (mRemoteInputListener != null) {
300             mRemoteInputListener.setRemoteInputController(mRemoteInputController);
301         }
302         // Register all stored callbacks from before the Controller was initialized.
303         for (RemoteInputController.Callback cb : mControllerCallbacks) {
304             mRemoteInputController.addCallback(cb);
305         }
306         mControllerCallbacks.clear();
307         mRemoteInputController.addCallback(new RemoteInputController.Callback() {
308             @Override
309             public void onRemoteInputSent(NotificationEntry entry) {
310                 if (mRemoteInputListener != null) {
311                     mRemoteInputListener.onRemoteInputSent(entry);
312                 }
313                 try {
314                     mBarService.onNotificationDirectReplied(entry.getSbn().getKey());
315                     if (entry.editedSuggestionInfo != null) {
316                         boolean modifiedBeforeSending =
317                                 !TextUtils.equals(entry.remoteInputText,
318                                         entry.editedSuggestionInfo.originalText);
319                         mBarService.onNotificationSmartReplySent(
320                                 entry.getSbn().getKey(),
321                                 entry.editedSuggestionInfo.index,
322                                 entry.editedSuggestionInfo.originalText,
323                                 NotificationLogger
324                                         .getNotificationLocation(entry)
325                                         .toMetricsEventEnum(),
326                                 modifiedBeforeSending);
327                     }
328                 } catch (RemoteException e) {
329                     // Nothing to do, system going down
330                 }
331             }
332         });
333     }
334 
addControllerCallback(RemoteInputController.Callback callback)335     public void addControllerCallback(RemoteInputController.Callback callback) {
336         if (mRemoteInputController != null) {
337             mRemoteInputController.addCallback(callback);
338         } else {
339             mControllerCallbacks.add(callback);
340         }
341     }
342 
removeControllerCallback(RemoteInputController.Callback callback)343     public void removeControllerCallback(RemoteInputController.Callback callback) {
344         if (mRemoteInputController != null) {
345             mRemoteInputController.removeCallback(callback);
346         } else {
347             mControllerCallbacks.remove(callback);
348         }
349     }
350 
addActionPressListener(Consumer<NotificationEntry> listener)351     public void addActionPressListener(Consumer<NotificationEntry> listener) {
352         mActionPressListeners.addIfAbsent(listener);
353     }
354 
removeActionPressListener(Consumer<NotificationEntry> listener)355     public void removeActionPressListener(Consumer<NotificationEntry> listener) {
356         mActionPressListeners.remove(listener);
357     }
358 
359     /**
360      * Activates a given {@link RemoteInput}
361      *
362      * @param view The view of the action button or suggestion chip that was tapped.
363      * @param inputs The remote inputs that need to be sent to the app.
364      * @param input The remote input that needs to be activated.
365      * @param pendingIntent The pending intent to be sent to the app.
366      * @param editedSuggestionInfo The smart reply that should be inserted in the remote input, or
367      *         {@code null} if the user is not editing a smart reply.
368      * @return Whether the {@link RemoteInput} was activated.
369      */
activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input, PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo)370     public boolean activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input,
371             PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo) {
372         return activateRemoteInput(view, inputs, input, pendingIntent, editedSuggestionInfo,
373                 null /* userMessageContent */, null /* authBypassCheck */);
374     }
375 
376     /**
377      * Activates a given {@link RemoteInput}
378      *
379      * @param view The view of the action button or suggestion chip that was tapped.
380      * @param inputs The remote inputs that need to be sent to the app.
381      * @param input The remote input that needs to be activated.
382      * @param pendingIntent The pending intent to be sent to the app.
383      * @param editedSuggestionInfo The smart reply that should be inserted in the remote input, or
384      *         {@code null} if the user is not editing a smart reply.
385      * @param userMessageContent User-entered text with which to initialize the remote input view.
386      * @param authBypassCheck Optional auth bypass check associated with this remote input
387      *         activation. If {@code null}, we never bypass.
388      * @return Whether the {@link RemoteInput} was activated.
389      */
activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input, PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo, @Nullable String userMessageContent, @Nullable AuthBypassPredicate authBypassCheck)390     public boolean activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input,
391             PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo,
392             @Nullable String userMessageContent,
393             @Nullable AuthBypassPredicate authBypassCheck) {
394         ViewParent p = view.getParent();
395         RemoteInputView riv = null;
396         ExpandableNotificationRow row = null;
397         while (p != null) {
398             if (p instanceof View) {
399                 View pv = (View) p;
400                 if (pv.getId() == com.android.internal.R.id.status_bar_latest_event_content) {
401                     riv = findRemoteInputView(pv);
402                     row = (ExpandableNotificationRow) pv.getTag(R.id.row_tag_for_content_view);
403                     break;
404                 }
405             }
406             p = p.getParent();
407         }
408 
409         if (row == null) {
410             return false;
411         }
412 
413         row.setUserExpanded(true);
414 
415         final boolean deferBouncer = authBypassCheck != null;
416         if (!deferBouncer && showBouncerForRemoteInput(view, pendingIntent, row)) {
417             return true;
418         }
419 
420         if (riv != null && !riv.isAttachedToWindow()) {
421             // the remoteInput isn't attached to the window anymore :/ Let's focus on the expanded
422             // one instead if it's available
423             riv = null;
424         }
425         if (riv == null) {
426             riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild());
427             if (riv == null) {
428                 return false;
429             }
430         }
431         if (riv == row.getPrivateLayout().getExpandedRemoteInput()
432                 && !row.getPrivateLayout().getExpandedChild().isShown()) {
433             // The expanded layout is selected, but it's not shown yet, let's wait on it to
434             // show before we do the animation.
435             mCallback.onMakeExpandedVisibleForRemoteInput(row, view, deferBouncer, () -> {
436                 activateRemoteInput(view, inputs, input, pendingIntent, editedSuggestionInfo,
437                         userMessageContent, authBypassCheck);
438             });
439             return true;
440         }
441 
442         if (!riv.isAttachedToWindow()) {
443             // if we still didn't find a view that is attached, let's abort.
444             return false;
445         }
446         int width = view.getWidth();
447         if (view instanceof TextView) {
448             // Center the reveal on the text which might be off-center from the TextView
449             TextView tv = (TextView) view;
450             if (tv.getLayout() != null) {
451                 int innerWidth = (int) tv.getLayout().getLineWidth(0);
452                 innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight();
453                 width = Math.min(width, innerWidth);
454             }
455         }
456         int cx = view.getLeft() + width / 2;
457         int cy = view.getTop() + view.getHeight() / 2;
458         int w = riv.getWidth();
459         int h = riv.getHeight();
460         int r = Math.max(
461                 Math.max(cx + cy, cx + (h - cy)),
462                 Math.max((w - cx) + cy, (w - cx) + (h - cy)));
463 
464         riv.getController().setRevealParams(new RemoteInputView.RevealParams(cx, cy, r));
465         riv.getController().setPendingIntent(pendingIntent);
466         riv.getController().setRemoteInput(input);
467         riv.getController().setRemoteInputs(inputs);
468         riv.getController().setEditedSuggestionInfo(editedSuggestionInfo);
469         riv.focusAnimated();
470         if (userMessageContent != null) {
471             riv.setEditTextContent(userMessageContent);
472         }
473         if (deferBouncer) {
474             final ExpandableNotificationRow finalRow = row;
475             riv.getController().setBouncerChecker(() ->
476                     !authBypassCheck.canSendRemoteInputWithoutBouncer()
477                             && showBouncerForRemoteInput(view, pendingIntent, finalRow));
478         }
479 
480         return true;
481     }
482 
showBouncerForRemoteInput(View view, PendingIntent pendingIntent, ExpandableNotificationRow row)483     private boolean showBouncerForRemoteInput(View view, PendingIntent pendingIntent,
484             ExpandableNotificationRow row) {
485         if (mLockscreenUserManager.shouldAllowLockscreenRemoteInput()) {
486             return false;
487         }
488 
489         final int userId = pendingIntent.getCreatorUserHandle().getIdentifier();
490 
491         final boolean isLockedManagedProfile =
492                 mUserManager.getUserInfo(userId).isManagedProfile()
493                         && mKeyguardManager.isDeviceLocked(userId);
494 
495         final boolean isParentUserLocked;
496         if (isLockedManagedProfile) {
497             final UserInfo profileParent = mUserManager.getProfileParent(userId);
498             isParentUserLocked = (profileParent != null)
499                     && mKeyguardManager.isDeviceLocked(profileParent.id);
500         } else {
501             isParentUserLocked = false;
502         }
503 
504         if ((mLockscreenUserManager.isLockscreenPublicMode(userId)
505                 || mStatusBarStateController.getState() == StatusBarState.KEYGUARD)) {
506             // If the parent user is no longer locked, and the user to which the remote
507             // input
508             // is destined is a locked, managed profile, then onLockedWorkRemoteInput
509             // should be
510             // called to unlock it.
511             if (isLockedManagedProfile && !isParentUserLocked) {
512                 mCallback.onLockedWorkRemoteInput(userId, row, view);
513             } else {
514                 // Even if we don't have security we should go through this flow, otherwise
515                 // we won't go to the shade.
516                 mCallback.onLockedRemoteInput(row, view);
517             }
518             return true;
519         }
520         if (isLockedManagedProfile) {
521             mCallback.onLockedWorkRemoteInput(userId, row, view);
522             return true;
523         }
524         return false;
525     }
526 
findRemoteInputView(View v)527     private RemoteInputView findRemoteInputView(View v) {
528         if (v == null) {
529             return null;
530         }
531         return v.findViewWithTag(RemoteInputView.VIEW_TAG);
532     }
533 
534     /**
535      * Disable remote input on the entry and remove the remote input view.
536      * This should be called when a user dismisses a notification that won't be lifetime extended.
537      */
cleanUpRemoteInputForUserRemoval(NotificationEntry entry)538     public void cleanUpRemoteInputForUserRemoval(NotificationEntry entry) {
539         if (isRemoteInputActive(entry)) {
540             entry.mRemoteEditImeVisible = false;
541             mRemoteInputController.removeRemoteInput(entry, null);
542         }
543     }
544 
545     /** Informs the remote input system that the panel has collapsed */
onPanelCollapsed()546     public void onPanelCollapsed() {
547         if (mRemoteInputListener != null) {
548             mRemoteInputListener.onPanelCollapsed();
549         }
550     }
551 
552     /** Returns whether the given notification is lifetime extended because of remote input */
isNotificationKeptForRemoteInputHistory(String key)553     public boolean isNotificationKeptForRemoteInputHistory(String key) {
554         return mRemoteInputListener != null
555                 && mRemoteInputListener.isNotificationKeptForRemoteInputHistory(key);
556     }
557 
558     /** Returns whether the notification should be lifetime extended for remote input history */
shouldKeepForRemoteInputHistory(NotificationEntry entry)559     public boolean shouldKeepForRemoteInputHistory(NotificationEntry entry) {
560         if (!FORCE_REMOTE_INPUT_HISTORY) {
561             return false;
562         }
563         return isSpinning(entry.getKey()) || entry.hasJustSentRemoteInput();
564     }
565 
566     /**
567      * Checks if the notification is being kept due to the user sending an inline reply, and if
568      * so, releases that hold.  This is called anytime an action on the notification is dispatched
569      * (after unlock, if applicable), and will then wait a short time to allow the app to update the
570      * notification in response to the action.
571      */
releaseNotificationIfKeptForRemoteInputHistory(NotificationEntry entry)572     private void releaseNotificationIfKeptForRemoteInputHistory(NotificationEntry entry) {
573         if (entry == null) {
574             return;
575         }
576         if (mRemoteInputListener != null) {
577             mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(entry);
578         }
579         for (Consumer<NotificationEntry> listener : mActionPressListeners) {
580             listener.accept(entry);
581         }
582     }
583 
584     /** Returns whether the notification should be lifetime extended for smart reply history */
shouldKeepForSmartReplyHistory(NotificationEntry entry)585     public boolean shouldKeepForSmartReplyHistory(NotificationEntry entry) {
586         if (!FORCE_REMOTE_INPUT_HISTORY) {
587             return false;
588         }
589         return mSmartReplyController.isSendingSmartReply(entry.getKey());
590     }
591 
checkRemoteInputOutside(MotionEvent event)592     public void checkRemoteInputOutside(MotionEvent event) {
593         if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
594                 && event.getX() == 0 && event.getY() == 0  // a touch outside both bars
595                 && isRemoteInputActive()) {
596             closeRemoteInputs();
597         }
598     }
599 
600     @Override
dump(PrintWriter pwOriginal, String[] args)601     public void dump(PrintWriter pwOriginal, String[] args) {
602         IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal);
603         if (mRemoteInputController != null) {
604             pw.println("mRemoteInputController: " + mRemoteInputController);
605             pw.increaseIndent();
606             mRemoteInputController.dump(pw);
607             pw.decreaseIndent();
608         }
609         if (mRemoteInputListener instanceof Dumpable) {
610             pw.println("mRemoteInputListener: " + mRemoteInputListener.getClass().getSimpleName());
611             pw.increaseIndent();
612             ((Dumpable) mRemoteInputListener).dump(pw, args);
613             pw.decreaseIndent();
614         }
615     }
616 
bindRow(ExpandableNotificationRow row)617     public void bindRow(ExpandableNotificationRow row) {
618         row.setRemoteInputController(mRemoteInputController);
619     }
620 
621     /**
622      * Return on-click handler for notification remote views
623      *
624      * @return on-click handler
625      */
getRemoteViewsOnClickHandler()626     public RemoteViews.InteractionHandler getRemoteViewsOnClickHandler() {
627         return mInteractionHandler;
628     }
629 
isRemoteInputActive()630     public boolean isRemoteInputActive() {
631         return mRemoteInputController != null && mRemoteInputController.isRemoteInputActive();
632     }
633 
isRemoteInputActive(NotificationEntry entry)634     public boolean isRemoteInputActive(NotificationEntry entry) {
635         return mRemoteInputController != null && mRemoteInputController.isRemoteInputActive(entry);
636     }
637 
isSpinning(String entryKey)638     public boolean isSpinning(String entryKey) {
639         return mRemoteInputController != null && mRemoteInputController.isSpinning(entryKey);
640     }
641 
closeRemoteInputs()642     public void closeRemoteInputs() {
643         if (mRemoteInputController != null) {
644             mRemoteInputController.closeRemoteInputs();
645         }
646     }
647 
648     /**
649      * Callback for various remote input related events, or for providing information that
650      * NotificationRemoteInputManager needs to know to decide what to do.
651      */
652     public interface Callback {
653 
654         /**
655          * Called when remote input was activated but the device is locked.
656          *
657          * @param row
658          * @param clicked
659          */
onLockedRemoteInput(ExpandableNotificationRow row, View clicked)660         void onLockedRemoteInput(ExpandableNotificationRow row, View clicked);
661 
662         /**
663          * Called when remote input was activated but the device is locked and in a managed profile.
664          *
665          * @param userId
666          * @param row
667          * @param clicked
668          */
onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked)669         void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked);
670 
671         /**
672          * Called when a row should be made expanded for the purposes of remote input.
673          *
674          * @param row
675          * @param clickedView
676          * @param deferBouncer
677          * @param runnable
678          */
onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView, boolean deferBouncer, Runnable runnable)679         void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView,
680                 boolean deferBouncer, Runnable runnable);
681 
682         /**
683          * Return whether or not remote input should be handled for this view.
684          *
685          * @param view
686          * @param pendingIntent
687          * @return true iff the remote input should be handled
688          */
shouldHandleRemoteInput(View view, PendingIntent pendingIntent)689         boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent);
690 
691         /**
692          * Performs any special handling for a remote view click. The default behaviour can be
693          * called through the defaultHandler parameter.
694          *
695          * @param view
696          * @param pendingIntent
697          * @param appRequestedAuth
698          * @param actionIndex
699          * @param defaultHandler
700          * @return  true iff the click was handled
701          */
handleRemoteViewClick(View view, PendingIntent pendingIntent, boolean appRequestedAuth, @Nullable Integer actionIndex, ClickHandler defaultHandler)702         boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
703                 boolean appRequestedAuth, @Nullable Integer actionIndex,
704                 ClickHandler defaultHandler);
705     }
706 
707     /**
708      * Helper interface meant for passing the default on click behaviour to NotificationPresenter,
709      * so it may do its own handling before invoking the default behaviour.
710      */
711     public interface ClickHandler {
712         /**
713          * Tries to handle a click on a remote view.
714          *
715          * @return true iff the click was handled
716          */
handleClick()717         boolean handleClick();
718     }
719 
720     /**
721      * Predicate that is associated with a specific {@link #activateRemoteInput(View, RemoteInput[],
722      * RemoteInput, PendingIntent, EditedSuggestionInfo, String, AuthBypassPredicate)}
723      * invocation that determines whether or not the bouncer can be bypassed when sending the
724      * RemoteInput.
725      */
726     public interface AuthBypassPredicate {
727         /**
728          * Determines if the RemoteInput can be sent without the bouncer. Should be checked the
729          * same frame that the RemoteInput is to be sent.
730          */
canSendRemoteInputWithoutBouncer()731         boolean canSendRemoteInputWithoutBouncer();
732     }
733 
734     /** Shows the bouncer if necessary */
735     public interface BouncerChecker {
736         /**
737          * Shows the bouncer if necessary in order to send a RemoteInput.
738          *
739          * @return {@code true} if the bouncer was shown, {@code false} otherwise
740          */
showBouncerIfNecessary()741         boolean showBouncerIfNecessary();
742     }
743 
744     /** An interface for listening to remote input events that relate to notification lifetime */
745     public interface RemoteInputListener {
746         /** Called when remote input pending intent has been sent */
onRemoteInputSent(@onNull NotificationEntry entry)747         void onRemoteInputSent(@NonNull NotificationEntry entry);
748 
749         /** Called when the notification shade becomes fully closed */
onPanelCollapsed()750         void onPanelCollapsed();
751 
752         /** @return whether lifetime of a notification is being extended by the listener */
isNotificationKeptForRemoteInputHistory(@onNull String key)753         boolean isNotificationKeptForRemoteInputHistory(@NonNull String key);
754 
755         /** Called on user interaction to end lifetime extension for history */
releaseNotificationIfKeptForRemoteInputHistory(@onNull NotificationEntry entry)756         void releaseNotificationIfKeptForRemoteInputHistory(@NonNull NotificationEntry entry);
757 
758         /** Called when the RemoteInputController is attached to the manager */
setRemoteInputController(@onNull RemoteInputController remoteInputController)759         void setRemoteInputController(@NonNull RemoteInputController remoteInputController);
760     }
761 }
762