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.Handler;
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.ArraySet;
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.annotations.VisibleForTesting;
50 import com.android.internal.statusbar.IStatusBarService;
51 import com.android.internal.statusbar.NotificationVisibility;
52 import com.android.systemui.Dumpable;
53 import com.android.systemui.R;
54 import com.android.systemui.dagger.qualifiers.Main;
55 import com.android.systemui.dump.DumpManager;
56 import com.android.systemui.flags.FeatureFlags;
57 import com.android.systemui.plugins.statusbar.StatusBarStateController;
58 import com.android.systemui.statusbar.dagger.StatusBarDependenciesModule;
59 import com.android.systemui.statusbar.notification.NotificationEntryListener;
60 import com.android.systemui.statusbar.notification.NotificationEntryManager;
61 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
62 import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo;
63 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
64 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
65 import com.android.systemui.statusbar.phone.StatusBar;
66 import com.android.systemui.statusbar.policy.RemoteInputUriController;
67 import com.android.systemui.statusbar.policy.RemoteInputView;
68 
69 import java.io.FileDescriptor;
70 import java.io.PrintWriter;
71 import java.util.ArrayList;
72 import java.util.List;
73 import java.util.Objects;
74 import java.util.Optional;
75 import java.util.Set;
76 
77 import dagger.Lazy;
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 public class NotificationRemoteInputManager implements Dumpable {
86     public static final boolean ENABLE_REMOTE_INPUT =
87             SystemProperties.getBoolean("debug.enable_remote_input", true);
88     public static boolean FORCE_REMOTE_INPUT_HISTORY =
89             SystemProperties.getBoolean("debug.force_remoteinput_history", true);
90     private static final boolean DEBUG = false;
91     private static final String TAG = "NotifRemoteInputManager";
92 
93     private RemoteInputListener mRemoteInputListener;
94 
95     // Dependencies:
96     private final NotificationLockscreenUserManager mLockscreenUserManager;
97     private final SmartReplyController mSmartReplyController;
98     private final NotificationEntryManager mEntryManager;
99     private final Handler mMainHandler;
100     private final ActionClickLogger mLogger;
101 
102     private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
103 
104     protected final Context mContext;
105     protected final FeatureFlags mFeatureFlags;
106     private final UserManager mUserManager;
107     private final KeyguardManager mKeyguardManager;
108     private final RemoteInputNotificationRebuilder mRebuilder;
109     private final StatusBarStateController mStatusBarStateController;
110     private final RemoteInputUriController mRemoteInputUriController;
111     private final NotificationClickNotifier mClickNotifier;
112 
113     protected RemoteInputController mRemoteInputController;
114     protected IStatusBarService mBarService;
115     protected Callback mCallback;
116 
117     private final List<RemoteInputController.Callback> mControllerCallbacks = new ArrayList<>();
118 
119     private final InteractionHandler mInteractionHandler = new InteractionHandler() {
120 
121         @Override
122         public boolean onInteraction(
123                 View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) {
124             mStatusBarOptionalLazy.get().ifPresent(
125                     statusBar -> statusBar.wakeUpIfDozing(
126                             SystemClock.uptimeMillis(), view, "NOTIFICATION_CLICK"));
127 
128             final NotificationEntry entry = getNotificationForParent(view.getParent());
129             mLogger.logInitialClick(entry, pendingIntent);
130 
131             if (handleRemoteInput(view, pendingIntent)) {
132                 mLogger.logRemoteInputWasHandled(entry);
133                 return true;
134             }
135 
136             if (DEBUG) {
137                 Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
138             }
139             logActionClick(view, entry, pendingIntent);
140             // The intent we are sending is for the application, which
141             // won't have permission to immediately start an activity after
142             // the user switches to home.  We know it is safe to do at this
143             // point, so make sure new activity switches are now allowed.
144             try {
145                 ActivityManager.getService().resumeAppSwitches();
146             } catch (RemoteException e) {
147             }
148             Notification.Action action = getActionFromView(view, entry, pendingIntent);
149             return mCallback.handleRemoteViewClick(view, pendingIntent,
150                     action == null ? false : action.isAuthenticationRequired(), () -> {
151                     Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
152                     mLogger.logStartingIntentWithDefaultHandler(entry, pendingIntent);
153                     boolean started = RemoteViews.startPendingIntent(view, pendingIntent, options);
154                     if (started) releaseNotificationIfKeptForRemoteInputHistory(entry);
155                     return started;
156             });
157         }
158 
159         private @Nullable Notification.Action getActionFromView(View view,
160                 NotificationEntry entry, PendingIntent actionIntent) {
161             Integer actionIndex = (Integer)
162                     view.getTag(com.android.internal.R.id.notification_action_index_tag);
163             if (actionIndex == null) {
164                 return null;
165             }
166             if (entry == null) {
167                 Log.w(TAG, "Couldn't determine notification for click.");
168                 return null;
169             }
170 
171             // Notification may be updated before this function is executed, and thus play safe
172             // here and verify that the action object is still the one that where the click happens.
173             StatusBarNotification statusBarNotification = entry.getSbn();
174             Notification.Action[] actions = statusBarNotification.getNotification().actions;
175             if (actions == null || actionIndex >= actions.length) {
176                 Log.w(TAG, "statusBarNotification.getNotification().actions is null or invalid");
177                 return null ;
178             }
179             final Notification.Action action =
180                     statusBarNotification.getNotification().actions[actionIndex];
181             if (!Objects.equals(action.actionIntent, actionIntent)) {
182                 Log.w(TAG, "actionIntent does not match");
183                 return null;
184             }
185             return action;
186         }
187 
188         private void logActionClick(
189                 View view,
190                 NotificationEntry entry,
191                 PendingIntent actionIntent) {
192             Notification.Action action = getActionFromView(view, entry, actionIntent);
193             if (action == null) {
194                 return;
195             }
196             ViewParent parent = view.getParent();
197             String key = entry.getSbn().getKey();
198             int buttonIndex = -1;
199             // If this is a default template, determine the index of the button.
200             if (view.getId() == com.android.internal.R.id.action0 &&
201                     parent != null && parent instanceof ViewGroup) {
202                 ViewGroup actionGroup = (ViewGroup) parent;
203                 buttonIndex = actionGroup.indexOfChild(view);
204             }
205             // TODO(b/204183781): get this from the current pipeline
206             final int count = mEntryManager.getActiveNotificationsCount();
207             final int rank = entry.getRanking().getRank();
208 
209             NotificationVisibility.NotificationLocation location =
210                     NotificationLogger.getNotificationLocation(entry);
211             final NotificationVisibility nv =
212                     NotificationVisibility.obtain(key, rank, count, true, location);
213             mClickNotifier.onNotificationActionClick(key, buttonIndex, action, nv, false);
214         }
215 
216         private NotificationEntry getNotificationForParent(ViewParent parent) {
217             while (parent != null) {
218                 if (parent instanceof ExpandableNotificationRow) {
219                     return ((ExpandableNotificationRow) parent).getEntry();
220                 }
221                 parent = parent.getParent();
222             }
223             return null;
224         }
225 
226         private boolean handleRemoteInput(View view, PendingIntent pendingIntent) {
227             if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) {
228                 return true;
229             }
230 
231             Object tag = view.getTag(com.android.internal.R.id.remote_input_tag);
232             RemoteInput[] inputs = null;
233             if (tag instanceof RemoteInput[]) {
234                 inputs = (RemoteInput[]) tag;
235             }
236 
237             if (inputs == null) {
238                 return false;
239             }
240 
241             RemoteInput input = null;
242 
243             for (RemoteInput i : inputs) {
244                 if (i.getAllowFreeFormInput()) {
245                     input = i;
246                 }
247             }
248 
249             if (input == null) {
250                 return false;
251             }
252 
253             return activateRemoteInput(view, inputs, input, pendingIntent,
254                     null /* editedSuggestionInfo */);
255         }
256     };
257 
258     /**
259      * Injected constructor. See {@link StatusBarDependenciesModule}.
260      */
NotificationRemoteInputManager( Context context, FeatureFlags featureFlags, NotificationLockscreenUserManager lockscreenUserManager, SmartReplyController smartReplyController, NotificationEntryManager notificationEntryManager, RemoteInputNotificationRebuilder rebuilder, Lazy<Optional<StatusBar>> statusBarOptionalLazy, StatusBarStateController statusBarStateController, @Main Handler mainHandler, RemoteInputUriController remoteInputUriController, NotificationClickNotifier clickNotifier, ActionClickLogger logger, DumpManager dumpManager)261     public NotificationRemoteInputManager(
262             Context context,
263             FeatureFlags featureFlags,
264             NotificationLockscreenUserManager lockscreenUserManager,
265             SmartReplyController smartReplyController,
266             NotificationEntryManager notificationEntryManager,
267             RemoteInputNotificationRebuilder rebuilder,
268             Lazy<Optional<StatusBar>> statusBarOptionalLazy,
269             StatusBarStateController statusBarStateController,
270             @Main Handler mainHandler,
271             RemoteInputUriController remoteInputUriController,
272             NotificationClickNotifier clickNotifier,
273             ActionClickLogger logger,
274             DumpManager dumpManager) {
275         mContext = context;
276         mFeatureFlags = featureFlags;
277         mLockscreenUserManager = lockscreenUserManager;
278         mSmartReplyController = smartReplyController;
279         mEntryManager = notificationEntryManager;
280         mStatusBarOptionalLazy = statusBarOptionalLazy;
281         mMainHandler = mainHandler;
282         mLogger = logger;
283         mBarService = IStatusBarService.Stub.asInterface(
284                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
285         mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
286         mRebuilder = rebuilder;
287         if (!featureFlags.isNewNotifPipelineRenderingEnabled()) {
288             mRemoteInputListener = createLegacyRemoteInputLifetimeExtender(mainHandler,
289                     notificationEntryManager, smartReplyController);
290         }
291         mKeyguardManager = context.getSystemService(KeyguardManager.class);
292         mStatusBarStateController = statusBarStateController;
293         mRemoteInputUriController = remoteInputUriController;
294         mClickNotifier = clickNotifier;
295 
296         dumpManager.registerDumpable(this);
297 
298         notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
299             @Override
300             public void onPreEntryUpdated(NotificationEntry entry) {
301                 // Mark smart replies as sent whenever a notification is updated - otherwise the
302                 // smart replies are never marked as sent.
303                 mSmartReplyController.stopSending(entry);
304             }
305 
306             @Override
307             public void onEntryRemoved(
308                     @Nullable NotificationEntry entry,
309                     NotificationVisibility visibility,
310                     boolean removedByUser,
311                     int reason) {
312                 // We're removing the notification, the smart controller can forget about it.
313                 mSmartReplyController.stopSending(entry);
314 
315                 if (removedByUser && entry != null) {
316                     onPerformRemoveNotification(entry, entry.getKey());
317                 }
318             }
319         });
320     }
321 
322     /** Add a listener for various remote input events.  Works with NEW pipeline only. */
setRemoteInputListener(@onNull RemoteInputListener remoteInputListener)323     public void setRemoteInputListener(@NonNull RemoteInputListener remoteInputListener) {
324         if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
325             if (mRemoteInputListener != null) {
326                 throw new IllegalStateException("mRemoteInputListener is already set");
327             }
328             mRemoteInputListener = remoteInputListener;
329             if (mRemoteInputController != null) {
330                 mRemoteInputListener.setRemoteInputController(mRemoteInputController);
331             }
332         }
333     }
334 
335     @NonNull
336     @VisibleForTesting
createLegacyRemoteInputLifetimeExtender( Handler mainHandler, NotificationEntryManager notificationEntryManager, SmartReplyController smartReplyController)337     protected LegacyRemoteInputLifetimeExtender createLegacyRemoteInputLifetimeExtender(
338             Handler mainHandler,
339             NotificationEntryManager notificationEntryManager,
340             SmartReplyController smartReplyController) {
341         return new LegacyRemoteInputLifetimeExtender();
342     }
343 
344     /** Initializes this component with the provided dependencies. */
setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate)345     public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) {
346         mCallback = callback;
347         mRemoteInputController = new RemoteInputController(delegate, mRemoteInputUriController);
348         if (mRemoteInputListener != null) {
349             mRemoteInputListener.setRemoteInputController(mRemoteInputController);
350         }
351         // Register all stored callbacks from before the Controller was initialized.
352         for (RemoteInputController.Callback cb : mControllerCallbacks) {
353             mRemoteInputController.addCallback(cb);
354         }
355         mControllerCallbacks.clear();
356         mRemoteInputController.addCallback(new RemoteInputController.Callback() {
357             @Override
358             public void onRemoteInputSent(NotificationEntry entry) {
359                 if (mRemoteInputListener != null) {
360                     mRemoteInputListener.onRemoteInputSent(entry);
361                 }
362                 try {
363                     mBarService.onNotificationDirectReplied(entry.getSbn().getKey());
364                     if (entry.editedSuggestionInfo != null) {
365                         boolean modifiedBeforeSending =
366                                 !TextUtils.equals(entry.remoteInputText,
367                                         entry.editedSuggestionInfo.originalText);
368                         mBarService.onNotificationSmartReplySent(
369                                 entry.getSbn().getKey(),
370                                 entry.editedSuggestionInfo.index,
371                                 entry.editedSuggestionInfo.originalText,
372                                 NotificationLogger
373                                         .getNotificationLocation(entry)
374                                         .toMetricsEventEnum(),
375                                 modifiedBeforeSending);
376                     }
377                 } catch (RemoteException e) {
378                     // Nothing to do, system going down
379                 }
380             }
381         });
382         if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
383             mSmartReplyController.setCallback((entry, reply) -> {
384                 StatusBarNotification newSbn = mRebuilder.rebuildForSendingSmartReply(entry, reply);
385                 mEntryManager.updateNotification(newSbn, null /* ranking */);
386             });
387         }
388     }
389 
addControllerCallback(RemoteInputController.Callback callback)390     public void addControllerCallback(RemoteInputController.Callback callback) {
391         if (mRemoteInputController != null) {
392             mRemoteInputController.addCallback(callback);
393         } else {
394             mControllerCallbacks.add(callback);
395         }
396     }
397 
removeControllerCallback(RemoteInputController.Callback callback)398     public void removeControllerCallback(RemoteInputController.Callback callback) {
399         if (mRemoteInputController != null) {
400             mRemoteInputController.removeCallback(callback);
401         } else {
402             mControllerCallbacks.remove(callback);
403         }
404     }
405 
406     /**
407      * Activates a given {@link RemoteInput}
408      *
409      * @param view The view of the action button or suggestion chip that was tapped.
410      * @param inputs The remote inputs that need to be sent to the app.
411      * @param input The remote input that needs to be activated.
412      * @param pendingIntent The pending intent to be sent to the app.
413      * @param editedSuggestionInfo The smart reply that should be inserted in the remote input, or
414      *         {@code null} if the user is not editing a smart reply.
415      * @return Whether the {@link RemoteInput} was activated.
416      */
activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input, PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo)417     public boolean activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input,
418             PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo) {
419         return activateRemoteInput(view, inputs, input, pendingIntent, editedSuggestionInfo,
420                 null /* userMessageContent */, null /* authBypassCheck */);
421     }
422 
423     /**
424      * Activates a given {@link RemoteInput}
425      *
426      * @param view The view of the action button or suggestion chip that was tapped.
427      * @param inputs The remote inputs that need to be sent to the app.
428      * @param input The remote input that needs to be activated.
429      * @param pendingIntent The pending intent to be sent to the app.
430      * @param editedSuggestionInfo The smart reply that should be inserted in the remote input, or
431      *         {@code null} if the user is not editing a smart reply.
432      * @param userMessageContent User-entered text with which to initialize the remote input view.
433      * @param authBypassCheck Optional auth bypass check associated with this remote input
434      *         activation. If {@code null}, we never bypass.
435      * @return Whether the {@link RemoteInput} was activated.
436      */
activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input, PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo, @Nullable String userMessageContent, @Nullable AuthBypassPredicate authBypassCheck)437     public boolean activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input,
438             PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo,
439             @Nullable String userMessageContent,
440             @Nullable AuthBypassPredicate authBypassCheck) {
441         ViewParent p = view.getParent();
442         RemoteInputView riv = null;
443         ExpandableNotificationRow row = null;
444         while (p != null) {
445             if (p instanceof View) {
446                 View pv = (View) p;
447                 if (pv.isRootNamespace()) {
448                     riv = findRemoteInputView(pv);
449                     row = (ExpandableNotificationRow) pv.getTag(R.id.row_tag_for_content_view);
450                     break;
451                 }
452             }
453             p = p.getParent();
454         }
455 
456         if (row == null) {
457             return false;
458         }
459 
460         row.setUserExpanded(true);
461 
462         final boolean deferBouncer = authBypassCheck != null;
463         if (!deferBouncer && showBouncerForRemoteInput(view, pendingIntent, row)) {
464             return true;
465         }
466 
467         if (riv != null && !riv.isAttachedToWindow()) {
468             // the remoteInput isn't attached to the window anymore :/ Let's focus on the expanded
469             // one instead if it's available
470             riv = null;
471         }
472         if (riv == null) {
473             riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild());
474             if (riv == null) {
475                 return false;
476             }
477         }
478         if (riv == row.getPrivateLayout().getExpandedRemoteInput()
479                 && !row.getPrivateLayout().getExpandedChild().isShown()) {
480             // The expanded layout is selected, but it's not shown yet, let's wait on it to
481             // show before we do the animation.
482             mCallback.onMakeExpandedVisibleForRemoteInput(row, view, deferBouncer, () -> {
483                 activateRemoteInput(view, inputs, input, pendingIntent, editedSuggestionInfo,
484                         userMessageContent, authBypassCheck);
485             });
486             return true;
487         }
488 
489         if (!riv.isAttachedToWindow()) {
490             // if we still didn't find a view that is attached, let's abort.
491             return false;
492         }
493         int width = view.getWidth();
494         if (view instanceof TextView) {
495             // Center the reveal on the text which might be off-center from the TextView
496             TextView tv = (TextView) view;
497             if (tv.getLayout() != null) {
498                 int innerWidth = (int) tv.getLayout().getLineWidth(0);
499                 innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight();
500                 width = Math.min(width, innerWidth);
501             }
502         }
503         int cx = view.getLeft() + width / 2;
504         int cy = view.getTop() + view.getHeight() / 2;
505         int w = riv.getWidth();
506         int h = riv.getHeight();
507         int r = Math.max(
508                 Math.max(cx + cy, cx + (h - cy)),
509                 Math.max((w - cx) + cy, (w - cx) + (h - cy)));
510 
511         riv.setRevealParameters(cx, cy, r);
512         riv.setPendingIntent(pendingIntent);
513         riv.setRemoteInput(inputs, input, editedSuggestionInfo);
514         riv.focusAnimated();
515         if (userMessageContent != null) {
516             riv.setEditTextContent(userMessageContent);
517         }
518         if (deferBouncer) {
519             final ExpandableNotificationRow finalRow = row;
520             riv.setBouncerChecker(() -> !authBypassCheck.canSendRemoteInputWithoutBouncer()
521                     && showBouncerForRemoteInput(view, pendingIntent, finalRow));
522         }
523 
524         return true;
525     }
526 
showBouncerForRemoteInput(View view, PendingIntent pendingIntent, ExpandableNotificationRow row)527     private boolean showBouncerForRemoteInput(View view, PendingIntent pendingIntent,
528             ExpandableNotificationRow row) {
529         if (mLockscreenUserManager.shouldAllowLockscreenRemoteInput()) {
530             return false;
531         }
532 
533         final int userId = pendingIntent.getCreatorUserHandle().getIdentifier();
534 
535         final boolean isLockedManagedProfile =
536                 mUserManager.getUserInfo(userId).isManagedProfile()
537                         && mKeyguardManager.isDeviceLocked(userId);
538 
539         final boolean isParentUserLocked;
540         if (isLockedManagedProfile) {
541             final UserInfo profileParent = mUserManager.getProfileParent(userId);
542             isParentUserLocked = (profileParent != null)
543                     && mKeyguardManager.isDeviceLocked(profileParent.id);
544         } else {
545             isParentUserLocked = false;
546         }
547 
548         if ((mLockscreenUserManager.isLockscreenPublicMode(userId)
549                 || mStatusBarStateController.getState() == StatusBarState.KEYGUARD)) {
550             // If the parent user is no longer locked, and the user to which the remote
551             // input
552             // is destined is a locked, managed profile, then onLockedWorkRemoteInput
553             // should be
554             // called to unlock it.
555             if (isLockedManagedProfile && !isParentUserLocked) {
556                 mCallback.onLockedWorkRemoteInput(userId, row, view);
557             } else {
558                 // Even if we don't have security we should go through this flow, otherwise
559                 // we won't go to the shade.
560                 mCallback.onLockedRemoteInput(row, view);
561             }
562             return true;
563         }
564         if (isLockedManagedProfile) {
565             mCallback.onLockedWorkRemoteInput(userId, row, view);
566             return true;
567         }
568         return false;
569     }
570 
findRemoteInputView(View v)571     private RemoteInputView findRemoteInputView(View v) {
572         if (v == null) {
573             return null;
574         }
575         return v.findViewWithTag(RemoteInputView.VIEW_TAG);
576     }
577 
getLifetimeExtenders()578     public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() {
579         // OLD pipeline code ONLY; can assume implementation
580         return ((LegacyRemoteInputLifetimeExtender) mRemoteInputListener).mLifetimeExtenders;
581     }
582 
583     @VisibleForTesting
onPerformRemoveNotification(NotificationEntry entry, final String key)584     void onPerformRemoveNotification(NotificationEntry entry, final String key) {
585         // OLD pipeline code ONLY; can assume implementation
586         ((LegacyRemoteInputLifetimeExtender) mRemoteInputListener)
587                 .mKeysKeptForRemoteInputHistory.remove(key);
588         cleanUpRemoteInputForUserRemoval(entry);
589     }
590 
591     /**
592      * Disable remote input on the entry and remove the remote input view.
593      * This should be called when a user dismisses a notification that won't be lifetime extended.
594      */
cleanUpRemoteInputForUserRemoval(NotificationEntry entry)595     public void cleanUpRemoteInputForUserRemoval(NotificationEntry entry) {
596         if (isRemoteInputActive(entry)) {
597             entry.mRemoteEditImeVisible = false;
598             mRemoteInputController.removeRemoteInput(entry, null);
599         }
600     }
601 
602     /** Informs the remote input system that the panel has collapsed */
onPanelCollapsed()603     public void onPanelCollapsed() {
604         if (mRemoteInputListener != null) {
605             mRemoteInputListener.onPanelCollapsed();
606         }
607     }
608 
609     /** Returns whether the given notification is lifetime extended because of remote input */
isNotificationKeptForRemoteInputHistory(String key)610     public boolean isNotificationKeptForRemoteInputHistory(String key) {
611         return mRemoteInputListener != null
612                 && mRemoteInputListener.isNotificationKeptForRemoteInputHistory(key);
613     }
614 
615     /** Returns whether the notification should be lifetime extended for remote input history */
shouldKeepForRemoteInputHistory(NotificationEntry entry)616     public boolean shouldKeepForRemoteInputHistory(NotificationEntry entry) {
617         if (!FORCE_REMOTE_INPUT_HISTORY) {
618             return false;
619         }
620         return isSpinning(entry.getKey()) || entry.hasJustSentRemoteInput();
621     }
622 
623     /**
624      * Checks if the notification is being kept due to the user sending an inline reply, and if
625      * so, releases that hold.  This is called anytime an action on the notification is dispatched
626      * (after unlock, if applicable), and will then wait a short time to allow the app to update the
627      * notification in response to the action.
628      */
releaseNotificationIfKeptForRemoteInputHistory(NotificationEntry entry)629     private void releaseNotificationIfKeptForRemoteInputHistory(NotificationEntry entry) {
630         if (entry == null) {
631             return;
632         }
633         if (mRemoteInputListener != null) {
634             mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(entry);
635         }
636     }
637 
638     /** Returns whether the notification should be lifetime extended for smart reply history */
shouldKeepForSmartReplyHistory(NotificationEntry entry)639     public boolean shouldKeepForSmartReplyHistory(NotificationEntry entry) {
640         if (!FORCE_REMOTE_INPUT_HISTORY) {
641             return false;
642         }
643         return mSmartReplyController.isSendingSmartReply(entry.getKey());
644     }
645 
checkRemoteInputOutside(MotionEvent event)646     public void checkRemoteInputOutside(MotionEvent event) {
647         if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
648                 && event.getX() == 0 && event.getY() == 0  // a touch outside both bars
649                 && isRemoteInputActive()) {
650             closeRemoteInputs();
651         }
652     }
653 
654     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)655     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
656         if (mRemoteInputListener instanceof Dumpable) {
657             ((Dumpable) mRemoteInputListener).dump(fd, pw, args);
658         }
659     }
660 
bindRow(ExpandableNotificationRow row)661     public void bindRow(ExpandableNotificationRow row) {
662         row.setRemoteInputController(mRemoteInputController);
663     }
664 
665     /**
666      * Return on-click handler for notification remote views
667      *
668      * @return on-click handler
669      */
getRemoteViewsOnClickHandler()670     public RemoteViews.InteractionHandler getRemoteViewsOnClickHandler() {
671         return mInteractionHandler;
672     }
673 
isRemoteInputActive()674     public boolean isRemoteInputActive() {
675         return mRemoteInputController != null && mRemoteInputController.isRemoteInputActive();
676     }
677 
isRemoteInputActive(NotificationEntry entry)678     public boolean isRemoteInputActive(NotificationEntry entry) {
679         return mRemoteInputController != null && mRemoteInputController.isRemoteInputActive(entry);
680     }
681 
isSpinning(String entryKey)682     public boolean isSpinning(String entryKey) {
683         return mRemoteInputController != null && mRemoteInputController.isSpinning(entryKey);
684     }
685 
closeRemoteInputs()686     public void closeRemoteInputs() {
687         if (mRemoteInputController != null) {
688             mRemoteInputController.closeRemoteInputs();
689         }
690     }
691 
692     /**
693      * Callback for various remote input related events, or for providing information that
694      * NotificationRemoteInputManager needs to know to decide what to do.
695      */
696     public interface Callback {
697 
698         /**
699          * Called when remote input was activated but the device is locked.
700          *
701          * @param row
702          * @param clicked
703          */
onLockedRemoteInput(ExpandableNotificationRow row, View clicked)704         void onLockedRemoteInput(ExpandableNotificationRow row, View clicked);
705 
706         /**
707          * Called when remote input was activated but the device is locked and in a managed profile.
708          *
709          * @param userId
710          * @param row
711          * @param clicked
712          */
onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked)713         void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked);
714 
715         /**
716          * Called when a row should be made expanded for the purposes of remote input.
717          *
718          * @param row
719          * @param clickedView
720          * @param deferBouncer
721          * @param runnable
722          */
onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView, boolean deferBouncer, Runnable runnable)723         void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView,
724                 boolean deferBouncer, Runnable runnable);
725 
726         /**
727          * Return whether or not remote input should be handled for this view.
728          *
729          * @param view
730          * @param pendingIntent
731          * @return true iff the remote input should be handled
732          */
shouldHandleRemoteInput(View view, PendingIntent pendingIntent)733         boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent);
734 
735         /**
736          * Performs any special handling for a remote view click. The default behaviour can be
737          * called through the defaultHandler parameter.
738          *
739          * @param view
740          * @param pendingIntent
741          * @param appRequestedAuth
742          * @param defaultHandler
743          * @return  true iff the click was handled
744          */
handleRemoteViewClick(View view, PendingIntent pendingIntent, boolean appRequestedAuth, ClickHandler defaultHandler)745         boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
746                 boolean appRequestedAuth, ClickHandler defaultHandler);
747     }
748 
749     /**
750      * Helper interface meant for passing the default on click behaviour to NotificationPresenter,
751      * so it may do its own handling before invoking the default behaviour.
752      */
753     public interface ClickHandler {
754         /**
755          * Tries to handle a click on a remote view.
756          *
757          * @return true iff the click was handled
758          */
handleClick()759         boolean handleClick();
760     }
761 
762     /**
763      * Predicate that is associated with a specific {@link #activateRemoteInput(View, RemoteInput[],
764      * RemoteInput, PendingIntent, EditedSuggestionInfo, String, AuthBypassPredicate)}
765      * invocation that determines whether or not the bouncer can be bypassed when sending the
766      * RemoteInput.
767      */
768     public interface AuthBypassPredicate {
769         /**
770          * Determines if the RemoteInput can be sent without the bouncer. Should be checked the
771          * same frame that the RemoteInput is to be sent.
772          */
canSendRemoteInputWithoutBouncer()773         boolean canSendRemoteInputWithoutBouncer();
774     }
775 
776     /** Shows the bouncer if necessary */
777     public interface BouncerChecker {
778         /**
779          * Shows the bouncer if necessary in order to send a RemoteInput.
780          *
781          * @return {@code true} if the bouncer was shown, {@code false} otherwise
782          */
showBouncerIfNecessary()783         boolean showBouncerIfNecessary();
784     }
785 
786     /** An interface for listening to remote input events that relate to notification lifetime */
787     public interface RemoteInputListener {
788         /** Called when remote input pending intent has been sent */
onRemoteInputSent(@onNull NotificationEntry entry)789         void onRemoteInputSent(@NonNull NotificationEntry entry);
790 
791         /** Called when the notification shade becomes fully closed */
onPanelCollapsed()792         void onPanelCollapsed();
793 
794         /** @return whether lifetime of a notification is being extended by the listener */
isNotificationKeptForRemoteInputHistory(@onNull String key)795         boolean isNotificationKeptForRemoteInputHistory(@NonNull String key);
796 
797         /** Called on user interaction to end lifetime extension for history */
releaseNotificationIfKeptForRemoteInputHistory(@onNull NotificationEntry entry)798         void releaseNotificationIfKeptForRemoteInputHistory(@NonNull NotificationEntry entry);
799 
800         /** Called when the RemoteInputController is attached to the manager */
setRemoteInputController(@onNull RemoteInputController remoteInputController)801         void setRemoteInputController(@NonNull RemoteInputController remoteInputController);
802     }
803 
804     @VisibleForTesting
805     protected class LegacyRemoteInputLifetimeExtender implements RemoteInputListener, Dumpable {
806 
807         /**
808          * How long to wait before auto-dismissing a notification that was kept for remote input,
809          * and has now sent a remote input. We auto-dismiss, because the app may not see a reason to
810          * cancel these given that they technically don't exist anymore. We wait a bit in case the
811          * app issues an update.
812          */
813         private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
814 
815         /**
816          * Notifications that are already removed but are kept around because we want to show the
817          * remote input history. See {@link RemoteInputHistoryExtender} and
818          * {@link SmartReplyHistoryExtender}.
819          */
820         protected final ArraySet<String> mKeysKeptForRemoteInputHistory = new ArraySet<>();
821 
822         /**
823          * Notifications that are already removed but are kept around because the remote input is
824          * actively being used (i.e. user is typing in it).  See {@link RemoteInputActiveExtender}.
825          */
826         protected final ArraySet<NotificationEntry> mEntriesKeptForRemoteInputActive =
827                 new ArraySet<>();
828 
829         protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback
830                 mNotificationLifetimeFinishedCallback;
831 
832         protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders =
833                 new ArrayList<>();
834         private RemoteInputController mRemoteInputController;
835 
LegacyRemoteInputLifetimeExtender()836         LegacyRemoteInputLifetimeExtender() {
837             addLifetimeExtenders();
838         }
839 
840         /**
841          * Adds all the notification lifetime extenders. Each extender represents a reason for the
842          * NotificationRemoteInputManager to keep a notification lifetime extended.
843          */
addLifetimeExtenders()844         protected void addLifetimeExtenders() {
845             mLifetimeExtenders.add(new RemoteInputHistoryExtender());
846             mLifetimeExtenders.add(new SmartReplyHistoryExtender());
847             mLifetimeExtenders.add(new RemoteInputActiveExtender());
848         }
849 
850         @Override
setRemoteInputController(@onNull RemoteInputController remoteInputController)851         public void setRemoteInputController(@NonNull RemoteInputController remoteInputController) {
852             mRemoteInputController= remoteInputController;
853         }
854 
855         @Override
onRemoteInputSent(@onNull NotificationEntry entry)856         public void onRemoteInputSent(@NonNull NotificationEntry entry) {
857             if (FORCE_REMOTE_INPUT_HISTORY
858                     && isNotificationKeptForRemoteInputHistory(entry.getKey())) {
859                 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
860             } else if (mEntriesKeptForRemoteInputActive.contains(entry)) {
861                 // We're currently holding onto this notification, but from the apps point of
862                 // view it is already canceled, so we'll need to cancel it on the apps behalf
863                 // after sending - unless the app posts an update in the mean time, so wait a
864                 // bit.
865                 mMainHandler.postDelayed(() -> {
866                     if (mEntriesKeptForRemoteInputActive.remove(entry)) {
867                         mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
868                     }
869                 }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
870             }
871         }
872 
873         @Override
onPanelCollapsed()874         public void onPanelCollapsed() {
875             for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) {
876                 NotificationEntry entry = mEntriesKeptForRemoteInputActive.valueAt(i);
877                 if (mRemoteInputController != null) {
878                     mRemoteInputController.removeRemoteInput(entry, null);
879                 }
880                 if (mNotificationLifetimeFinishedCallback != null) {
881                     mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
882                 }
883             }
884             mEntriesKeptForRemoteInputActive.clear();
885         }
886 
887         @Override
isNotificationKeptForRemoteInputHistory(@onNull String key)888         public boolean isNotificationKeptForRemoteInputHistory(@NonNull String key) {
889             return mKeysKeptForRemoteInputHistory.contains(key);
890         }
891 
892         @Override
releaseNotificationIfKeptForRemoteInputHistory( @onNull NotificationEntry entry)893         public void releaseNotificationIfKeptForRemoteInputHistory(
894                 @NonNull NotificationEntry entry) {
895             final String key = entry.getKey();
896             if (isNotificationKeptForRemoteInputHistory(key)) {
897                 mMainHandler.postDelayed(() -> {
898                     if (isNotificationKeptForRemoteInputHistory(key)) {
899                         mNotificationLifetimeFinishedCallback.onSafeToRemove(key);
900                     }
901                 }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
902             }
903         }
904 
905         @VisibleForTesting
getEntriesKeptForRemoteInputActive()906         public Set<NotificationEntry> getEntriesKeptForRemoteInputActive() {
907             return mEntriesKeptForRemoteInputActive;
908         }
909 
910         @Override
dump(@onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)911         public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw,
912                 @NonNull String[] args) {
913             pw.println("LegacyRemoteInputLifetimeExtender:");
914             pw.print("  mKeysKeptForRemoteInputHistory: ");
915             pw.println(mKeysKeptForRemoteInputHistory);
916             pw.print("  mEntriesKeptForRemoteInputActive: ");
917             pw.println(mEntriesKeptForRemoteInputActive);
918         }
919 
920         /**
921          * NotificationRemoteInputManager has multiple reasons to keep notification lifetime
922          * extended so we implement multiple NotificationLifetimeExtenders
923          */
924         protected abstract class RemoteInputExtender implements NotificationLifetimeExtender {
925             @Override
setCallback(NotificationSafeToRemoveCallback callback)926             public void setCallback(NotificationSafeToRemoveCallback callback) {
927                 if (mNotificationLifetimeFinishedCallback == null) {
928                     mNotificationLifetimeFinishedCallback = callback;
929                 }
930             }
931         }
932 
933         /**
934          * Notification is kept alive as it was cancelled in response to a remote input interaction.
935          * This allows us to show what you replied and allows you to continue typing into it.
936          */
937         protected class RemoteInputHistoryExtender extends RemoteInputExtender {
938             @Override
shouldExtendLifetime(@onNull NotificationEntry entry)939             public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
940                 return shouldKeepForRemoteInputHistory(entry);
941             }
942 
943             @Override
setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend)944             public void setShouldManageLifetime(NotificationEntry entry,
945                     boolean shouldExtend) {
946                 if (shouldExtend) {
947                     StatusBarNotification newSbn = mRebuilder.rebuildForRemoteInputReply(entry);
948                     entry.onRemoteInputInserted();
949 
950                     if (newSbn == null) {
951                         return;
952                     }
953 
954                     mEntryManager.updateNotification(newSbn, null);
955 
956                     // Ensure the entry hasn't already been removed. This can happen if there is an
957                     // inflation exception while updating the remote history
958                     if (entry.isRemoved()) {
959                         return;
960                     }
961 
962                     if (Log.isLoggable(TAG, Log.DEBUG)) {
963                         Log.d(TAG, "Keeping notification around after sending remote input "
964                                 + entry.getKey());
965                     }
966 
967                     mKeysKeptForRemoteInputHistory.add(entry.getKey());
968                 } else {
969                     mKeysKeptForRemoteInputHistory.remove(entry.getKey());
970                 }
971             }
972         }
973 
974         /**
975          * Notification is kept alive for smart reply history.  Similar to REMOTE_INPUT_HISTORY but
976          * with {@link SmartReplyController} specific logic
977          */
978         protected class SmartReplyHistoryExtender extends RemoteInputExtender {
979             @Override
shouldExtendLifetime(@onNull NotificationEntry entry)980             public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
981                 return shouldKeepForSmartReplyHistory(entry);
982             }
983 
984             @Override
setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend)985             public void setShouldManageLifetime(NotificationEntry entry,
986                     boolean shouldExtend) {
987                 if (shouldExtend) {
988                     StatusBarNotification newSbn = mRebuilder.rebuildForCanceledSmartReplies(entry);
989 
990                     if (newSbn == null) {
991                         return;
992                     }
993 
994                     mEntryManager.updateNotification(newSbn, null);
995 
996                     if (entry.isRemoved()) {
997                         return;
998                     }
999 
1000                     if (Log.isLoggable(TAG, Log.DEBUG)) {
1001                         Log.d(TAG, "Keeping notification around after sending smart reply "
1002                                 + entry.getKey());
1003                     }
1004 
1005                     mKeysKeptForRemoteInputHistory.add(entry.getKey());
1006                 } else {
1007                     mKeysKeptForRemoteInputHistory.remove(entry.getKey());
1008                     mSmartReplyController.stopSending(entry);
1009                 }
1010             }
1011         }
1012 
1013         /**
1014          * Notification is kept alive because the user is still using the remote input
1015          */
1016         protected class RemoteInputActiveExtender extends RemoteInputExtender {
1017             @Override
shouldExtendLifetime(@onNull NotificationEntry entry)1018             public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
1019                 return isRemoteInputActive(entry);
1020             }
1021 
1022             @Override
setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend)1023             public void setShouldManageLifetime(NotificationEntry entry,
1024                     boolean shouldExtend) {
1025                 if (shouldExtend) {
1026                     if (Log.isLoggable(TAG, Log.DEBUG)) {
1027                         Log.d(TAG, "Keeping notification around while remote input active "
1028                                 + entry.getKey());
1029                     }
1030                     mEntriesKeptForRemoteInputActive.add(entry);
1031                 } else {
1032                     mEntriesKeptForRemoteInputActive.remove(entry);
1033                 }
1034             }
1035         }
1036     }
1037 }
1038