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