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