Home | History | Annotate | Download | only in statusbar
      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
    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. */
    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      */
    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 
    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      */
    445     protected void addLifetimeExtenders() {
    446         mLifetimeExtenders.add(new RemoteInputHistoryExtender());
    447         mLifetimeExtenders.add(new SmartReplyHistoryExtender());
    448         mLifetimeExtenders.add(new RemoteInputActiveExtender());
    449     }
    450 
    451     public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() {
    452         return mLifetimeExtenders;
    453     }
    454 
    455     public RemoteInputController getController() {
    456         return mRemoteInputController;
    457     }
    458 
    459     @VisibleForTesting
    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 
    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 
    480     public boolean isNotificationKeptForRemoteInputHistory(String key) {
    481         return mKeysKeptForRemoteInputHistory.contains(key);
    482     }
    483 
    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 
    491     public boolean shouldKeepForSmartReplyHistory(NotificationEntry entry) {
    492         if (!FORCE_REMOTE_INPUT_HISTORY) {
    493             return false;
    494         }
    495         return mSmartReplyController.isSendingSmartReply(entry.key);
    496     }
    497 
    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
    507     StatusBarNotification rebuildNotificationForCanceledSmartReplies(
    508             NotificationEntry entry) {
    509         return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */,
    510                 false /* showSpinner */);
    511     }
    512 
    513     @VisibleForTesting
    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
    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 
    565     public void bindRow(ExpandableNotificationRow row) {
    566         row.setRemoteInputController(mRemoteInputController);
    567         row.setRemoteViewClickHandler(mOnClickHandler);
    568     }
    569 
    570     @VisibleForTesting
    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
    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
    594         public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
    595             return shouldKeepForRemoteInputHistory(entry);
    596         }
    597 
    598         @Override
    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
    640         public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
    641             return shouldKeepForSmartReplyHistory(entry);
    642         }
    643 
    644         @Override
    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
    678         public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
    679             return mRemoteInputController.isRemoteInputActive(entry);
    680         }
    681 
    682         @Override
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    760         boolean handleClick();
    761     }
    762 }
    763