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