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