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