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