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