blob: 9391737fe23c565055eb8c9434cc0f40fba29574 [file] [log] [blame]
Eliot Courtneye77edea2017-11-15 14:25:21 +09001/*
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 */
16package com.android.systemui.statusbar;
17
18import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;
19
Kevina5ff1fa2018-08-21 16:35:48 -070020import android.annotation.NonNull;
Eliot Courtneye77edea2017-11-15 14:25:21 +090021import android.app.ActivityManager;
Sunny Goyal02794532018-08-22 15:18:37 -070022import android.app.ActivityOptions;
Jason Monk297c04e2018-08-23 17:16:59 -040023import android.app.KeyguardManager;
Kevina5ff1fa2018-08-21 16:35:48 -070024import android.app.Notification;
Eliot Courtneye77edea2017-11-15 14:25:21 +090025import android.app.PendingIntent;
26import android.app.RemoteInput;
27import android.content.Context;
28import android.content.Intent;
29import android.os.RemoteException;
30import android.os.ServiceManager;
31import android.os.SystemClock;
32import android.os.SystemProperties;
33import android.os.UserManager;
34import android.service.notification.StatusBarNotification;
Kevina5ff1fa2018-08-21 16:35:48 -070035import android.text.TextUtils;
Eliot Courtneye77edea2017-11-15 14:25:21 +090036import android.util.ArraySet;
37import android.util.Log;
Sunny Goyal43c97042018-08-23 15:21:26 -070038import android.util.Pair;
Eliot Courtneye77edea2017-11-15 14:25:21 +090039import android.view.MotionEvent;
40import android.view.View;
41import android.view.ViewGroup;
42import android.view.ViewParent;
43import android.widget.RemoteViews;
44import android.widget.TextView;
45
46import com.android.internal.annotations.VisibleForTesting;
47import com.android.internal.statusbar.IStatusBarService;
Dieter Hsud39f0d52018-04-14 02:08:30 +080048import com.android.internal.statusbar.NotificationVisibility;
Eliot Courtney6c313d32017-12-14 19:57:51 +090049import com.android.systemui.Dependency;
Eliot Courtneye77edea2017-11-15 14:25:21 +090050import com.android.systemui.Dumpable;
Rohan Shah20790b82018-07-02 17:21:04 -070051import com.android.systemui.statusbar.notification.NotificationData;
52import com.android.systemui.statusbar.notification.NotificationEntryManager;
53import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
Jason Monk297c04e2018-08-23 17:16:59 -040054import com.android.systemui.statusbar.phone.ShadeController;
Eliot Courtneye77edea2017-11-15 14:25:21 +090055import com.android.systemui.statusbar.policy.RemoteInputView;
56
57import java.io.FileDescriptor;
58import java.io.PrintWriter;
Kevina5ff1fa2018-08-21 16:35:48 -070059import java.util.ArrayList;
Eliot Courtneye77edea2017-11-15 14:25:21 +090060import java.util.Set;
61
62/**
63 * Class for handling remote input state over a set of notifications. This class handles things
64 * like keeping notifications temporarily that were cancelled as a response to a remote input
65 * interaction, keeping track of notifications to remove when NotificationPresenter is collapsed,
66 * and handling clicks on remote views.
67 */
68public class NotificationRemoteInputManager implements Dumpable {
69 public static final boolean ENABLE_REMOTE_INPUT =
70 SystemProperties.getBoolean("debug.enable_remote_input", true);
Kevina5ff1fa2018-08-21 16:35:48 -070071 public static boolean FORCE_REMOTE_INPUT_HISTORY =
Eliot Courtneye77edea2017-11-15 14:25:21 +090072 SystemProperties.getBoolean("debug.force_remoteinput_history", true);
73 private static final boolean DEBUG = false;
Kevina5ff1fa2018-08-21 16:35:48 -070074 private static final String TAG = "NotifRemoteInputManager";
Eliot Courtneye77edea2017-11-15 14:25:21 +090075
76 /**
77 * How long to wait before auto-dismissing a notification that was kept for remote input, and
78 * has now sent a remote input. We auto-dismiss, because the app may not see a reason to cancel
79 * these given that they technically don't exist anymore. We wait a bit in case the app issues
80 * an update.
81 */
82 private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
83
Kevina5ff1fa2018-08-21 16:35:48 -070084 /**
85 * Notifications that are already removed but are kept around because we want to show the
86 * remote input history. See {@link RemoteInputHistoryExtender} and
87 * {@link SmartReplyHistoryExtender}.
88 */
89 protected final ArraySet<String> mKeysKeptForRemoteInputHistory = new ArraySet<>();
90
91 /**
92 * Notifications that are already removed but are kept around because the remote input is
93 * actively being used (i.e. user is typing in it). See {@link RemoteInputActiveExtender}.
94 */
95 protected final ArraySet<NotificationData.Entry> mEntriesKeptForRemoteInputActive =
Eliot Courtneye77edea2017-11-15 14:25:21 +090096 new ArraySet<>();
Eliot Courtney6c313d32017-12-14 19:57:51 +090097
98 // Dependencies:
99 protected final NotificationLockscreenUserManager mLockscreenUserManager =
100 Dependency.get(NotificationLockscreenUserManager.class);
Kevina5ff1fa2018-08-21 16:35:48 -0700101 protected final SmartReplyController mSmartReplyController =
102 Dependency.get(SmartReplyController.class);
Jason Monk297c04e2018-08-23 17:16:59 -0400103 private final NotificationEntryManager mEntryManager
104 = Dependency.get(NotificationEntryManager.class);
105
106 // Lazy
107 private ShadeController mShadeController;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900108
Eliot Courtneye77edea2017-11-15 14:25:21 +0900109 protected final Context mContext;
110 private final UserManager mUserManager;
Jason Monk297c04e2018-08-23 17:16:59 -0400111 private final KeyguardManager mKeyguardManager;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900112
113 protected RemoteInputController mRemoteInputController;
Kevina5ff1fa2018-08-21 16:35:48 -0700114 protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback
115 mNotificationLifetimeFinishedCallback;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900116 protected IStatusBarService mBarService;
117 protected Callback mCallback;
Kevina5ff1fa2018-08-21 16:35:48 -0700118 protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
Eliot Courtneye77edea2017-11-15 14:25:21 +0900119
120 private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() {
121
122 @Override
123 public boolean onClickHandler(
Sunny Goyal43c97042018-08-23 15:21:26 -0700124 View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) {
Jason Monk297c04e2018-08-23 17:16:59 -0400125 getShadeController().wakeUpIfDozing(SystemClock.uptimeMillis(), view);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900126
127 if (handleRemoteInput(view, pendingIntent)) {
128 return true;
129 }
130
131 if (DEBUG) {
132 Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
133 }
134 logActionClick(view);
135 // The intent we are sending is for the application, which
136 // won't have permission to immediately start an activity after
137 // the user switches to home. We know it is safe to do at this
138 // point, so make sure new activity switches are now allowed.
139 try {
140 ActivityManager.getService().resumeAppSwitches();
141 } catch (RemoteException e) {
142 }
yoshiki iguchi67c166d2018-11-26 13:11:36 +0900143 return mCallback.handleRemoteViewClick(view, pendingIntent, () -> {
Sunny Goyal43c97042018-08-23 15:21:26 -0700144 Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
145 options.second.setLaunchWindowingMode(
146 WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
147 return RemoteViews.startPendingIntent(view, pendingIntent, options);
148 });
Eliot Courtneye77edea2017-11-15 14:25:21 +0900149 }
150
151 private void logActionClick(View view) {
Tony Mak7d4b3a52018-11-27 17:29:36 +0000152 Integer actionIndex = (Integer)
153 view.getTag(com.android.internal.R.id.notification_action_index_tag);
154 if (actionIndex == null) {
155 Log.e(TAG, "Couldn't retrieve the actionIndex from the clicked button");
156 return;
157 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900158 ViewParent parent = view.getParent();
Tony Mak7d4b3a52018-11-27 17:29:36 +0000159 StatusBarNotification statusBarNotification = getNotificationForParent(parent);
160 if (statusBarNotification == null) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900161 Log.w(TAG, "Couldn't determine notification for click.");
162 return;
163 }
Tony Mak7d4b3a52018-11-27 17:29:36 +0000164 String key = statusBarNotification.getKey();
165 int buttonIndex = -1;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900166 // If this is a default template, determine the index of the button.
167 if (view.getId() == com.android.internal.R.id.action0 &&
168 parent != null && parent instanceof ViewGroup) {
169 ViewGroup actionGroup = (ViewGroup) parent;
Tony Mak7d4b3a52018-11-27 17:29:36 +0000170 buttonIndex = actionGroup.indexOfChild(view);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900171 }
Dieter Hsud39f0d52018-04-14 02:08:30 +0800172 final int count = mEntryManager.getNotificationData().getActiveNotifications().size();
173 final int rank = mEntryManager.getNotificationData().getRank(key);
Tony Mak7d4b3a52018-11-27 17:29:36 +0000174 final Notification.Action action =
175 statusBarNotification.getNotification().actions[actionIndex];
Dieter Hsud39f0d52018-04-14 02:08:30 +0800176 final NotificationVisibility nv = NotificationVisibility.obtain(key, rank, count, true);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900177 try {
Tony Mak7d4b3a52018-11-27 17:29:36 +0000178 mBarService.onNotificationActionClick(key, buttonIndex, action, nv, false);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900179 } catch (RemoteException e) {
180 // Ignore
181 }
182 }
183
Tony Mak7d4b3a52018-11-27 17:29:36 +0000184 private StatusBarNotification getNotificationForParent(ViewParent parent) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900185 while (parent != null) {
186 if (parent instanceof ExpandableNotificationRow) {
Tony Mak7d4b3a52018-11-27 17:29:36 +0000187 return ((ExpandableNotificationRow) parent).getStatusBarNotification();
Eliot Courtneye77edea2017-11-15 14:25:21 +0900188 }
189 parent = parent.getParent();
190 }
191 return null;
192 }
193
Eliot Courtneye77edea2017-11-15 14:25:21 +0900194 private boolean handleRemoteInput(View view, PendingIntent pendingIntent) {
195 if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) {
196 return true;
197 }
198
199 Object tag = view.getTag(com.android.internal.R.id.remote_input_tag);
200 RemoteInput[] inputs = null;
201 if (tag instanceof RemoteInput[]) {
202 inputs = (RemoteInput[]) tag;
203 }
204
205 if (inputs == null) {
206 return false;
207 }
208
209 RemoteInput input = null;
210
211 for (RemoteInput i : inputs) {
212 if (i.getAllowFreeFormInput()) {
213 input = i;
214 }
215 }
216
217 if (input == null) {
218 return false;
219 }
220
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000221 return activateRemoteInput(view, inputs, input, pendingIntent);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900222 }
223 };
224
Jason Monk297c04e2018-08-23 17:16:59 -0400225 private ShadeController getShadeController() {
226 if (mShadeController == null) {
227 mShadeController = Dependency.get(ShadeController.class);
228 }
229 return mShadeController;
230 }
231
Eliot Courtney6c313d32017-12-14 19:57:51 +0900232 public NotificationRemoteInputManager(Context context) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900233 mContext = context;
234 mBarService = IStatusBarService.Stub.asInterface(
235 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
236 mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
Kevina5ff1fa2018-08-21 16:35:48 -0700237 addLifetimeExtenders();
Jason Monk297c04e2018-08-23 17:16:59 -0400238 mKeyguardManager = context.getSystemService(KeyguardManager.class);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900239 }
240
Gus Prevas21437b32018-12-05 10:36:13 -0500241 /** Initializes this component with the provided dependencies. */
242 public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900243 mCallback = callback;
244 mRemoteInputController = new RemoteInputController(delegate);
245 mRemoteInputController.addCallback(new RemoteInputController.Callback() {
246 @Override
247 public void onRemoteInputSent(NotificationData.Entry entry) {
Kenny Guy8cc15d22018-05-09 09:50:55 +0100248 if (FORCE_REMOTE_INPUT_HISTORY
Kevina5ff1fa2018-08-21 16:35:48 -0700249 && isNotificationKeptForRemoteInputHistory(entry.key)) {
250 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
251 } else if (mEntriesKeptForRemoteInputActive.contains(entry)) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900252 // We're currently holding onto this notification, but from the apps point of
253 // view it is already canceled, so we'll need to cancel it on the apps behalf
254 // after sending - unless the app posts an update in the mean time, so wait a
255 // bit.
Jason Monk297c04e2018-08-23 17:16:59 -0400256 Dependency.get(Dependency.MAIN_HANDLER).postDelayed(() -> {
Kevina5ff1fa2018-08-21 16:35:48 -0700257 if (mEntriesKeptForRemoteInputActive.remove(entry)) {
258 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900259 }
260 }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
261 }
Amith Yamasani129b4812018-02-08 13:55:59 -0800262 try {
263 mBarService.onNotificationDirectReplied(entry.notification.getKey());
264 } catch (RemoteException e) {
265 // Nothing to do, system going down
266 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900267 }
268 });
Kevina5ff1fa2018-08-21 16:35:48 -0700269 mSmartReplyController.setCallback((entry, reply) -> {
270 StatusBarNotification newSbn =
271 rebuildNotificationWithRemoteInput(entry, reply, true /* showSpinner */);
272 mEntryManager.updateNotification(newSbn, null /* ranking */);
273 });
274 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900275
Kevina5ff1fa2018-08-21 16:35:48 -0700276 /**
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000277 * Activates a given {@link RemoteInput}
278 *
279 * @param view The view of the action button or suggestion chip that was tapped.
280 * @param inputs The remote inputs that need to be sent to the app.
281 * @param input The remote input that needs to be activated.
282 * @param pendingIntent The pending intent to be sent to the app.
283 * @return Whether the {@link RemoteInput} was activated.
284 */
285 public boolean activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input,
286 PendingIntent pendingIntent) {
287
288 ViewParent p = view.getParent();
289 RemoteInputView riv = null;
290 while (p != null) {
291 if (p instanceof View) {
292 View pv = (View) p;
293 if (pv.isRootNamespace()) {
294 riv = findRemoteInputView(pv);
295 break;
296 }
297 }
298 p = p.getParent();
299 }
300 ExpandableNotificationRow row = null;
301 while (p != null) {
302 if (p instanceof ExpandableNotificationRow) {
303 row = (ExpandableNotificationRow) p;
304 break;
305 }
306 p = p.getParent();
307 }
308
309 if (row == null) {
310 return false;
311 }
312
313 row.setUserExpanded(true);
314
315 if (!mLockscreenUserManager.shouldAllowLockscreenRemoteInput()) {
316 final int userId = pendingIntent.getCreatorUserHandle().getIdentifier();
317 if (mLockscreenUserManager.isLockscreenPublicMode(userId)) {
318 mCallback.onLockedRemoteInput(row, view);
319 return true;
320 }
321 if (mUserManager.getUserInfo(userId).isManagedProfile()
322 && mKeyguardManager.isDeviceLocked(userId)) {
323 mCallback.onLockedWorkRemoteInput(userId, row, view);
324 return true;
325 }
326 }
327
328 if (riv == null) {
329 riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild());
330 if (riv == null) {
331 return false;
332 }
333 if (!row.getPrivateLayout().getExpandedChild().isShown()) {
334 mCallback.onMakeExpandedVisibleForRemoteInput(row, view);
335 return true;
336 }
337 }
338
339 int width = view.getWidth();
340 if (view instanceof TextView) {
341 // Center the reveal on the text which might be off-center from the TextView
342 TextView tv = (TextView) view;
343 if (tv.getLayout() != null) {
344 int innerWidth = (int) tv.getLayout().getLineWidth(0);
345 innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight();
346 width = Math.min(width, innerWidth);
347 }
348 }
349 int cx = view.getLeft() + width / 2;
350 int cy = view.getTop() + view.getHeight() / 2;
351 int w = riv.getWidth();
352 int h = riv.getHeight();
353 int r = Math.max(
354 Math.max(cx + cy, cx + (h - cy)),
355 Math.max((w - cx) + cy, (w - cx) + (h - cy)));
356
357 riv.setRevealParameters(cx, cy, r);
358 riv.setPendingIntent(pendingIntent);
359 riv.setRemoteInput(inputs, input);
360 riv.focusAnimated();
361
362 return true;
363 }
364
365 private RemoteInputView findRemoteInputView(View v) {
366 if (v == null) {
367 return null;
368 }
369 return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG);
370 }
371
372 /**
Kevina5ff1fa2018-08-21 16:35:48 -0700373 * Adds all the notification lifetime extenders. Each extender represents a reason for the
374 * NotificationRemoteInputManager to keep a notification lifetime extended.
375 */
376 protected void addLifetimeExtenders() {
377 mLifetimeExtenders.add(new RemoteInputHistoryExtender());
378 mLifetimeExtenders.add(new SmartReplyHistoryExtender());
379 mLifetimeExtenders.add(new RemoteInputActiveExtender());
380 }
381
382 public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() {
383 return mLifetimeExtenders;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900384 }
385
386 public RemoteInputController getController() {
387 return mRemoteInputController;
388 }
389
Eliot Courtneye77edea2017-11-15 14:25:21 +0900390 public void onPerformRemoveNotification(StatusBarNotification n,
391 NotificationData.Entry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700392 if (mKeysKeptForRemoteInputHistory.contains(n.getKey())) {
393 mKeysKeptForRemoteInputHistory.remove(n.getKey());
394 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900395 if (mRemoteInputController.isRemoteInputActive(entry)) {
396 mRemoteInputController.removeRemoteInput(entry, null);
397 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900398 }
399
Kevina5ff1fa2018-08-21 16:35:48 -0700400 public void onPanelCollapsed() {
401 for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) {
402 NotificationData.Entry entry = mEntriesKeptForRemoteInputActive.valueAt(i);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900403 mRemoteInputController.removeRemoteInput(entry, null);
Kevina5ff1fa2018-08-21 16:35:48 -0700404 if (mNotificationLifetimeFinishedCallback != null) {
405 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
406 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900407 }
Kevina5ff1fa2018-08-21 16:35:48 -0700408 mEntriesKeptForRemoteInputActive.clear();
409 }
410
411 public boolean isNotificationKeptForRemoteInputHistory(String key) {
412 return mKeysKeptForRemoteInputHistory.contains(key);
413 }
414
415 public boolean shouldKeepForRemoteInputHistory(NotificationData.Entry entry) {
Evan Laird94492852018-10-25 13:43:01 -0400416 if (entry.isDismissed()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700417 return false;
418 }
419 if (!FORCE_REMOTE_INPUT_HISTORY) {
420 return false;
421 }
422 return (mRemoteInputController.isSpinning(entry.key) || entry.hasJustSentRemoteInput());
423 }
424
425 public boolean shouldKeepForSmartReplyHistory(NotificationData.Entry entry) {
Evan Laird94492852018-10-25 13:43:01 -0400426 if (entry.isDismissed()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700427 return false;
428 }
429 if (!FORCE_REMOTE_INPUT_HISTORY) {
430 return false;
431 }
432 return mSmartReplyController.isSendingSmartReply(entry.key);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900433 }
434
435 public void checkRemoteInputOutside(MotionEvent event) {
436 if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
437 && event.getX() == 0 && event.getY() == 0 // a touch outside both bars
438 && mRemoteInputController.isRemoteInputActive()) {
439 mRemoteInputController.closeRemoteInputs();
440 }
441 }
442
Kevina5ff1fa2018-08-21 16:35:48 -0700443 @VisibleForTesting
444 StatusBarNotification rebuildNotificationForCanceledSmartReplies(
445 NotificationData.Entry entry) {
446 return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */,
447 false /* showSpinner */);
448 }
449
450 @VisibleForTesting
451 StatusBarNotification rebuildNotificationWithRemoteInput(NotificationData.Entry entry,
452 CharSequence remoteInputText, boolean showSpinner) {
453 StatusBarNotification sbn = entry.notification;
454
455 Notification.Builder b = Notification.Builder
456 .recoverBuilder(mContext, sbn.getNotification().clone());
457 if (remoteInputText != null) {
458 CharSequence[] oldHistory = sbn.getNotification().extras
459 .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
460 CharSequence[] newHistory;
461 if (oldHistory == null) {
462 newHistory = new CharSequence[1];
463 } else {
464 newHistory = new CharSequence[oldHistory.length + 1];
465 System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length);
466 }
467 newHistory[0] = String.valueOf(remoteInputText);
468 b.setRemoteInputHistory(newHistory);
469 }
470 b.setShowRemoteInputSpinner(showSpinner);
471 b.setHideSmartReplies(true);
472
473 Notification newNotification = b.build();
474
475 // Undo any compatibility view inflation
476 newNotification.contentView = sbn.getNotification().contentView;
477 newNotification.bigContentView = sbn.getNotification().bigContentView;
478 newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
479
480 return new StatusBarNotification(
481 sbn.getPackageName(),
482 sbn.getOpPkg(),
483 sbn.getId(),
484 sbn.getTag(),
485 sbn.getUid(),
486 sbn.getInitialPid(),
487 newNotification,
488 sbn.getUser(),
489 sbn.getOverrideGroupKey(),
490 sbn.getPostTime());
491 }
492
Eliot Courtneye77edea2017-11-15 14:25:21 +0900493 @Override
494 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
495 pw.println("NotificationRemoteInputManager state:");
Kevina5ff1fa2018-08-21 16:35:48 -0700496 pw.print(" mKeysKeptForRemoteInputHistory: ");
497 pw.println(mKeysKeptForRemoteInputHistory);
498 pw.print(" mEntriesKeptForRemoteInputActive: ");
499 pw.println(mEntriesKeptForRemoteInputActive);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900500 }
501
502 public void bindRow(ExpandableNotificationRow row) {
503 row.setRemoteInputController(mRemoteInputController);
504 row.setRemoteViewClickHandler(mOnClickHandler);
505 }
506
Eliot Courtneye77edea2017-11-15 14:25:21 +0900507 @VisibleForTesting
Kevina5ff1fa2018-08-21 16:35:48 -0700508 public Set<NotificationData.Entry> getEntriesKeptForRemoteInputActive() {
509 return mEntriesKeptForRemoteInputActive;
510 }
511
512 /**
513 * NotificationRemoteInputManager has multiple reasons to keep notification lifetime extended
514 * so we implement multiple NotificationLifetimeExtenders
515 */
516 protected abstract class RemoteInputExtender implements NotificationLifetimeExtender {
517 @Override
518 public void setCallback(NotificationSafeToRemoveCallback callback) {
519 if (mNotificationLifetimeFinishedCallback == null) {
520 mNotificationLifetimeFinishedCallback = callback;
521 }
522 }
523 }
524
525 /**
526 * Notification is kept alive as it was cancelled in response to a remote input interaction.
527 * This allows us to show what you replied and allows you to continue typing into it.
528 */
529 protected class RemoteInputHistoryExtender extends RemoteInputExtender {
530 @Override
531 public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) {
532 return shouldKeepForRemoteInputHistory(entry);
533 }
534
535 @Override
Kevine9e938c2018-09-06 13:38:11 -0700536 public void setShouldManageLifetime(NotificationData.Entry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700537 boolean shouldExtend) {
538 if (shouldExtend) {
539 CharSequence remoteInputText = entry.remoteInputText;
540 if (TextUtils.isEmpty(remoteInputText)) {
541 remoteInputText = entry.remoteInputTextWhenReset;
542 }
543 StatusBarNotification newSbn = rebuildNotificationWithRemoteInput(entry,
544 remoteInputText, false /* showSpinner */);
545 entry.onRemoteInputInserted();
546
547 if (newSbn == null) {
548 return;
549 }
550
551 mEntryManager.updateNotification(newSbn, null);
552
553 // Ensure the entry hasn't already been removed. This can happen if there is an
554 // inflation exception while updating the remote history
Evan Laird94492852018-10-25 13:43:01 -0400555 if (entry.isRemoved()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700556 return;
557 }
558
559 if (Log.isLoggable(TAG, Log.DEBUG)) {
560 Log.d(TAG, "Keeping notification around after sending remote input "
561 + entry.key);
562 }
563
564 mKeysKeptForRemoteInputHistory.add(entry.key);
565 } else {
566 mKeysKeptForRemoteInputHistory.remove(entry.key);
567 }
568 }
569 }
570
571 /**
572 * Notification is kept alive for smart reply history. Similar to REMOTE_INPUT_HISTORY but with
573 * {@link SmartReplyController} specific logic
574 */
575 protected class SmartReplyHistoryExtender extends RemoteInputExtender {
576 @Override
577 public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) {
578 return shouldKeepForSmartReplyHistory(entry);
579 }
580
581 @Override
Kevine9e938c2018-09-06 13:38:11 -0700582 public void setShouldManageLifetime(NotificationData.Entry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700583 boolean shouldExtend) {
584 if (shouldExtend) {
585 StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);
586
587 if (newSbn == null) {
588 return;
589 }
590
591 mEntryManager.updateNotification(newSbn, null);
592
Evan Laird94492852018-10-25 13:43:01 -0400593 if (entry.isRemoved()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700594 return;
595 }
596
597 if (Log.isLoggable(TAG, Log.DEBUG)) {
598 Log.d(TAG, "Keeping notification around after sending smart reply "
599 + entry.key);
600 }
601
602 mKeysKeptForRemoteInputHistory.add(entry.key);
603 } else {
604 mKeysKeptForRemoteInputHistory.remove(entry.key);
605 mSmartReplyController.stopSending(entry);
606 }
607 }
608 }
609
610 /**
611 * Notification is kept alive because the user is still using the remote input
612 */
613 protected class RemoteInputActiveExtender extends RemoteInputExtender {
614 @Override
615 public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) {
Evan Laird94492852018-10-25 13:43:01 -0400616 if (entry.isDismissed()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700617 return false;
618 }
619 return mRemoteInputController.isRemoteInputActive(entry);
620 }
621
622 @Override
Kevine9e938c2018-09-06 13:38:11 -0700623 public void setShouldManageLifetime(NotificationData.Entry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700624 boolean shouldExtend) {
625 if (shouldExtend) {
626 if (Log.isLoggable(TAG, Log.DEBUG)) {
627 Log.d(TAG, "Keeping notification around while remote input active "
628 + entry.key);
629 }
630 mEntriesKeptForRemoteInputActive.add(entry);
631 } else {
632 mEntriesKeptForRemoteInputActive.remove(entry);
633 }
634 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900635 }
636
637 /**
638 * Callback for various remote input related events, or for providing information that
639 * NotificationRemoteInputManager needs to know to decide what to do.
640 */
641 public interface Callback {
642
643 /**
644 * Called when remote input was activated but the device is locked.
645 *
646 * @param row
647 * @param clicked
648 */
649 void onLockedRemoteInput(ExpandableNotificationRow row, View clicked);
650
651 /**
652 * Called when remote input was activated but the device is locked and in a managed profile.
653 *
654 * @param userId
655 * @param row
656 * @param clicked
657 */
658 void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked);
659
660 /**
661 * Called when a row should be made expanded for the purposes of remote input.
662 *
663 * @param row
664 * @param clickedView
665 */
666 void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView);
667
668 /**
669 * Return whether or not remote input should be handled for this view.
670 *
671 * @param view
672 * @param pendingIntent
673 * @return true iff the remote input should be handled
674 */
675 boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent);
676
677 /**
678 * Performs any special handling for a remote view click. The default behaviour can be
679 * called through the defaultHandler parameter.
680 *
yoshiki iguchi67c166d2018-11-26 13:11:36 +0900681 * @param view
Eliot Courtneye77edea2017-11-15 14:25:21 +0900682 * @param pendingIntent
Eliot Courtneye77edea2017-11-15 14:25:21 +0900683 * @param defaultHandler
684 * @return true iff the click was handled
685 */
yoshiki iguchi67c166d2018-11-26 13:11:36 +0900686 boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
687 ClickHandler defaultHandler);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900688 }
689
690 /**
691 * Helper interface meant for passing the default on click behaviour to NotificationPresenter,
692 * so it may do its own handling before invoking the default behaviour.
693 */
694 public interface ClickHandler {
695 /**
696 * Tries to handle a click on a remote view.
697 *
698 * @return true iff the click was handled
699 */
700 boolean handleClick();
701 }
702}