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