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 static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;
19 
20 import static com.android.systemui.Dependency.MAIN_HANDLER_NAME;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.app.ActivityManager;
25 import android.app.ActivityOptions;
26 import android.app.KeyguardManager;
27 import android.app.Notification;
28 import android.app.PendingIntent;
29 import android.app.RemoteInput;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.os.Handler;
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.plugins.statusbar.StatusBarStateController;
56 import com.android.systemui.statusbar.notification.NotificationEntryListener;
57 import com.android.systemui.statusbar.notification.NotificationEntryManager;
58 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
59 import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo;
60 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
61 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
62 import com.android.systemui.statusbar.phone.ShadeController;
63 import com.android.systemui.statusbar.policy.RemoteInputView;
64 
65 import java.io.FileDescriptor;
66 import java.io.PrintWriter;
67 import java.util.ArrayList;
68 import java.util.Objects;
69 import java.util.Set;
70 
71 import javax.inject.Inject;
72 import javax.inject.Named;
73 import javax.inject.Singleton;
74 
75 import dagger.Lazy;
76 
77 /**
78  * Class for handling remote input state over a set of notifications. This class handles things
79  * like keeping notifications temporarily that were cancelled as a response to a remote input
80  * interaction, keeping track of notifications to remove when NotificationPresenter is collapsed,
81  * and handling clicks on remote views.
82  */
83 @Singleton
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 
120     private final Lazy<ShadeController> mShadeController;
121 
122     protected final Context mContext;
123     private final UserManager mUserManager;
124     private final KeyguardManager mKeyguardManager;
125     private final StatusBarStateController mStatusBarStateController;
126 
127     protected RemoteInputController mRemoteInputController;
128     protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback
129             mNotificationLifetimeFinishedCallback;
130     protected IStatusBarService mBarService;
131     protected Callback mCallback;
132     protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
133 
134     private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() {
135 
136         @Override
137         public boolean onClickHandler(
138                 View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) {
139             mShadeController.get().wakeUpIfDozing(SystemClock.uptimeMillis(), view,
140                     "NOTIFICATION_CLICK");
141 
142             if (handleRemoteInput(view, pendingIntent)) {
143                 return true;
144             }
145 
146             if (DEBUG) {
147                 Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
148             }
149             logActionClick(view, pendingIntent);
150             // The intent we are sending is for the application, which
151             // won't have permission to immediately start an activity after
152             // the user switches to home.  We know it is safe to do at this
153             // point, so make sure new activity switches are now allowed.
154             try {
155                 ActivityManager.getService().resumeAppSwitches();
156             } catch (RemoteException e) {
157             }
158             return mCallback.handleRemoteViewClick(view, pendingIntent, () -> {
159                 Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
160                 options.second.setLaunchWindowingMode(
161                         WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
162                 return RemoteViews.startPendingIntent(view, pendingIntent, options);
163             });
164         }
165 
166         private void logActionClick(View view, PendingIntent actionIntent) {
167             Integer actionIndex = (Integer)
168                     view.getTag(com.android.internal.R.id.notification_action_index_tag);
169             if (actionIndex == null) {
170                 // Custom action button, not logging.
171                 return;
172             }
173             ViewParent parent = view.getParent();
174             StatusBarNotification statusBarNotification = getNotificationForParent(parent);
175             if (statusBarNotification == null) {
176                 Log.w(TAG, "Couldn't determine notification for click.");
177                 return;
178             }
179             String key = statusBarNotification.getKey();
180             int buttonIndex = -1;
181             // If this is a default template, determine the index of the button.
182             if (view.getId() == com.android.internal.R.id.action0 &&
183                     parent != null && parent instanceof ViewGroup) {
184                 ViewGroup actionGroup = (ViewGroup) parent;
185                 buttonIndex = actionGroup.indexOfChild(view);
186             }
187             final int count = mEntryManager.getNotificationData().getActiveNotifications().size();
188             final int rank = mEntryManager.getNotificationData().getRank(key);
189 
190             // Notification may be updated before this function is executed, and thus play safe
191             // here and verify that the action object is still the one that where the click happens.
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;
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;
202             }
203             NotificationVisibility.NotificationLocation location =
204                     NotificationLogger.getNotificationLocation(
205                             mEntryManager.getNotificationData().get(key));
206             final NotificationVisibility nv =
207                     NotificationVisibility.obtain(key, rank, count, true, location);
208             try {
209                 mBarService.onNotificationActionClick(key, buttonIndex, action, nv, false);
210             } catch (RemoteException e) {
211                 // Ignore
212             }
213         }
214 
215         private StatusBarNotification getNotificationForParent(ViewParent parent) {
216             while (parent != null) {
217                 if (parent instanceof ExpandableNotificationRow) {
218                     return ((ExpandableNotificationRow) parent).getStatusBarNotification();
219                 }
220                 parent = parent.getParent();
221             }
222             return null;
223         }
224 
225         private boolean handleRemoteInput(View view, PendingIntent pendingIntent) {
226             if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) {
227                 return true;
228             }
229 
230             Object tag = view.getTag(com.android.internal.R.id.remote_input_tag);
231             RemoteInput[] inputs = null;
232             if (tag instanceof RemoteInput[]) {
233                 inputs = (RemoteInput[]) tag;
234             }
235 
236             if (inputs == null) {
237                 return false;
238             }
239 
240             RemoteInput input = null;
241 
242             for (RemoteInput i : inputs) {
243                 if (i.getAllowFreeFormInput()) {
244                     input = i;
245                 }
246             }
247 
248             if (input == null) {
249                 return false;
250             }
251 
252             return activateRemoteInput(view, inputs, input, pendingIntent,
253                     null /* editedSuggestionInfo */);
254         }
255     };
256 
257     @Inject
NotificationRemoteInputManager( Context context, NotificationLockscreenUserManager lockscreenUserManager, SmartReplyController smartReplyController, NotificationEntryManager notificationEntryManager, Lazy<ShadeController> shadeController, StatusBarStateController statusBarStateController, @Named(MAIN_HANDLER_NAME) Handler mainHandler)258     public NotificationRemoteInputManager(
259             Context context,
260             NotificationLockscreenUserManager lockscreenUserManager,
261             SmartReplyController smartReplyController,
262             NotificationEntryManager notificationEntryManager,
263             Lazy<ShadeController> shadeController,
264             StatusBarStateController statusBarStateController,
265             @Named(MAIN_HANDLER_NAME) Handler mainHandler) {
266         mContext = context;
267         mLockscreenUserManager = lockscreenUserManager;
268         mSmartReplyController = smartReplyController;
269         mEntryManager = notificationEntryManager;
270         mShadeController = shadeController;
271         mMainHandler = mainHandler;
272         mBarService = IStatusBarService.Stub.asInterface(
273                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
274         mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
275         addLifetimeExtenders();
276         mKeyguardManager = context.getSystemService(KeyguardManager.class);
277         mStatusBarStateController = statusBarStateController;
278 
279         notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
280             @Override
281             public void onPreEntryUpdated(NotificationEntry entry) {
282                 // Mark smart replies as sent whenever a notification is updated - otherwise the
283                 // smart replies are never marked as sent.
284                 mSmartReplyController.stopSending(entry);
285             }
286 
287             @Override
288             public void onEntryRemoved(
289                     @Nullable NotificationEntry entry,
290                     NotificationVisibility visibility,
291                     boolean removedByUser) {
292                 // We're removing the notification, the smart controller can forget about it.
293                 mSmartReplyController.stopSending(entry);
294 
295                 if (removedByUser && entry != null) {
296                     onPerformRemoveNotification(entry, entry.key);
297                 }
298             }
299         });
300     }
301 
302     /** Initializes this component with the provided dependencies. */
setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate)303     public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) {
304         mCallback = callback;
305         mRemoteInputController = new RemoteInputController(delegate);
306         mRemoteInputController.addCallback(new RemoteInputController.Callback() {
307             @Override
308             public void onRemoteInputSent(NotificationEntry entry) {
309                 if (FORCE_REMOTE_INPUT_HISTORY
310                         && isNotificationKeptForRemoteInputHistory(entry.key)) {
311                     mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
312                 } else if (mEntriesKeptForRemoteInputActive.contains(entry)) {
313                     // We're currently holding onto this notification, but from the apps point of
314                     // view it is already canceled, so we'll need to cancel it on the apps behalf
315                     // after sending - unless the app posts an update in the mean time, so wait a
316                     // bit.
317                     mMainHandler.postDelayed(() -> {
318                         if (mEntriesKeptForRemoteInputActive.remove(entry)) {
319                             mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
320                         }
321                     }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
322                 }
323                 try {
324                     mBarService.onNotificationDirectReplied(entry.notification.getKey());
325                     if (entry.editedSuggestionInfo != null) {
326                         boolean modifiedBeforeSending =
327                                 !TextUtils.equals(entry.remoteInputText,
328                                         entry.editedSuggestionInfo.originalText);
329                         mBarService.onNotificationSmartReplySent(
330                                 entry.notification.getKey(),
331                                 entry.editedSuggestionInfo.index,
332                                 entry.editedSuggestionInfo.originalText,
333                                 NotificationLogger
334                                         .getNotificationLocation(entry)
335                                         .toMetricsEventEnum(),
336                                 modifiedBeforeSending);
337                     }
338                 } catch (RemoteException e) {
339                     // Nothing to do, system going down
340                 }
341             }
342         });
343         mSmartReplyController.setCallback((entry, reply) -> {
344             StatusBarNotification newSbn =
345                     rebuildNotificationWithRemoteInput(entry, reply, true /* showSpinner */);
346             mEntryManager.updateNotification(newSbn, null /* ranking */);
347         });
348     }
349 
350     /**
351      * Activates a given {@link RemoteInput}
352      *
353      * @param view The view of the action button or suggestion chip that was tapped.
354      * @param inputs The remote inputs that need to be sent to the app.
355      * @param input The remote input that needs to be activated.
356      * @param pendingIntent The pending intent to be sent to the app.
357      * @param editedSuggestionInfo The smart reply that should be inserted in the remote input, or
358      *         {@code null} if the user is not editing a smart reply.
359      * @return Whether the {@link RemoteInput} was activated.
360      */
activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input, PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo)361     public boolean activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input,
362             PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo) {
363 
364         ViewParent p = view.getParent();
365         RemoteInputView riv = null;
366         ExpandableNotificationRow row = null;
367         while (p != null) {
368             if (p instanceof View) {
369                 View pv = (View) p;
370                 if (pv.isRootNamespace()) {
371                     riv = findRemoteInputView(pv);
372                     row = (ExpandableNotificationRow) pv.getTag(R.id.row_tag_for_content_view);
373                     break;
374                 }
375             }
376             p = p.getParent();
377         }
378 
379         if (row == null) {
380             return false;
381         }
382 
383         row.setUserExpanded(true);
384 
385         if (!mLockscreenUserManager.shouldAllowLockscreenRemoteInput()) {
386             final int userId = pendingIntent.getCreatorUserHandle().getIdentifier();
387             if (mLockscreenUserManager.isLockscreenPublicMode(userId)
388                     || mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
389                 // Even if we don't have security we should go through this flow, otherwise we won't
390                 // go to the shade
391                 mCallback.onLockedRemoteInput(row, view);
392                 return true;
393             }
394             if (mUserManager.getUserInfo(userId).isManagedProfile()
395                     && mKeyguardManager.isDeviceLocked(userId)) {
396                 mCallback.onLockedWorkRemoteInput(userId, row, view);
397                 return true;
398             }
399         }
400 
401         if (riv != null && !riv.isAttachedToWindow()) {
402             // the remoteInput isn't attached to the window anymore :/ Let's focus on the expanded
403             // one instead if it's available
404             riv = null;
405         }
406         if (riv == null) {
407             riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild());
408             if (riv == null) {
409                 return false;
410             }
411         }
412         if (riv == row.getPrivateLayout().getExpandedRemoteInput()
413                 && !row.getPrivateLayout().getExpandedChild().isShown()) {
414             // The expanded layout is selected, but it's not shown yet, let's wait on it to
415             // show before we do the animation.
416             mCallback.onMakeExpandedVisibleForRemoteInput(row, view);
417             return true;
418         }
419 
420         if (!riv.isAttachedToWindow()) {
421             // if we still didn't find a view that is attached, let's abort.
422             return false;
423         }
424         int width = view.getWidth();
425         if (view instanceof TextView) {
426             // Center the reveal on the text which might be off-center from the TextView
427             TextView tv = (TextView) view;
428             if (tv.getLayout() != null) {
429                 int innerWidth = (int) tv.getLayout().getLineWidth(0);
430                 innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight();
431                 width = Math.min(width, innerWidth);
432             }
433         }
434         int cx = view.getLeft() + width / 2;
435         int cy = view.getTop() + view.getHeight() / 2;
436         int w = riv.getWidth();
437         int h = riv.getHeight();
438         int r = Math.max(
439                 Math.max(cx + cy, cx + (h - cy)),
440                 Math.max((w - cx) + cy, (w - cx) + (h - cy)));
441 
442         riv.setRevealParameters(cx, cy, r);
443         riv.setPendingIntent(pendingIntent);
444         riv.setRemoteInput(inputs, input, editedSuggestionInfo);
445         riv.focusAnimated();
446 
447         return true;
448     }
449 
findRemoteInputView(View v)450     private RemoteInputView findRemoteInputView(View v) {
451         if (v == null) {
452             return null;
453         }
454         return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG);
455     }
456 
457     /**
458      * Adds all the notification lifetime extenders. Each extender represents a reason for the
459      * NotificationRemoteInputManager to keep a notification lifetime extended.
460      */
addLifetimeExtenders()461     protected void addLifetimeExtenders() {
462         mLifetimeExtenders.add(new RemoteInputHistoryExtender());
463         mLifetimeExtenders.add(new SmartReplyHistoryExtender());
464         mLifetimeExtenders.add(new RemoteInputActiveExtender());
465     }
466 
getLifetimeExtenders()467     public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() {
468         return mLifetimeExtenders;
469     }
470 
getController()471     public RemoteInputController getController() {
472         return mRemoteInputController;
473     }
474 
475     @VisibleForTesting
onPerformRemoveNotification(NotificationEntry entry, final String key)476     void onPerformRemoveNotification(NotificationEntry entry, final String key) {
477         if (mKeysKeptForRemoteInputHistory.contains(key)) {
478             mKeysKeptForRemoteInputHistory.remove(key);
479         }
480         if (mRemoteInputController.isRemoteInputActive(entry)) {
481             mRemoteInputController.removeRemoteInput(entry, null);
482         }
483     }
484 
onPanelCollapsed()485     public void onPanelCollapsed() {
486         for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) {
487             NotificationEntry entry = mEntriesKeptForRemoteInputActive.valueAt(i);
488             mRemoteInputController.removeRemoteInput(entry, null);
489             if (mNotificationLifetimeFinishedCallback != null) {
490                 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
491             }
492         }
493         mEntriesKeptForRemoteInputActive.clear();
494     }
495 
isNotificationKeptForRemoteInputHistory(String key)496     public boolean isNotificationKeptForRemoteInputHistory(String key) {
497         return mKeysKeptForRemoteInputHistory.contains(key);
498     }
499 
shouldKeepForRemoteInputHistory(NotificationEntry entry)500     public boolean shouldKeepForRemoteInputHistory(NotificationEntry entry) {
501         if (!FORCE_REMOTE_INPUT_HISTORY) {
502             return false;
503         }
504         return (mRemoteInputController.isSpinning(entry.key) || entry.hasJustSentRemoteInput());
505     }
506 
shouldKeepForSmartReplyHistory(NotificationEntry entry)507     public boolean shouldKeepForSmartReplyHistory(NotificationEntry entry) {
508         if (!FORCE_REMOTE_INPUT_HISTORY) {
509             return false;
510         }
511         return mSmartReplyController.isSendingSmartReply(entry.key);
512     }
513 
checkRemoteInputOutside(MotionEvent event)514     public void checkRemoteInputOutside(MotionEvent event) {
515         if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
516                 && event.getX() == 0 && event.getY() == 0  // a touch outside both bars
517                 && mRemoteInputController.isRemoteInputActive()) {
518             mRemoteInputController.closeRemoteInputs();
519         }
520     }
521 
522     @VisibleForTesting
rebuildNotificationForCanceledSmartReplies( NotificationEntry entry)523     StatusBarNotification rebuildNotificationForCanceledSmartReplies(
524             NotificationEntry entry) {
525         return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */,
526                 false /* showSpinner */);
527     }
528 
529     @VisibleForTesting
rebuildNotificationWithRemoteInput(NotificationEntry entry, CharSequence remoteInputText, boolean showSpinner)530     StatusBarNotification rebuildNotificationWithRemoteInput(NotificationEntry entry,
531             CharSequence remoteInputText, boolean showSpinner) {
532         StatusBarNotification sbn = entry.notification;
533 
534         Notification.Builder b = Notification.Builder
535                 .recoverBuilder(mContext, sbn.getNotification().clone());
536         if (remoteInputText != null) {
537             CharSequence[] oldHistory = sbn.getNotification().extras
538                     .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
539             CharSequence[] newHistory;
540             if (oldHistory == null) {
541                 newHistory = new CharSequence[1];
542             } else {
543                 newHistory = new CharSequence[oldHistory.length + 1];
544                 System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length);
545             }
546             newHistory[0] = String.valueOf(remoteInputText);
547             b.setRemoteInputHistory(newHistory);
548         }
549         b.setShowRemoteInputSpinner(showSpinner);
550         b.setHideSmartReplies(true);
551 
552         Notification newNotification = b.build();
553 
554         // Undo any compatibility view inflation
555         newNotification.contentView = sbn.getNotification().contentView;
556         newNotification.bigContentView = sbn.getNotification().bigContentView;
557         newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
558 
559         return new StatusBarNotification(
560                 sbn.getPackageName(),
561                 sbn.getOpPkg(),
562                 sbn.getId(),
563                 sbn.getTag(),
564                 sbn.getUid(),
565                 sbn.getInitialPid(),
566                 newNotification,
567                 sbn.getUser(),
568                 sbn.getOverrideGroupKey(),
569                 sbn.getPostTime());
570     }
571 
572     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)573     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
574         pw.println("NotificationRemoteInputManager state:");
575         pw.print("  mKeysKeptForRemoteInputHistory: ");
576         pw.println(mKeysKeptForRemoteInputHistory);
577         pw.print("  mEntriesKeptForRemoteInputActive: ");
578         pw.println(mEntriesKeptForRemoteInputActive);
579     }
580 
bindRow(ExpandableNotificationRow row)581     public void bindRow(ExpandableNotificationRow row) {
582         row.setRemoteInputController(mRemoteInputController);
583         row.setRemoteViewClickHandler(mOnClickHandler);
584     }
585 
586     @VisibleForTesting
getEntriesKeptForRemoteInputActive()587     public Set<NotificationEntry> getEntriesKeptForRemoteInputActive() {
588         return mEntriesKeptForRemoteInputActive;
589     }
590 
591     /**
592      * NotificationRemoteInputManager has multiple reasons to keep notification lifetime extended
593      * so we implement multiple NotificationLifetimeExtenders
594      */
595     protected abstract class RemoteInputExtender implements NotificationLifetimeExtender {
596         @Override
setCallback(NotificationSafeToRemoveCallback callback)597         public void setCallback(NotificationSafeToRemoveCallback callback) {
598             if (mNotificationLifetimeFinishedCallback == null) {
599                 mNotificationLifetimeFinishedCallback = callback;
600             }
601         }
602     }
603 
604     /**
605      * Notification is kept alive as it was cancelled in response to a remote input interaction.
606      * This allows us to show what you replied and allows you to continue typing into it.
607      */
608     protected class RemoteInputHistoryExtender extends RemoteInputExtender {
609         @Override
shouldExtendLifetime(@onNull NotificationEntry entry)610         public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
611             return shouldKeepForRemoteInputHistory(entry);
612         }
613 
614         @Override
setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend)615         public void setShouldManageLifetime(NotificationEntry entry,
616                 boolean shouldExtend) {
617             if (shouldExtend) {
618                 CharSequence remoteInputText = entry.remoteInputText;
619                 if (TextUtils.isEmpty(remoteInputText)) {
620                     remoteInputText = entry.remoteInputTextWhenReset;
621                 }
622                 StatusBarNotification newSbn = rebuildNotificationWithRemoteInput(entry,
623                         remoteInputText, false /* showSpinner */);
624                 entry.onRemoteInputInserted();
625 
626                 if (newSbn == null) {
627                     return;
628                 }
629 
630                 mEntryManager.updateNotification(newSbn, null);
631 
632                 // Ensure the entry hasn't already been removed. This can happen if there is an
633                 // inflation exception while updating the remote history
634                 if (entry.isRemoved()) {
635                     return;
636                 }
637 
638                 if (Log.isLoggable(TAG, Log.DEBUG)) {
639                     Log.d(TAG, "Keeping notification around after sending remote input "
640                             + entry.key);
641                 }
642 
643                 mKeysKeptForRemoteInputHistory.add(entry.key);
644             } else {
645                 mKeysKeptForRemoteInputHistory.remove(entry.key);
646             }
647         }
648     }
649 
650     /**
651      * Notification is kept alive for smart reply history.  Similar to REMOTE_INPUT_HISTORY but with
652      * {@link SmartReplyController} specific logic
653      */
654     protected class SmartReplyHistoryExtender extends RemoteInputExtender {
655         @Override
shouldExtendLifetime(@onNull NotificationEntry entry)656         public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
657             return shouldKeepForSmartReplyHistory(entry);
658         }
659 
660         @Override
setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend)661         public void setShouldManageLifetime(NotificationEntry entry,
662                 boolean shouldExtend) {
663             if (shouldExtend) {
664                 StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);
665 
666                 if (newSbn == null) {
667                     return;
668                 }
669 
670                 mEntryManager.updateNotification(newSbn, null);
671 
672                 if (entry.isRemoved()) {
673                     return;
674                 }
675 
676                 if (Log.isLoggable(TAG, Log.DEBUG)) {
677                     Log.d(TAG, "Keeping notification around after sending smart reply "
678                             + entry.key);
679                 }
680 
681                 mKeysKeptForRemoteInputHistory.add(entry.key);
682             } else {
683                 mKeysKeptForRemoteInputHistory.remove(entry.key);
684                 mSmartReplyController.stopSending(entry);
685             }
686         }
687     }
688 
689     /**
690      * Notification is kept alive because the user is still using the remote input
691      */
692     protected class RemoteInputActiveExtender extends RemoteInputExtender {
693         @Override
shouldExtendLifetime(@onNull NotificationEntry entry)694         public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
695             return mRemoteInputController.isRemoteInputActive(entry);
696         }
697 
698         @Override
setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend)699         public void setShouldManageLifetime(NotificationEntry entry,
700                 boolean shouldExtend) {
701             if (shouldExtend) {
702                 if (Log.isLoggable(TAG, Log.DEBUG)) {
703                     Log.d(TAG, "Keeping notification around while remote input active "
704                             + entry.key);
705                 }
706                 mEntriesKeptForRemoteInputActive.add(entry);
707             } else {
708                 mEntriesKeptForRemoteInputActive.remove(entry);
709             }
710         }
711     }
712 
713     /**
714      * Callback for various remote input related events, or for providing information that
715      * NotificationRemoteInputManager needs to know to decide what to do.
716      */
717     public interface Callback {
718 
719         /**
720          * Called when remote input was activated but the device is locked.
721          *
722          * @param row
723          * @param clicked
724          */
onLockedRemoteInput(ExpandableNotificationRow row, View clicked)725         void onLockedRemoteInput(ExpandableNotificationRow row, View clicked);
726 
727         /**
728          * Called when remote input was activated but the device is locked and in a managed profile.
729          *
730          * @param userId
731          * @param row
732          * @param clicked
733          */
onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked)734         void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked);
735 
736         /**
737          * Called when a row should be made expanded for the purposes of remote input.
738          *
739          * @param row
740          * @param clickedView
741          */
onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView)742         void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView);
743 
744         /**
745          * Return whether or not remote input should be handled for this view.
746          *
747          * @param view
748          * @param pendingIntent
749          * @return true iff the remote input should be handled
750          */
shouldHandleRemoteInput(View view, PendingIntent pendingIntent)751         boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent);
752 
753         /**
754          * Performs any special handling for a remote view click. The default behaviour can be
755          * called through the defaultHandler parameter.
756          *
757          * @param view
758          * @param pendingIntent
759          * @param defaultHandler
760          * @return  true iff the click was handled
761          */
handleRemoteViewClick(View view, PendingIntent pendingIntent, ClickHandler defaultHandler)762         boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
763                 ClickHandler defaultHandler);
764     }
765 
766     /**
767      * Helper interface meant for passing the default on click behaviour to NotificationPresenter,
768      * so it may do its own handling before invoking the default behaviour.
769      */
770     public interface ClickHandler {
771         /**
772          * Tries to handle a click on a remote view.
773          *
774          * @return true iff the click was handled
775          */
handleClick()776         boolean handleClick();
777     }
778 }
779