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