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