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