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