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 static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY; 19 20 import static com.android.systemui.Dependency.MAIN_HANDLER_NAME; 21 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.app.ActivityManager; 25 import android.app.ActivityOptions; 26 import android.app.KeyguardManager; 27 import android.app.Notification; 28 import android.app.PendingIntent; 29 import android.app.RemoteInput; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.os.Handler; 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.statusbar.notification.NotificationEntryListener; 56 import com.android.systemui.statusbar.notification.NotificationEntryManager; 57 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 58 import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo; 59 import com.android.systemui.statusbar.notification.logging.NotificationLogger; 60 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 61 import com.android.systemui.statusbar.phone.ShadeController; 62 import com.android.systemui.statusbar.policy.RemoteInputView; 63 64 import java.io.FileDescriptor; 65 import java.io.PrintWriter; 66 import java.util.ArrayList; 67 import java.util.Objects; 68 import java.util.Set; 69 70 import javax.inject.Inject; 71 import javax.inject.Named; 72 import javax.inject.Singleton; 73 74 import dagger.Lazy; 75 76 /** 77 * Class for handling remote input state over a set of notifications. This class handles things 78 * like keeping notifications temporarily that were cancelled as a response to a remote input 79 * interaction, keeping track of notifications to remove when NotificationPresenter is collapsed, 80 * and handling clicks on remote views. 81 */ 82 @Singleton 83 public class NotificationRemoteInputManager implements Dumpable { 84 public static final boolean ENABLE_REMOTE_INPUT = 85 SystemProperties.getBoolean("debug.enable_remote_input", true); 86 public static boolean FORCE_REMOTE_INPUT_HISTORY = 87 SystemProperties.getBoolean("debug.force_remoteinput_history", true); 88 private static final boolean DEBUG = false; 89 private static final String TAG = "NotifRemoteInputManager"; 90 91 /** 92 * How long to wait before auto-dismissing a notification that was kept for remote input, and 93 * has now sent a remote input. We auto-dismiss, because the app may not see a reason to cancel 94 * these given that they technically don't exist anymore. We wait a bit in case the app issues 95 * an update. 96 */ 97 private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200; 98 99 /** 100 * Notifications that are already removed but are kept around because we want to show the 101 * remote input history. See {@link RemoteInputHistoryExtender} and 102 * {@link SmartReplyHistoryExtender}. 103 */ 104 protected final ArraySet<String> mKeysKeptForRemoteInputHistory = new ArraySet<>(); 105 106 /** 107 * Notifications that are already removed but are kept around because the remote input is 108 * actively being used (i.e. user is typing in it). See {@link RemoteInputActiveExtender}. 109 */ 110 protected final ArraySet<NotificationEntry> mEntriesKeptForRemoteInputActive = 111 new ArraySet<>(); 112 113 // Dependencies: 114 private final NotificationLockscreenUserManager mLockscreenUserManager; 115 private final SmartReplyController mSmartReplyController; 116 private final NotificationEntryManager mEntryManager; 117 private final Handler mMainHandler; 118 119 private final Lazy<ShadeController> mShadeController; 120 121 protected final Context mContext; 122 private final UserManager mUserManager; 123 private final KeyguardManager mKeyguardManager; 124 125 protected RemoteInputController mRemoteInputController; 126 protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback 127 mNotificationLifetimeFinishedCallback; 128 protected IStatusBarService mBarService; 129 protected Callback mCallback; 130 protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders = new ArrayList<>(); 131 132 private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() { 133 134 @Override 135 public boolean onClickHandler( 136 View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) { 137 mShadeController.get().wakeUpIfDozing(SystemClock.uptimeMillis(), view, 138 "NOTIFICATION_CLICK"); 139 140 if (handleRemoteInput(view, pendingIntent)) { 141 return true; 142 } 143 144 if (DEBUG) { 145 Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent); 146 } 147 logActionClick(view, pendingIntent); 148 // The intent we are sending is for the application, which 149 // won't have permission to immediately start an activity after 150 // the user switches to home. We know it is safe to do at this 151 // point, so make sure new activity switches are now allowed. 152 try { 153 ActivityManager.getService().resumeAppSwitches(); 154 } catch (RemoteException e) { 155 } 156 return mCallback.handleRemoteViewClick(view, pendingIntent, () -> { 157 Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view); 158 options.second.setLaunchWindowingMode( 159 WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY); 160 return RemoteViews.startPendingIntent(view, pendingIntent, options); 161 }); 162 } 163 164 private void logActionClick(View view, PendingIntent actionIntent) { 165 Integer actionIndex = (Integer) 166 view.getTag(com.android.internal.R.id.notification_action_index_tag); 167 if (actionIndex == null) { 168 // Custom action button, not logging. 169 return; 170 } 171 ViewParent parent = view.getParent(); 172 StatusBarNotification statusBarNotification = getNotificationForParent(parent); 173 if (statusBarNotification == null) { 174 Log.w(TAG, "Couldn't determine notification for click."); 175 return; 176 } 177 String key = statusBarNotification.getKey(); 178 int buttonIndex = -1; 179 // If this is a default template, determine the index of the button. 180 if (view.getId() == com.android.internal.R.id.action0 && 181 parent != null && parent instanceof ViewGroup) { 182 ViewGroup actionGroup = (ViewGroup) parent; 183 buttonIndex = actionGroup.indexOfChild(view); 184 } 185 final int count = mEntryManager.getNotificationData().getActiveNotifications().size(); 186 final int rank = mEntryManager.getNotificationData().getRank(key); 187 188 // Notification may be updated before this function is executed, and thus play safe 189 // here and verify that the action object is still the one that where the click happens. 190 Notification.Action[] actions = statusBarNotification.getNotification().actions; 191 if (actions == null || actionIndex >= actions.length) { 192 Log.w(TAG, "statusBarNotification.getNotification().actions is null or invalid"); 193 return; 194 } 195 final Notification.Action action = 196 statusBarNotification.getNotification().actions[actionIndex]; 197 if (!Objects.equals(action.actionIntent, actionIntent)) { 198 Log.w(TAG, "actionIntent does not match"); 199 return; 200 } 201 NotificationVisibility.NotificationLocation location = 202 NotificationLogger.getNotificationLocation( 203 mEntryManager.getNotificationData().get(key)); 204 final NotificationVisibility nv = 205 NotificationVisibility.obtain(key, rank, count, true, location); 206 try { 207 mBarService.onNotificationActionClick(key, buttonIndex, action, nv, false); 208 } catch (RemoteException e) { 209 // Ignore 210 } 211 } 212 213 private StatusBarNotification getNotificationForParent(ViewParent parent) { 214 while (parent != null) { 215 if (parent instanceof ExpandableNotificationRow) { 216 return ((ExpandableNotificationRow) parent).getStatusBarNotification(); 217 } 218 parent = parent.getParent(); 219 } 220 return null; 221 } 222 223 private boolean handleRemoteInput(View view, PendingIntent pendingIntent) { 224 if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) { 225 return true; 226 } 227 228 Object tag = view.getTag(com.android.internal.R.id.remote_input_tag); 229 RemoteInput[] inputs = null; 230 if (tag instanceof RemoteInput[]) { 231 inputs = (RemoteInput[]) tag; 232 } 233 234 if (inputs == null) { 235 return false; 236 } 237 238 RemoteInput input = null; 239 240 for (RemoteInput i : inputs) { 241 if (i.getAllowFreeFormInput()) { 242 input = i; 243 } 244 } 245 246 if (input == null) { 247 return false; 248 } 249 250 return activateRemoteInput(view, inputs, input, pendingIntent, 251 null /* editedSuggestionInfo */); 252 } 253 }; 254 255 @Inject NotificationRemoteInputManager( Context context, NotificationLockscreenUserManager lockscreenUserManager, SmartReplyController smartReplyController, NotificationEntryManager notificationEntryManager, Lazy<ShadeController> shadeController, @Named(MAIN_HANDLER_NAME) Handler mainHandler)256 public NotificationRemoteInputManager( 257 Context context, 258 NotificationLockscreenUserManager lockscreenUserManager, 259 SmartReplyController smartReplyController, 260 NotificationEntryManager notificationEntryManager, 261 Lazy<ShadeController> shadeController, 262 @Named(MAIN_HANDLER_NAME) Handler mainHandler) { 263 mContext = context; 264 mLockscreenUserManager = lockscreenUserManager; 265 mSmartReplyController = smartReplyController; 266 mEntryManager = notificationEntryManager; 267 mShadeController = shadeController; 268 mMainHandler = mainHandler; 269 mBarService = IStatusBarService.Stub.asInterface( 270 ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 271 mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); 272 addLifetimeExtenders(); 273 mKeyguardManager = context.getSystemService(KeyguardManager.class); 274 275 notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() { 276 @Override 277 public void onPreEntryUpdated(NotificationEntry entry) { 278 // Mark smart replies as sent whenever a notification is updated - otherwise the 279 // smart replies are never marked as sent. 280 mSmartReplyController.stopSending(entry); 281 } 282 283 @Override 284 public void onEntryRemoved( 285 @Nullable NotificationEntry entry, 286 NotificationVisibility visibility, 287 boolean removedByUser) { 288 // We're removing the notification, the smart controller can forget about it. 289 mSmartReplyController.stopSending(entry); 290 291 if (removedByUser && entry != null) { 292 onPerformRemoveNotification(entry, entry.key); 293 } 294 } 295 }); 296 } 297 298 /** Initializes this component with the provided dependencies. */ setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate)299 public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) { 300 mCallback = callback; 301 mRemoteInputController = new RemoteInputController(delegate); 302 mRemoteInputController.addCallback(new RemoteInputController.Callback() { 303 @Override 304 public void onRemoteInputSent(NotificationEntry entry) { 305 if (FORCE_REMOTE_INPUT_HISTORY 306 && isNotificationKeptForRemoteInputHistory(entry.key)) { 307 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key); 308 } else if (mEntriesKeptForRemoteInputActive.contains(entry)) { 309 // We're currently holding onto this notification, but from the apps point of 310 // view it is already canceled, so we'll need to cancel it on the apps behalf 311 // after sending - unless the app posts an update in the mean time, so wait a 312 // bit. 313 mMainHandler.postDelayed(() -> { 314 if (mEntriesKeptForRemoteInputActive.remove(entry)) { 315 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key); 316 } 317 }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY); 318 } 319 try { 320 mBarService.onNotificationDirectReplied(entry.notification.getKey()); 321 if (entry.editedSuggestionInfo != null) { 322 boolean modifiedBeforeSending = 323 !TextUtils.equals(entry.remoteInputText, 324 entry.editedSuggestionInfo.originalText); 325 mBarService.onNotificationSmartReplySent( 326 entry.notification.getKey(), 327 entry.editedSuggestionInfo.index, 328 entry.editedSuggestionInfo.originalText, 329 NotificationLogger 330 .getNotificationLocation(entry) 331 .toMetricsEventEnum(), 332 modifiedBeforeSending); 333 } 334 } catch (RemoteException e) { 335 // Nothing to do, system going down 336 } 337 } 338 }); 339 mSmartReplyController.setCallback((entry, reply) -> { 340 StatusBarNotification newSbn = 341 rebuildNotificationWithRemoteInput(entry, reply, true /* showSpinner */); 342 mEntryManager.updateNotification(newSbn, null /* ranking */); 343 }); 344 } 345 346 /** 347 * Activates a given {@link RemoteInput} 348 * 349 * @param view The view of the action button or suggestion chip that was tapped. 350 * @param inputs The remote inputs that need to be sent to the app. 351 * @param input The remote input that needs to be activated. 352 * @param pendingIntent The pending intent to be sent to the app. 353 * @param editedSuggestionInfo The smart reply that should be inserted in the remote input, or 354 * {@code null} if the user is not editing a smart reply. 355 * @return Whether the {@link RemoteInput} was activated. 356 */ activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input, PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo)357 public boolean activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input, 358 PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo) { 359 360 ViewParent p = view.getParent(); 361 RemoteInputView riv = null; 362 ExpandableNotificationRow row = null; 363 while (p != null) { 364 if (p instanceof View) { 365 View pv = (View) p; 366 if (pv.isRootNamespace()) { 367 riv = findRemoteInputView(pv); 368 row = (ExpandableNotificationRow) pv.getTag(R.id.row_tag_for_content_view); 369 break; 370 } 371 } 372 p = p.getParent(); 373 } 374 375 if (row == null) { 376 return false; 377 } 378 379 row.setUserExpanded(true); 380 381 if (!mLockscreenUserManager.shouldAllowLockscreenRemoteInput()) { 382 final int userId = pendingIntent.getCreatorUserHandle().getIdentifier(); 383 if (mLockscreenUserManager.isLockscreenPublicMode(userId)) { 384 mCallback.onLockedRemoteInput(row, view); 385 return true; 386 } 387 if (mUserManager.getUserInfo(userId).isManagedProfile() 388 && mKeyguardManager.isDeviceLocked(userId)) { 389 mCallback.onLockedWorkRemoteInput(userId, row, view); 390 return true; 391 } 392 } 393 394 if (riv == null) { 395 riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild()); 396 if (riv == null) { 397 return false; 398 } 399 } 400 if (riv == row.getPrivateLayout().getExpandedRemoteInput() 401 && !row.getPrivateLayout().getExpandedChild().isShown()) { 402 // The expanded layout is selected, but it's not shown yet, let's wait on it to 403 // show before we do the animation. 404 mCallback.onMakeExpandedVisibleForRemoteInput(row, view); 405 return true; 406 } 407 408 int width = view.getWidth(); 409 if (view instanceof TextView) { 410 // Center the reveal on the text which might be off-center from the TextView 411 TextView tv = (TextView) view; 412 if (tv.getLayout() != null) { 413 int innerWidth = (int) tv.getLayout().getLineWidth(0); 414 innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight(); 415 width = Math.min(width, innerWidth); 416 } 417 } 418 int cx = view.getLeft() + width / 2; 419 int cy = view.getTop() + view.getHeight() / 2; 420 int w = riv.getWidth(); 421 int h = riv.getHeight(); 422 int r = Math.max( 423 Math.max(cx + cy, cx + (h - cy)), 424 Math.max((w - cx) + cy, (w - cx) + (h - cy))); 425 426 riv.setRevealParameters(cx, cy, r); 427 riv.setPendingIntent(pendingIntent); 428 riv.setRemoteInput(inputs, input, editedSuggestionInfo); 429 riv.focusAnimated(); 430 431 return true; 432 } 433 findRemoteInputView(View v)434 private RemoteInputView findRemoteInputView(View v) { 435 if (v == null) { 436 return null; 437 } 438 return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG); 439 } 440 441 /** 442 * Adds all the notification lifetime extenders. Each extender represents a reason for the 443 * NotificationRemoteInputManager to keep a notification lifetime extended. 444 */ addLifetimeExtenders()445 protected void addLifetimeExtenders() { 446 mLifetimeExtenders.add(new RemoteInputHistoryExtender()); 447 mLifetimeExtenders.add(new SmartReplyHistoryExtender()); 448 mLifetimeExtenders.add(new RemoteInputActiveExtender()); 449 } 450 getLifetimeExtenders()451 public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() { 452 return mLifetimeExtenders; 453 } 454 getController()455 public RemoteInputController getController() { 456 return mRemoteInputController; 457 } 458 459 @VisibleForTesting onPerformRemoveNotification(NotificationEntry entry, final String key)460 void onPerformRemoveNotification(NotificationEntry entry, final String key) { 461 if (mKeysKeptForRemoteInputHistory.contains(key)) { 462 mKeysKeptForRemoteInputHistory.remove(key); 463 } 464 if (mRemoteInputController.isRemoteInputActive(entry)) { 465 mRemoteInputController.removeRemoteInput(entry, null); 466 } 467 } 468 onPanelCollapsed()469 public void onPanelCollapsed() { 470 for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) { 471 NotificationEntry entry = mEntriesKeptForRemoteInputActive.valueAt(i); 472 mRemoteInputController.removeRemoteInput(entry, null); 473 if (mNotificationLifetimeFinishedCallback != null) { 474 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key); 475 } 476 } 477 mEntriesKeptForRemoteInputActive.clear(); 478 } 479 isNotificationKeptForRemoteInputHistory(String key)480 public boolean isNotificationKeptForRemoteInputHistory(String key) { 481 return mKeysKeptForRemoteInputHistory.contains(key); 482 } 483 shouldKeepForRemoteInputHistory(NotificationEntry entry)484 public boolean shouldKeepForRemoteInputHistory(NotificationEntry entry) { 485 if (!FORCE_REMOTE_INPUT_HISTORY) { 486 return false; 487 } 488 return (mRemoteInputController.isSpinning(entry.key) || entry.hasJustSentRemoteInput()); 489 } 490 shouldKeepForSmartReplyHistory(NotificationEntry entry)491 public boolean shouldKeepForSmartReplyHistory(NotificationEntry entry) { 492 if (!FORCE_REMOTE_INPUT_HISTORY) { 493 return false; 494 } 495 return mSmartReplyController.isSendingSmartReply(entry.key); 496 } 497 checkRemoteInputOutside(MotionEvent event)498 public void checkRemoteInputOutside(MotionEvent event) { 499 if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar 500 && event.getX() == 0 && event.getY() == 0 // a touch outside both bars 501 && mRemoteInputController.isRemoteInputActive()) { 502 mRemoteInputController.closeRemoteInputs(); 503 } 504 } 505 506 @VisibleForTesting rebuildNotificationForCanceledSmartReplies( NotificationEntry entry)507 StatusBarNotification rebuildNotificationForCanceledSmartReplies( 508 NotificationEntry entry) { 509 return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */, 510 false /* showSpinner */); 511 } 512 513 @VisibleForTesting rebuildNotificationWithRemoteInput(NotificationEntry entry, CharSequence remoteInputText, boolean showSpinner)514 StatusBarNotification rebuildNotificationWithRemoteInput(NotificationEntry entry, 515 CharSequence remoteInputText, boolean showSpinner) { 516 StatusBarNotification sbn = entry.notification; 517 518 Notification.Builder b = Notification.Builder 519 .recoverBuilder(mContext, sbn.getNotification().clone()); 520 if (remoteInputText != null) { 521 CharSequence[] oldHistory = sbn.getNotification().extras 522 .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY); 523 CharSequence[] newHistory; 524 if (oldHistory == null) { 525 newHistory = new CharSequence[1]; 526 } else { 527 newHistory = new CharSequence[oldHistory.length + 1]; 528 System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length); 529 } 530 newHistory[0] = String.valueOf(remoteInputText); 531 b.setRemoteInputHistory(newHistory); 532 } 533 b.setShowRemoteInputSpinner(showSpinner); 534 b.setHideSmartReplies(true); 535 536 Notification newNotification = b.build(); 537 538 // Undo any compatibility view inflation 539 newNotification.contentView = sbn.getNotification().contentView; 540 newNotification.bigContentView = sbn.getNotification().bigContentView; 541 newNotification.headsUpContentView = sbn.getNotification().headsUpContentView; 542 543 return new StatusBarNotification( 544 sbn.getPackageName(), 545 sbn.getOpPkg(), 546 sbn.getId(), 547 sbn.getTag(), 548 sbn.getUid(), 549 sbn.getInitialPid(), 550 newNotification, 551 sbn.getUser(), 552 sbn.getOverrideGroupKey(), 553 sbn.getPostTime()); 554 } 555 556 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)557 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 558 pw.println("NotificationRemoteInputManager state:"); 559 pw.print(" mKeysKeptForRemoteInputHistory: "); 560 pw.println(mKeysKeptForRemoteInputHistory); 561 pw.print(" mEntriesKeptForRemoteInputActive: "); 562 pw.println(mEntriesKeptForRemoteInputActive); 563 } 564 bindRow(ExpandableNotificationRow row)565 public void bindRow(ExpandableNotificationRow row) { 566 row.setRemoteInputController(mRemoteInputController); 567 row.setRemoteViewClickHandler(mOnClickHandler); 568 } 569 570 @VisibleForTesting getEntriesKeptForRemoteInputActive()571 public Set<NotificationEntry> getEntriesKeptForRemoteInputActive() { 572 return mEntriesKeptForRemoteInputActive; 573 } 574 575 /** 576 * NotificationRemoteInputManager has multiple reasons to keep notification lifetime extended 577 * so we implement multiple NotificationLifetimeExtenders 578 */ 579 protected abstract class RemoteInputExtender implements NotificationLifetimeExtender { 580 @Override setCallback(NotificationSafeToRemoveCallback callback)581 public void setCallback(NotificationSafeToRemoveCallback callback) { 582 if (mNotificationLifetimeFinishedCallback == null) { 583 mNotificationLifetimeFinishedCallback = callback; 584 } 585 } 586 } 587 588 /** 589 * Notification is kept alive as it was cancelled in response to a remote input interaction. 590 * This allows us to show what you replied and allows you to continue typing into it. 591 */ 592 protected class RemoteInputHistoryExtender extends RemoteInputExtender { 593 @Override shouldExtendLifetime(@onNull NotificationEntry entry)594 public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) { 595 return shouldKeepForRemoteInputHistory(entry); 596 } 597 598 @Override setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend)599 public void setShouldManageLifetime(NotificationEntry entry, 600 boolean shouldExtend) { 601 if (shouldExtend) { 602 CharSequence remoteInputText = entry.remoteInputText; 603 if (TextUtils.isEmpty(remoteInputText)) { 604 remoteInputText = entry.remoteInputTextWhenReset; 605 } 606 StatusBarNotification newSbn = rebuildNotificationWithRemoteInput(entry, 607 remoteInputText, false /* showSpinner */); 608 entry.onRemoteInputInserted(); 609 610 if (newSbn == null) { 611 return; 612 } 613 614 mEntryManager.updateNotification(newSbn, null); 615 616 // Ensure the entry hasn't already been removed. This can happen if there is an 617 // inflation exception while updating the remote history 618 if (entry.isRemoved()) { 619 return; 620 } 621 622 if (Log.isLoggable(TAG, Log.DEBUG)) { 623 Log.d(TAG, "Keeping notification around after sending remote input " 624 + entry.key); 625 } 626 627 mKeysKeptForRemoteInputHistory.add(entry.key); 628 } else { 629 mKeysKeptForRemoteInputHistory.remove(entry.key); 630 } 631 } 632 } 633 634 /** 635 * Notification is kept alive for smart reply history. Similar to REMOTE_INPUT_HISTORY but with 636 * {@link SmartReplyController} specific logic 637 */ 638 protected class SmartReplyHistoryExtender extends RemoteInputExtender { 639 @Override shouldExtendLifetime(@onNull NotificationEntry entry)640 public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) { 641 return shouldKeepForSmartReplyHistory(entry); 642 } 643 644 @Override setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend)645 public void setShouldManageLifetime(NotificationEntry entry, 646 boolean shouldExtend) { 647 if (shouldExtend) { 648 StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry); 649 650 if (newSbn == null) { 651 return; 652 } 653 654 mEntryManager.updateNotification(newSbn, null); 655 656 if (entry.isRemoved()) { 657 return; 658 } 659 660 if (Log.isLoggable(TAG, Log.DEBUG)) { 661 Log.d(TAG, "Keeping notification around after sending smart reply " 662 + entry.key); 663 } 664 665 mKeysKeptForRemoteInputHistory.add(entry.key); 666 } else { 667 mKeysKeptForRemoteInputHistory.remove(entry.key); 668 mSmartReplyController.stopSending(entry); 669 } 670 } 671 } 672 673 /** 674 * Notification is kept alive because the user is still using the remote input 675 */ 676 protected class RemoteInputActiveExtender extends RemoteInputExtender { 677 @Override shouldExtendLifetime(@onNull NotificationEntry entry)678 public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) { 679 return mRemoteInputController.isRemoteInputActive(entry); 680 } 681 682 @Override setShouldManageLifetime(NotificationEntry entry, boolean shouldExtend)683 public void setShouldManageLifetime(NotificationEntry entry, 684 boolean shouldExtend) { 685 if (shouldExtend) { 686 if (Log.isLoggable(TAG, Log.DEBUG)) { 687 Log.d(TAG, "Keeping notification around while remote input active " 688 + entry.key); 689 } 690 mEntriesKeptForRemoteInputActive.add(entry); 691 } else { 692 mEntriesKeptForRemoteInputActive.remove(entry); 693 } 694 } 695 } 696 697 /** 698 * Callback for various remote input related events, or for providing information that 699 * NotificationRemoteInputManager needs to know to decide what to do. 700 */ 701 public interface Callback { 702 703 /** 704 * Called when remote input was activated but the device is locked. 705 * 706 * @param row 707 * @param clicked 708 */ onLockedRemoteInput(ExpandableNotificationRow row, View clicked)709 void onLockedRemoteInput(ExpandableNotificationRow row, View clicked); 710 711 /** 712 * Called when remote input was activated but the device is locked and in a managed profile. 713 * 714 * @param userId 715 * @param row 716 * @param clicked 717 */ onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked)718 void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked); 719 720 /** 721 * Called when a row should be made expanded for the purposes of remote input. 722 * 723 * @param row 724 * @param clickedView 725 */ onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView)726 void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView); 727 728 /** 729 * Return whether or not remote input should be handled for this view. 730 * 731 * @param view 732 * @param pendingIntent 733 * @return true iff the remote input should be handled 734 */ shouldHandleRemoteInput(View view, PendingIntent pendingIntent)735 boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent); 736 737 /** 738 * Performs any special handling for a remote view click. The default behaviour can be 739 * called through the defaultHandler parameter. 740 * 741 * @param view 742 * @param pendingIntent 743 * @param defaultHandler 744 * @return true iff the click was handled 745 */ handleRemoteViewClick(View view, PendingIntent pendingIntent, ClickHandler defaultHandler)746 boolean handleRemoteViewClick(View view, PendingIntent pendingIntent, 747 ClickHandler defaultHandler); 748 } 749 750 /** 751 * Helper interface meant for passing the default on click behaviour to NotificationPresenter, 752 * so it may do its own handling before invoking the default behaviour. 753 */ 754 public interface ClickHandler { 755 /** 756 * Tries to handle a click on a remote view. 757 * 758 * @return true iff the click was handled 759 */ handleClick()760 boolean handleClick(); 761 } 762 } 763