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.PowerManager; 28 import android.os.RemoteException; 29 import android.os.ServiceManager; 30 import android.os.SystemProperties; 31 import android.os.UserManager; 32 import android.service.notification.StatusBarNotification; 33 import android.text.TextUtils; 34 import android.util.IndentingPrintWriter; 35 import android.util.Log; 36 import android.util.Pair; 37 import android.view.MotionEvent; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.view.ViewParent; 41 import android.widget.RemoteViews; 42 import android.widget.RemoteViews.InteractionHandler; 43 import android.widget.TextView; 44 45 import androidx.annotation.NonNull; 46 import androidx.annotation.Nullable; 47 48 import com.android.internal.statusbar.IStatusBarService; 49 import com.android.internal.statusbar.NotificationVisibility; 50 import com.android.systemui.Dumpable; 51 import com.android.systemui.R; 52 import com.android.systemui.dump.DumpManager; 53 import com.android.systemui.plugins.statusbar.StatusBarStateController; 54 import com.android.systemui.power.domain.interactor.PowerInteractor; 55 import com.android.systemui.statusbar.dagger.CentralSurfacesDependenciesModule; 56 import com.android.systemui.statusbar.notification.NotifPipelineFlags; 57 import com.android.systemui.statusbar.notification.RemoteInputControllerLogger; 58 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 59 import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo; 60 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; 61 import com.android.systemui.statusbar.notification.logging.NotificationLogger; 62 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 63 import com.android.systemui.statusbar.policy.RemoteInputUriController; 64 import com.android.systemui.statusbar.policy.RemoteInputView; 65 import com.android.systemui.util.DumpUtilsKt; 66 import com.android.systemui.util.ListenerSet; 67 68 import java.io.PrintWriter; 69 import java.util.ArrayList; 70 import java.util.List; 71 import java.util.Objects; 72 import java.util.function.Consumer; 73 74 /** 75 * Class for handling remote input state over a set of notifications. This class handles things 76 * like keeping notifications temporarily that were cancelled as a response to a remote input 77 * interaction, keeping track of notifications to remove when NotificationPresenter is collapsed, 78 * and handling clicks on remote views. 79 */ 80 public class NotificationRemoteInputManager implements Dumpable { 81 public static final boolean ENABLE_REMOTE_INPUT = 82 SystemProperties.getBoolean("debug.enable_remote_input", true); 83 public static boolean FORCE_REMOTE_INPUT_HISTORY = 84 SystemProperties.getBoolean("debug.force_remoteinput_history", true); 85 private static final boolean DEBUG = false; 86 private static final String TAG = "NotifRemoteInputManager"; 87 88 private RemoteInputListener mRemoteInputListener; 89 90 // Dependencies: 91 private final NotificationLockscreenUserManager mLockscreenUserManager; 92 private final SmartReplyController mSmartReplyController; 93 private final NotificationVisibilityProvider mVisibilityProvider; 94 private final PowerInteractor mPowerInteractor; 95 private final ActionClickLogger mLogger; 96 protected final Context mContext; 97 protected final NotifPipelineFlags mNotifPipelineFlags; 98 private final UserManager mUserManager; 99 private final KeyguardManager mKeyguardManager; 100 private final StatusBarStateController mStatusBarStateController; 101 private final RemoteInputUriController mRemoteInputUriController; 102 103 private final RemoteInputControllerLogger mRemoteInputControllerLogger; 104 private final NotificationClickNotifier mClickNotifier; 105 106 protected RemoteInputController mRemoteInputController; 107 protected IStatusBarService mBarService; 108 protected Callback mCallback; 109 110 private final List<RemoteInputController.Callback> mControllerCallbacks = new ArrayList<>(); 111 private final ListenerSet<Consumer<NotificationEntry>> mActionPressListeners = 112 new ListenerSet<>(); 113 114 private final InteractionHandler mInteractionHandler = new InteractionHandler() { 115 116 @Override 117 public boolean onInteraction( 118 View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) { 119 mPowerInteractor.wakeUpIfDozing( 120 "NOTIFICATION_CLICK", PowerManager.WAKE_REASON_GESTURE); 121 122 Integer actionIndex = (Integer) 123 view.getTag(com.android.internal.R.id.notification_action_index_tag); 124 125 final NotificationEntry entry = getNotificationForParent(view.getParent()); 126 mLogger.logInitialClick(entry, actionIndex, pendingIntent); 127 128 if (handleRemoteInput(view, pendingIntent)) { 129 mLogger.logRemoteInputWasHandled(entry, actionIndex); 130 return true; 131 } 132 133 if (DEBUG) { 134 Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent); 135 } 136 logActionClick(view, entry, pendingIntent); 137 // The intent we are sending is for the application, which 138 // won't have permission to immediately start an activity after 139 // the user switches to home. We know it is safe to do at this 140 // point, so make sure new activity switches are now allowed. 141 try { 142 ActivityManager.getService().resumeAppSwitches(); 143 } catch (RemoteException e) { 144 } 145 Notification.Action action = getActionFromView(view, entry, pendingIntent); 146 return mCallback.handleRemoteViewClick(view, pendingIntent, 147 action == null ? false : action.isAuthenticationRequired(), actionIndex, () -> { 148 Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view); 149 mLogger.logStartingIntentWithDefaultHandler(entry, pendingIntent, actionIndex); 150 boolean started = RemoteViews.startPendingIntent(view, pendingIntent, options); 151 if (started) releaseNotificationIfKeptForRemoteInputHistory(entry); 152 return started; 153 }); 154 } 155 156 private @Nullable Notification.Action getActionFromView(View view, 157 NotificationEntry entry, PendingIntent actionIntent) { 158 Integer actionIndex = (Integer) 159 view.getTag(com.android.internal.R.id.notification_action_index_tag); 160 if (actionIndex == null) { 161 return null; 162 } 163 if (entry == null) { 164 Log.w(TAG, "Couldn't determine notification for click."); 165 return null; 166 } 167 168 // Notification may be updated before this function is executed, and thus play safe 169 // here and verify that the action object is still the one that where the click happens. 170 StatusBarNotification statusBarNotification = entry.getSbn(); 171 Notification.Action[] actions = statusBarNotification.getNotification().actions; 172 if (actions == null || actionIndex >= actions.length) { 173 Log.w(TAG, "statusBarNotification.getNotification().actions is null or invalid"); 174 return null ; 175 } 176 final Notification.Action action = 177 statusBarNotification.getNotification().actions[actionIndex]; 178 if (!Objects.equals(action.actionIntent, actionIntent)) { 179 Log.w(TAG, "actionIntent does not match"); 180 return null; 181 } 182 return action; 183 } 184 185 private void logActionClick( 186 View view, 187 NotificationEntry entry, 188 PendingIntent actionIntent) { 189 Notification.Action action = getActionFromView(view, entry, actionIntent); 190 if (action == null) { 191 return; 192 } 193 ViewParent parent = view.getParent(); 194 String key = entry.getSbn().getKey(); 195 int buttonIndex = -1; 196 // If this is a default template, determine the index of the button. 197 if (view.getId() == com.android.internal.R.id.action0 && 198 parent != null && parent instanceof ViewGroup) { 199 ViewGroup actionGroup = (ViewGroup) parent; 200 buttonIndex = actionGroup.indexOfChild(view); 201 } 202 final NotificationVisibility nv = mVisibilityProvider.obtain(entry, true); 203 mClickNotifier.onNotificationActionClick(key, buttonIndex, action, nv, false); 204 } 205 206 private NotificationEntry getNotificationForParent(ViewParent parent) { 207 while (parent != null) { 208 if (parent instanceof ExpandableNotificationRow) { 209 return ((ExpandableNotificationRow) parent).getEntry(); 210 } 211 parent = parent.getParent(); 212 } 213 return null; 214 } 215 216 private boolean handleRemoteInput(View view, PendingIntent pendingIntent) { 217 if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) { 218 return true; 219 } 220 221 Object tag = view.getTag(com.android.internal.R.id.remote_input_tag); 222 RemoteInput[] inputs = null; 223 if (tag instanceof RemoteInput[]) { 224 inputs = (RemoteInput[]) tag; 225 } 226 227 if (inputs == null) { 228 return false; 229 } 230 231 RemoteInput input = null; 232 233 for (RemoteInput i : inputs) { 234 if (i.getAllowFreeFormInput()) { 235 input = i; 236 } 237 } 238 239 if (input == null) { 240 return false; 241 } 242 243 return activateRemoteInput(view, inputs, input, pendingIntent, 244 null /* editedSuggestionInfo */); 245 } 246 }; 247 248 /** 249 * Injected constructor. See {@link CentralSurfacesDependenciesModule}. 250 */ NotificationRemoteInputManager( Context context, NotifPipelineFlags notifPipelineFlags, NotificationLockscreenUserManager lockscreenUserManager, SmartReplyController smartReplyController, NotificationVisibilityProvider visibilityProvider, PowerInteractor powerInteractor, StatusBarStateController statusBarStateController, RemoteInputUriController remoteInputUriController, RemoteInputControllerLogger remoteInputControllerLogger, NotificationClickNotifier clickNotifier, ActionClickLogger logger, DumpManager dumpManager)251 public NotificationRemoteInputManager( 252 Context context, 253 NotifPipelineFlags notifPipelineFlags, 254 NotificationLockscreenUserManager lockscreenUserManager, 255 SmartReplyController smartReplyController, 256 NotificationVisibilityProvider visibilityProvider, 257 PowerInteractor powerInteractor, 258 StatusBarStateController statusBarStateController, 259 RemoteInputUriController remoteInputUriController, 260 RemoteInputControllerLogger remoteInputControllerLogger, 261 NotificationClickNotifier clickNotifier, 262 ActionClickLogger logger, 263 DumpManager dumpManager) { 264 mContext = context; 265 mNotifPipelineFlags = notifPipelineFlags; 266 mLockscreenUserManager = lockscreenUserManager; 267 mSmartReplyController = smartReplyController; 268 mVisibilityProvider = visibilityProvider; 269 mPowerInteractor = powerInteractor; 270 mLogger = logger; 271 mBarService = IStatusBarService.Stub.asInterface( 272 ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 273 mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); 274 mKeyguardManager = context.getSystemService(KeyguardManager.class); 275 mStatusBarStateController = statusBarStateController; 276 mRemoteInputUriController = remoteInputUriController; 277 mRemoteInputControllerLogger = remoteInputControllerLogger; 278 mClickNotifier = clickNotifier; 279 280 dumpManager.registerDumpable(this); 281 } 282 283 /** Add a listener for various remote input events. Works with NEW pipeline only. */ setRemoteInputListener(@onNull RemoteInputListener remoteInputListener)284 public void setRemoteInputListener(@NonNull RemoteInputListener remoteInputListener) { 285 if (mRemoteInputListener != null) { 286 throw new IllegalStateException("mRemoteInputListener is already set"); 287 } 288 mRemoteInputListener = remoteInputListener; 289 if (mRemoteInputController != null) { 290 mRemoteInputListener.setRemoteInputController(mRemoteInputController); 291 } 292 } 293 294 /** Initializes this component with the provided dependencies. */ setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate)295 public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) { 296 mCallback = callback; 297 mRemoteInputController = new RemoteInputController(delegate, 298 mRemoteInputUriController, mRemoteInputControllerLogger); 299 if (mRemoteInputListener != null) { 300 mRemoteInputListener.setRemoteInputController(mRemoteInputController); 301 } 302 // Register all stored callbacks from before the Controller was initialized. 303 for (RemoteInputController.Callback cb : mControllerCallbacks) { 304 mRemoteInputController.addCallback(cb); 305 } 306 mControllerCallbacks.clear(); 307 mRemoteInputController.addCallback(new RemoteInputController.Callback() { 308 @Override 309 public void onRemoteInputSent(NotificationEntry entry) { 310 if (mRemoteInputListener != null) { 311 mRemoteInputListener.onRemoteInputSent(entry); 312 } 313 try { 314 mBarService.onNotificationDirectReplied(entry.getSbn().getKey()); 315 if (entry.editedSuggestionInfo != null) { 316 boolean modifiedBeforeSending = 317 !TextUtils.equals(entry.remoteInputText, 318 entry.editedSuggestionInfo.originalText); 319 mBarService.onNotificationSmartReplySent( 320 entry.getSbn().getKey(), 321 entry.editedSuggestionInfo.index, 322 entry.editedSuggestionInfo.originalText, 323 NotificationLogger 324 .getNotificationLocation(entry) 325 .toMetricsEventEnum(), 326 modifiedBeforeSending); 327 } 328 } catch (RemoteException e) { 329 // Nothing to do, system going down 330 } 331 } 332 }); 333 } 334 addControllerCallback(RemoteInputController.Callback callback)335 public void addControllerCallback(RemoteInputController.Callback callback) { 336 if (mRemoteInputController != null) { 337 mRemoteInputController.addCallback(callback); 338 } else { 339 mControllerCallbacks.add(callback); 340 } 341 } 342 removeControllerCallback(RemoteInputController.Callback callback)343 public void removeControllerCallback(RemoteInputController.Callback callback) { 344 if (mRemoteInputController != null) { 345 mRemoteInputController.removeCallback(callback); 346 } else { 347 mControllerCallbacks.remove(callback); 348 } 349 } 350 addActionPressListener(Consumer<NotificationEntry> listener)351 public void addActionPressListener(Consumer<NotificationEntry> listener) { 352 mActionPressListeners.addIfAbsent(listener); 353 } 354 removeActionPressListener(Consumer<NotificationEntry> listener)355 public void removeActionPressListener(Consumer<NotificationEntry> listener) { 356 mActionPressListeners.remove(listener); 357 } 358 359 /** 360 * Activates a given {@link RemoteInput} 361 * 362 * @param view The view of the action button or suggestion chip that was tapped. 363 * @param inputs The remote inputs that need to be sent to the app. 364 * @param input The remote input that needs to be activated. 365 * @param pendingIntent The pending intent to be sent to the app. 366 * @param editedSuggestionInfo The smart reply that should be inserted in the remote input, or 367 * {@code null} if the user is not editing a smart reply. 368 * @return Whether the {@link RemoteInput} was activated. 369 */ activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input, PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo)370 public boolean activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input, 371 PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo) { 372 return activateRemoteInput(view, inputs, input, pendingIntent, editedSuggestionInfo, 373 null /* userMessageContent */, null /* authBypassCheck */); 374 } 375 376 /** 377 * Activates a given {@link RemoteInput} 378 * 379 * @param view The view of the action button or suggestion chip that was tapped. 380 * @param inputs The remote inputs that need to be sent to the app. 381 * @param input The remote input that needs to be activated. 382 * @param pendingIntent The pending intent to be sent to the app. 383 * @param editedSuggestionInfo The smart reply that should be inserted in the remote input, or 384 * {@code null} if the user is not editing a smart reply. 385 * @param userMessageContent User-entered text with which to initialize the remote input view. 386 * @param authBypassCheck Optional auth bypass check associated with this remote input 387 * activation. If {@code null}, we never bypass. 388 * @return Whether the {@link RemoteInput} was activated. 389 */ activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input, PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo, @Nullable String userMessageContent, @Nullable AuthBypassPredicate authBypassCheck)390 public boolean activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input, 391 PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo, 392 @Nullable String userMessageContent, 393 @Nullable AuthBypassPredicate authBypassCheck) { 394 ViewParent p = view.getParent(); 395 RemoteInputView riv = null; 396 ExpandableNotificationRow row = null; 397 while (p != null) { 398 if (p instanceof View) { 399 View pv = (View) p; 400 if (pv.getId() == com.android.internal.R.id.status_bar_latest_event_content) { 401 riv = findRemoteInputView(pv); 402 row = (ExpandableNotificationRow) pv.getTag(R.id.row_tag_for_content_view); 403 break; 404 } 405 } 406 p = p.getParent(); 407 } 408 409 if (row == null) { 410 return false; 411 } 412 413 row.setUserExpanded(true); 414 415 final boolean deferBouncer = authBypassCheck != null; 416 if (!deferBouncer && showBouncerForRemoteInput(view, pendingIntent, row)) { 417 return true; 418 } 419 420 if (riv != null && !riv.isAttachedToWindow()) { 421 // the remoteInput isn't attached to the window anymore :/ Let's focus on the expanded 422 // one instead if it's available 423 riv = null; 424 } 425 if (riv == null) { 426 riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild()); 427 if (riv == null) { 428 return false; 429 } 430 } 431 if (riv == row.getPrivateLayout().getExpandedRemoteInput() 432 && !row.getPrivateLayout().getExpandedChild().isShown()) { 433 // The expanded layout is selected, but it's not shown yet, let's wait on it to 434 // show before we do the animation. 435 mCallback.onMakeExpandedVisibleForRemoteInput(row, view, deferBouncer, () -> { 436 activateRemoteInput(view, inputs, input, pendingIntent, editedSuggestionInfo, 437 userMessageContent, authBypassCheck); 438 }); 439 return true; 440 } 441 442 if (!riv.isAttachedToWindow()) { 443 // if we still didn't find a view that is attached, let's abort. 444 return false; 445 } 446 int width = view.getWidth(); 447 if (view instanceof TextView) { 448 // Center the reveal on the text which might be off-center from the TextView 449 TextView tv = (TextView) view; 450 if (tv.getLayout() != null) { 451 int innerWidth = (int) tv.getLayout().getLineWidth(0); 452 innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight(); 453 width = Math.min(width, innerWidth); 454 } 455 } 456 int cx = view.getLeft() + width / 2; 457 int cy = view.getTop() + view.getHeight() / 2; 458 int w = riv.getWidth(); 459 int h = riv.getHeight(); 460 int r = Math.max( 461 Math.max(cx + cy, cx + (h - cy)), 462 Math.max((w - cx) + cy, (w - cx) + (h - cy))); 463 464 riv.getController().setRevealParams(new RemoteInputView.RevealParams(cx, cy, r)); 465 riv.getController().setPendingIntent(pendingIntent); 466 riv.getController().setRemoteInput(input); 467 riv.getController().setRemoteInputs(inputs); 468 riv.getController().setEditedSuggestionInfo(editedSuggestionInfo); 469 riv.focusAnimated(); 470 if (userMessageContent != null) { 471 riv.setEditTextContent(userMessageContent); 472 } 473 if (deferBouncer) { 474 final ExpandableNotificationRow finalRow = row; 475 riv.getController().setBouncerChecker(() -> 476 !authBypassCheck.canSendRemoteInputWithoutBouncer() 477 && showBouncerForRemoteInput(view, pendingIntent, finalRow)); 478 } 479 480 return true; 481 } 482 showBouncerForRemoteInput(View view, PendingIntent pendingIntent, ExpandableNotificationRow row)483 private boolean showBouncerForRemoteInput(View view, PendingIntent pendingIntent, 484 ExpandableNotificationRow row) { 485 if (mLockscreenUserManager.shouldAllowLockscreenRemoteInput()) { 486 return false; 487 } 488 489 final int userId = pendingIntent.getCreatorUserHandle().getIdentifier(); 490 491 final boolean isLockedManagedProfile = 492 mUserManager.getUserInfo(userId).isManagedProfile() 493 && mKeyguardManager.isDeviceLocked(userId); 494 495 final boolean isParentUserLocked; 496 if (isLockedManagedProfile) { 497 final UserInfo profileParent = mUserManager.getProfileParent(userId); 498 isParentUserLocked = (profileParent != null) 499 && mKeyguardManager.isDeviceLocked(profileParent.id); 500 } else { 501 isParentUserLocked = false; 502 } 503 504 if ((mLockscreenUserManager.isLockscreenPublicMode(userId) 505 || mStatusBarStateController.getState() == StatusBarState.KEYGUARD)) { 506 // If the parent user is no longer locked, and the user to which the remote 507 // input 508 // is destined is a locked, managed profile, then onLockedWorkRemoteInput 509 // should be 510 // called to unlock it. 511 if (isLockedManagedProfile && !isParentUserLocked) { 512 mCallback.onLockedWorkRemoteInput(userId, row, view); 513 } else { 514 // Even if we don't have security we should go through this flow, otherwise 515 // we won't go to the shade. 516 mCallback.onLockedRemoteInput(row, view); 517 } 518 return true; 519 } 520 if (isLockedManagedProfile) { 521 mCallback.onLockedWorkRemoteInput(userId, row, view); 522 return true; 523 } 524 return false; 525 } 526 findRemoteInputView(View v)527 private RemoteInputView findRemoteInputView(View v) { 528 if (v == null) { 529 return null; 530 } 531 return v.findViewWithTag(RemoteInputView.VIEW_TAG); 532 } 533 534 /** 535 * Disable remote input on the entry and remove the remote input view. 536 * This should be called when a user dismisses a notification that won't be lifetime extended. 537 */ cleanUpRemoteInputForUserRemoval(NotificationEntry entry)538 public void cleanUpRemoteInputForUserRemoval(NotificationEntry entry) { 539 if (isRemoteInputActive(entry)) { 540 entry.mRemoteEditImeVisible = false; 541 mRemoteInputController.removeRemoteInput(entry, null); 542 } 543 } 544 545 /** Informs the remote input system that the panel has collapsed */ onPanelCollapsed()546 public void onPanelCollapsed() { 547 if (mRemoteInputListener != null) { 548 mRemoteInputListener.onPanelCollapsed(); 549 } 550 } 551 552 /** Returns whether the given notification is lifetime extended because of remote input */ isNotificationKeptForRemoteInputHistory(String key)553 public boolean isNotificationKeptForRemoteInputHistory(String key) { 554 return mRemoteInputListener != null 555 && mRemoteInputListener.isNotificationKeptForRemoteInputHistory(key); 556 } 557 558 /** Returns whether the notification should be lifetime extended for remote input history */ shouldKeepForRemoteInputHistory(NotificationEntry entry)559 public boolean shouldKeepForRemoteInputHistory(NotificationEntry entry) { 560 if (!FORCE_REMOTE_INPUT_HISTORY) { 561 return false; 562 } 563 return isSpinning(entry.getKey()) || entry.hasJustSentRemoteInput(); 564 } 565 566 /** 567 * Checks if the notification is being kept due to the user sending an inline reply, and if 568 * so, releases that hold. This is called anytime an action on the notification is dispatched 569 * (after unlock, if applicable), and will then wait a short time to allow the app to update the 570 * notification in response to the action. 571 */ releaseNotificationIfKeptForRemoteInputHistory(NotificationEntry entry)572 private void releaseNotificationIfKeptForRemoteInputHistory(NotificationEntry entry) { 573 if (entry == null) { 574 return; 575 } 576 if (mRemoteInputListener != null) { 577 mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(entry); 578 } 579 for (Consumer<NotificationEntry> listener : mActionPressListeners) { 580 listener.accept(entry); 581 } 582 } 583 584 /** Returns whether the notification should be lifetime extended for smart reply history */ shouldKeepForSmartReplyHistory(NotificationEntry entry)585 public boolean shouldKeepForSmartReplyHistory(NotificationEntry entry) { 586 if (!FORCE_REMOTE_INPUT_HISTORY) { 587 return false; 588 } 589 return mSmartReplyController.isSendingSmartReply(entry.getKey()); 590 } 591 checkRemoteInputOutside(MotionEvent event)592 public void checkRemoteInputOutside(MotionEvent event) { 593 if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar 594 && event.getX() == 0 && event.getY() == 0 // a touch outside both bars 595 && isRemoteInputActive()) { 596 closeRemoteInputs(); 597 } 598 } 599 600 @Override dump(PrintWriter pwOriginal, String[] args)601 public void dump(PrintWriter pwOriginal, String[] args) { 602 IndentingPrintWriter pw = DumpUtilsKt.asIndenting(pwOriginal); 603 if (mRemoteInputController != null) { 604 pw.println("mRemoteInputController: " + mRemoteInputController); 605 pw.increaseIndent(); 606 mRemoteInputController.dump(pw); 607 pw.decreaseIndent(); 608 } 609 if (mRemoteInputListener instanceof Dumpable) { 610 pw.println("mRemoteInputListener: " + mRemoteInputListener.getClass().getSimpleName()); 611 pw.increaseIndent(); 612 ((Dumpable) mRemoteInputListener).dump(pw, args); 613 pw.decreaseIndent(); 614 } 615 } 616 bindRow(ExpandableNotificationRow row)617 public void bindRow(ExpandableNotificationRow row) { 618 row.setRemoteInputController(mRemoteInputController); 619 } 620 621 /** 622 * Return on-click handler for notification remote views 623 * 624 * @return on-click handler 625 */ getRemoteViewsOnClickHandler()626 public RemoteViews.InteractionHandler getRemoteViewsOnClickHandler() { 627 return mInteractionHandler; 628 } 629 isRemoteInputActive()630 public boolean isRemoteInputActive() { 631 return mRemoteInputController != null && mRemoteInputController.isRemoteInputActive(); 632 } 633 isRemoteInputActive(NotificationEntry entry)634 public boolean isRemoteInputActive(NotificationEntry entry) { 635 return mRemoteInputController != null && mRemoteInputController.isRemoteInputActive(entry); 636 } 637 isSpinning(String entryKey)638 public boolean isSpinning(String entryKey) { 639 return mRemoteInputController != null && mRemoteInputController.isSpinning(entryKey); 640 } 641 closeRemoteInputs()642 public void closeRemoteInputs() { 643 if (mRemoteInputController != null) { 644 mRemoteInputController.closeRemoteInputs(); 645 } 646 } 647 648 /** 649 * Callback for various remote input related events, or for providing information that 650 * NotificationRemoteInputManager needs to know to decide what to do. 651 */ 652 public interface Callback { 653 654 /** 655 * Called when remote input was activated but the device is locked. 656 * 657 * @param row 658 * @param clicked 659 */ onLockedRemoteInput(ExpandableNotificationRow row, View clicked)660 void onLockedRemoteInput(ExpandableNotificationRow row, View clicked); 661 662 /** 663 * Called when remote input was activated but the device is locked and in a managed profile. 664 * 665 * @param userId 666 * @param row 667 * @param clicked 668 */ onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked)669 void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked); 670 671 /** 672 * Called when a row should be made expanded for the purposes of remote input. 673 * 674 * @param row 675 * @param clickedView 676 * @param deferBouncer 677 * @param runnable 678 */ onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView, boolean deferBouncer, Runnable runnable)679 void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView, 680 boolean deferBouncer, Runnable runnable); 681 682 /** 683 * Return whether or not remote input should be handled for this view. 684 * 685 * @param view 686 * @param pendingIntent 687 * @return true iff the remote input should be handled 688 */ shouldHandleRemoteInput(View view, PendingIntent pendingIntent)689 boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent); 690 691 /** 692 * Performs any special handling for a remote view click. The default behaviour can be 693 * called through the defaultHandler parameter. 694 * 695 * @param view 696 * @param pendingIntent 697 * @param appRequestedAuth 698 * @param actionIndex 699 * @param defaultHandler 700 * @return true iff the click was handled 701 */ handleRemoteViewClick(View view, PendingIntent pendingIntent, boolean appRequestedAuth, @Nullable Integer actionIndex, ClickHandler defaultHandler)702 boolean handleRemoteViewClick(View view, PendingIntent pendingIntent, 703 boolean appRequestedAuth, @Nullable Integer actionIndex, 704 ClickHandler defaultHandler); 705 } 706 707 /** 708 * Helper interface meant for passing the default on click behaviour to NotificationPresenter, 709 * so it may do its own handling before invoking the default behaviour. 710 */ 711 public interface ClickHandler { 712 /** 713 * Tries to handle a click on a remote view. 714 * 715 * @return true iff the click was handled 716 */ handleClick()717 boolean handleClick(); 718 } 719 720 /** 721 * Predicate that is associated with a specific {@link #activateRemoteInput(View, RemoteInput[], 722 * RemoteInput, PendingIntent, EditedSuggestionInfo, String, AuthBypassPredicate)} 723 * invocation that determines whether or not the bouncer can be bypassed when sending the 724 * RemoteInput. 725 */ 726 public interface AuthBypassPredicate { 727 /** 728 * Determines if the RemoteInput can be sent without the bouncer. Should be checked the 729 * same frame that the RemoteInput is to be sent. 730 */ canSendRemoteInputWithoutBouncer()731 boolean canSendRemoteInputWithoutBouncer(); 732 } 733 734 /** Shows the bouncer if necessary */ 735 public interface BouncerChecker { 736 /** 737 * Shows the bouncer if necessary in order to send a RemoteInput. 738 * 739 * @return {@code true} if the bouncer was shown, {@code false} otherwise 740 */ showBouncerIfNecessary()741 boolean showBouncerIfNecessary(); 742 } 743 744 /** An interface for listening to remote input events that relate to notification lifetime */ 745 public interface RemoteInputListener { 746 /** Called when remote input pending intent has been sent */ onRemoteInputSent(@onNull NotificationEntry entry)747 void onRemoteInputSent(@NonNull NotificationEntry entry); 748 749 /** Called when the notification shade becomes fully closed */ onPanelCollapsed()750 void onPanelCollapsed(); 751 752 /** @return whether lifetime of a notification is being extended by the listener */ isNotificationKeptForRemoteInputHistory(@onNull String key)753 boolean isNotificationKeptForRemoteInputHistory(@NonNull String key); 754 755 /** Called on user interaction to end lifetime extension for history */ releaseNotificationIfKeptForRemoteInputHistory(@onNull NotificationEntry entry)756 void releaseNotificationIfKeptForRemoteInputHistory(@NonNull NotificationEntry entry); 757 758 /** Called when the RemoteInputController is attached to the manager */ setRemoteInputController(@onNull RemoteInputController remoteInputController)759 void setRemoteInputController(@NonNull RemoteInputController remoteInputController); 760 } 761 } 762