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