blob: a630e49d850a78fe0ebb6c636154f1ba0b137903 [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
Gus Prevas772e5322018-12-21 16:22:16 -050020import static com.android.systemui.Dependency.MAIN_HANDLER_NAME;
21
Kevina5ff1fa2018-08-21 16:35:48 -070022import android.annotation.NonNull;
Ned Burns1dd6b402019-01-02 15:25:23 -050023import android.annotation.Nullable;
Eliot Courtneye77edea2017-11-15 14:25:21 +090024import android.app.ActivityManager;
Sunny Goyal02794532018-08-22 15:18:37 -070025import android.app.ActivityOptions;
Jason Monk297c04e2018-08-23 17:16:59 -040026import android.app.KeyguardManager;
Kevina5ff1fa2018-08-21 16:35:48 -070027import android.app.Notification;
Eliot Courtneye77edea2017-11-15 14:25:21 +090028import android.app.PendingIntent;
29import android.app.RemoteInput;
30import android.content.Context;
31import android.content.Intent;
Gus Prevas772e5322018-12-21 16:22:16 -050032import android.os.Handler;
Eliot Courtneye77edea2017-11-15 14:25:21 +090033import android.os.RemoteException;
34import android.os.ServiceManager;
35import android.os.SystemClock;
36import android.os.SystemProperties;
37import android.os.UserManager;
38import android.service.notification.StatusBarNotification;
Kevina5ff1fa2018-08-21 16:35:48 -070039import android.text.TextUtils;
Eliot Courtneye77edea2017-11-15 14:25:21 +090040import android.util.ArraySet;
41import android.util.Log;
Sunny Goyal43c97042018-08-23 15:21:26 -070042import android.util.Pair;
Eliot Courtneye77edea2017-11-15 14:25:21 +090043import android.view.MotionEvent;
44import android.view.View;
45import android.view.ViewGroup;
46import android.view.ViewParent;
47import android.widget.RemoteViews;
48import android.widget.TextView;
49
50import com.android.internal.annotations.VisibleForTesting;
51import com.android.internal.statusbar.IStatusBarService;
Dieter Hsud39f0d52018-04-14 02:08:30 +080052import com.android.internal.statusbar.NotificationVisibility;
Eliot Courtneye77edea2017-11-15 14:25:21 +090053import com.android.systemui.Dumpable;
Gus Prevas772e5322018-12-21 16:22:16 -050054import com.android.systemui.statusbar.notification.NotificationEntryListener;
Rohan Shah20790b82018-07-02 17:21:04 -070055import com.android.systemui.statusbar.notification.NotificationEntryManager;
Ned Burnsf81c4c42019-01-07 14:10:43 -050056import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Milo Sredkov13d88112019-02-01 12:23:24 +000057import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo;
Gustav Senntonf892fe92019-01-22 15:31:42 +000058import com.android.systemui.statusbar.notification.logging.NotificationLogger;
Rohan Shah20790b82018-07-02 17:21:04 -070059import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
Jason Monk297c04e2018-08-23 17:16:59 -040060import com.android.systemui.statusbar.phone.ShadeController;
Eliot Courtneye77edea2017-11-15 14:25:21 +090061import com.android.systemui.statusbar.policy.RemoteInputView;
62
63import java.io.FileDescriptor;
64import java.io.PrintWriter;
Kevina5ff1fa2018-08-21 16:35:48 -070065import java.util.ArrayList;
Eliot Courtneye77edea2017-11-15 14:25:21 +090066import java.util.Set;
67
Jason Monk27d01a622018-12-10 15:57:09 -050068import javax.inject.Inject;
Gus Prevas772e5322018-12-21 16:22:16 -050069import javax.inject.Named;
Jason Monk27d01a622018-12-10 15:57:09 -050070import javax.inject.Singleton;
71
Gus Prevas772e5322018-12-21 16:22:16 -050072import dagger.Lazy;
73
Eliot Courtneye77edea2017-11-15 14:25:21 +090074/**
75 * Class for handling remote input state over a set of notifications. This class handles things
76 * like keeping notifications temporarily that were cancelled as a response to a remote input
77 * interaction, keeping track of notifications to remove when NotificationPresenter is collapsed,
78 * and handling clicks on remote views.
79 */
Jason Monk27d01a622018-12-10 15:57:09 -050080@Singleton
Eliot Courtneye77edea2017-11-15 14:25:21 +090081public class NotificationRemoteInputManager implements Dumpable {
82 public static final boolean ENABLE_REMOTE_INPUT =
83 SystemProperties.getBoolean("debug.enable_remote_input", true);
Kevina5ff1fa2018-08-21 16:35:48 -070084 public static boolean FORCE_REMOTE_INPUT_HISTORY =
Eliot Courtneye77edea2017-11-15 14:25:21 +090085 SystemProperties.getBoolean("debug.force_remoteinput_history", true);
86 private static final boolean DEBUG = false;
Kevina5ff1fa2018-08-21 16:35:48 -070087 private static final String TAG = "NotifRemoteInputManager";
Eliot Courtneye77edea2017-11-15 14:25:21 +090088
89 /**
90 * How long to wait before auto-dismissing a notification that was kept for remote input, and
91 * has now sent a remote input. We auto-dismiss, because the app may not see a reason to cancel
92 * these given that they technically don't exist anymore. We wait a bit in case the app issues
93 * an update.
94 */
95 private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
96
Kevina5ff1fa2018-08-21 16:35:48 -070097 /**
98 * Notifications that are already removed but are kept around because we want to show the
99 * remote input history. See {@link RemoteInputHistoryExtender} and
100 * {@link SmartReplyHistoryExtender}.
101 */
102 protected final ArraySet<String> mKeysKeptForRemoteInputHistory = new ArraySet<>();
103
104 /**
105 * Notifications that are already removed but are kept around because the remote input is
106 * actively being used (i.e. user is typing in it). See {@link RemoteInputActiveExtender}.
107 */
Ned Burnsf81c4c42019-01-07 14:10:43 -0500108 protected final ArraySet<NotificationEntry> mEntriesKeptForRemoteInputActive =
Eliot Courtneye77edea2017-11-15 14:25:21 +0900109 new ArraySet<>();
Eliot Courtney6c313d32017-12-14 19:57:51 +0900110
111 // Dependencies:
Gus Prevas772e5322018-12-21 16:22:16 -0500112 private final NotificationLockscreenUserManager mLockscreenUserManager;
113 private final SmartReplyController mSmartReplyController;
114 private final NotificationEntryManager mEntryManager;
115 private final Handler mMainHandler;
Jason Monk297c04e2018-08-23 17:16:59 -0400116
Gus Prevas772e5322018-12-21 16:22:16 -0500117 private final Lazy<ShadeController> mShadeController;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900118
Eliot Courtneye77edea2017-11-15 14:25:21 +0900119 protected final Context mContext;
120 private final UserManager mUserManager;
Jason Monk297c04e2018-08-23 17:16:59 -0400121 private final KeyguardManager mKeyguardManager;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900122
123 protected RemoteInputController mRemoteInputController;
Kevina5ff1fa2018-08-21 16:35:48 -0700124 protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback
125 mNotificationLifetimeFinishedCallback;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900126 protected IStatusBarService mBarService;
127 protected Callback mCallback;
Kevina5ff1fa2018-08-21 16:35:48 -0700128 protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
Eliot Courtneye77edea2017-11-15 14:25:21 +0900129
130 private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() {
131
132 @Override
133 public boolean onClickHandler(
Sunny Goyal43c97042018-08-23 15:21:26 -0700134 View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) {
Michael Wrighte3001042019-02-05 00:13:14 +0000135 mShadeController.get().wakeUpIfDozing(SystemClock.uptimeMillis(), view,
136 "NOTIFICATION_CLICK");
Eliot Courtneye77edea2017-11-15 14:25:21 +0900137
138 if (handleRemoteInput(view, pendingIntent)) {
139 return true;
140 }
141
142 if (DEBUG) {
143 Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
144 }
145 logActionClick(view);
146 // The intent we are sending is for the application, which
147 // won't have permission to immediately start an activity after
148 // the user switches to home. We know it is safe to do at this
149 // point, so make sure new activity switches are now allowed.
150 try {
151 ActivityManager.getService().resumeAppSwitches();
152 } catch (RemoteException e) {
153 }
yoshiki iguchi67c166d2018-11-26 13:11:36 +0900154 return mCallback.handleRemoteViewClick(view, pendingIntent, () -> {
Sunny Goyal43c97042018-08-23 15:21:26 -0700155 Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
156 options.second.setLaunchWindowingMode(
157 WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
158 return RemoteViews.startPendingIntent(view, pendingIntent, options);
159 });
Eliot Courtneye77edea2017-11-15 14:25:21 +0900160 }
161
162 private void logActionClick(View view) {
Tony Mak7d4b3a52018-11-27 17:29:36 +0000163 Integer actionIndex = (Integer)
164 view.getTag(com.android.internal.R.id.notification_action_index_tag);
165 if (actionIndex == null) {
166 Log.e(TAG, "Couldn't retrieve the actionIndex from the clicked button");
167 return;
168 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900169 ViewParent parent = view.getParent();
Tony Mak7d4b3a52018-11-27 17:29:36 +0000170 StatusBarNotification statusBarNotification = getNotificationForParent(parent);
171 if (statusBarNotification == null) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900172 Log.w(TAG, "Couldn't determine notification for click.");
173 return;
174 }
Tony Mak7d4b3a52018-11-27 17:29:36 +0000175 String key = statusBarNotification.getKey();
176 int buttonIndex = -1;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900177 // If this is a default template, determine the index of the button.
178 if (view.getId() == com.android.internal.R.id.action0 &&
179 parent != null && parent instanceof ViewGroup) {
180 ViewGroup actionGroup = (ViewGroup) parent;
Tony Mak7d4b3a52018-11-27 17:29:36 +0000181 buttonIndex = actionGroup.indexOfChild(view);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900182 }
Dieter Hsud39f0d52018-04-14 02:08:30 +0800183 final int count = mEntryManager.getNotificationData().getActiveNotifications().size();
184 final int rank = mEntryManager.getNotificationData().getRank(key);
Tony Mak7d4b3a52018-11-27 17:29:36 +0000185 final Notification.Action action =
186 statusBarNotification.getNotification().actions[actionIndex];
Gustav Senntonf892fe92019-01-22 15:31:42 +0000187 NotificationVisibility.NotificationLocation location =
188 NotificationLogger.getNotificationLocation(
189 mEntryManager.getNotificationData().get(key));
190 final NotificationVisibility nv =
191 NotificationVisibility.obtain(key, rank, count, true, location);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900192 try {
Tony Mak7d4b3a52018-11-27 17:29:36 +0000193 mBarService.onNotificationActionClick(key, buttonIndex, action, nv, false);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900194 } catch (RemoteException e) {
195 // Ignore
196 }
197 }
198
Tony Mak7d4b3a52018-11-27 17:29:36 +0000199 private StatusBarNotification getNotificationForParent(ViewParent parent) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900200 while (parent != null) {
201 if (parent instanceof ExpandableNotificationRow) {
Tony Mak7d4b3a52018-11-27 17:29:36 +0000202 return ((ExpandableNotificationRow) parent).getStatusBarNotification();
Eliot Courtneye77edea2017-11-15 14:25:21 +0900203 }
204 parent = parent.getParent();
205 }
206 return null;
207 }
208
Eliot Courtneye77edea2017-11-15 14:25:21 +0900209 private boolean handleRemoteInput(View view, PendingIntent pendingIntent) {
210 if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) {
211 return true;
212 }
213
214 Object tag = view.getTag(com.android.internal.R.id.remote_input_tag);
215 RemoteInput[] inputs = null;
216 if (tag instanceof RemoteInput[]) {
217 inputs = (RemoteInput[]) tag;
218 }
219
220 if (inputs == null) {
221 return false;
222 }
223
224 RemoteInput input = null;
225
226 for (RemoteInput i : inputs) {
227 if (i.getAllowFreeFormInput()) {
228 input = i;
229 }
230 }
231
232 if (input == null) {
233 return false;
234 }
235
Milo Sredkov13d88112019-02-01 12:23:24 +0000236 return activateRemoteInput(view, inputs, input, pendingIntent,
237 null /* editedSuggestionInfo */);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900238 }
239 };
240
Jason Monk27d01a622018-12-10 15:57:09 -0500241 @Inject
Gus Prevas772e5322018-12-21 16:22:16 -0500242 public NotificationRemoteInputManager(
243 Context context,
244 NotificationLockscreenUserManager lockscreenUserManager,
245 SmartReplyController smartReplyController,
246 NotificationEntryManager notificationEntryManager,
247 Lazy<ShadeController> shadeController,
248 @Named(MAIN_HANDLER_NAME) Handler mainHandler) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900249 mContext = context;
Gus Prevas772e5322018-12-21 16:22:16 -0500250 mLockscreenUserManager = lockscreenUserManager;
251 mSmartReplyController = smartReplyController;
252 mEntryManager = notificationEntryManager;
253 mShadeController = shadeController;
254 mMainHandler = mainHandler;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900255 mBarService = IStatusBarService.Stub.asInterface(
256 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
257 mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
Kevina5ff1fa2018-08-21 16:35:48 -0700258 addLifetimeExtenders();
Jason Monk297c04e2018-08-23 17:16:59 -0400259 mKeyguardManager = context.getSystemService(KeyguardManager.class);
Gus Prevas772e5322018-12-21 16:22:16 -0500260
261 notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
262 @Override
263 public void onEntryRemoved(
Ned Burnsf81c4c42019-01-07 14:10:43 -0500264 @Nullable NotificationEntry entry,
Gus Prevasca1b6f72018-12-28 10:53:11 -0500265 NotificationVisibility visibility,
Gus Prevas772e5322018-12-21 16:22:16 -0500266 boolean removedByUser) {
Ned Burns1dd6b402019-01-02 15:25:23 -0500267 if (removedByUser && entry != null) {
Ned Burnsef2ef6c2019-01-02 16:48:08 -0500268 onPerformRemoveNotification(entry, entry.key);
Gus Prevas772e5322018-12-21 16:22:16 -0500269 }
270 }
271 });
Eliot Courtneye77edea2017-11-15 14:25:21 +0900272 }
273
Gus Prevas21437b32018-12-05 10:36:13 -0500274 /** Initializes this component with the provided dependencies. */
275 public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900276 mCallback = callback;
277 mRemoteInputController = new RemoteInputController(delegate);
278 mRemoteInputController.addCallback(new RemoteInputController.Callback() {
279 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500280 public void onRemoteInputSent(NotificationEntry entry) {
Kenny Guy8cc15d22018-05-09 09:50:55 +0100281 if (FORCE_REMOTE_INPUT_HISTORY
Kevina5ff1fa2018-08-21 16:35:48 -0700282 && isNotificationKeptForRemoteInputHistory(entry.key)) {
283 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
284 } else if (mEntriesKeptForRemoteInputActive.contains(entry)) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900285 // We're currently holding onto this notification, but from the apps point of
286 // view it is already canceled, so we'll need to cancel it on the apps behalf
287 // after sending - unless the app posts an update in the mean time, so wait a
288 // bit.
Gus Prevas772e5322018-12-21 16:22:16 -0500289 mMainHandler.postDelayed(() -> {
Kevina5ff1fa2018-08-21 16:35:48 -0700290 if (mEntriesKeptForRemoteInputActive.remove(entry)) {
291 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900292 }
293 }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
294 }
Amith Yamasani129b4812018-02-08 13:55:59 -0800295 try {
296 mBarService.onNotificationDirectReplied(entry.notification.getKey());
Milo Sredkov13d88112019-02-01 12:23:24 +0000297 if (entry.editedSuggestionInfo != null) {
298 boolean modifiedBeforeSending =
299 !TextUtils.equals(entry.remoteInputText,
300 entry.editedSuggestionInfo.originalText);
301 mBarService.onNotificationSmartReplySent(
302 entry.notification.getKey(),
303 entry.editedSuggestionInfo.index,
304 entry.editedSuggestionInfo.originalText,
305 NotificationLogger
306 .getNotificationLocation(entry)
307 .toMetricsEventEnum(),
308 modifiedBeforeSending);
309 }
Amith Yamasani129b4812018-02-08 13:55:59 -0800310 } catch (RemoteException e) {
311 // Nothing to do, system going down
312 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900313 }
314 });
Kevina5ff1fa2018-08-21 16:35:48 -0700315 mSmartReplyController.setCallback((entry, reply) -> {
316 StatusBarNotification newSbn =
317 rebuildNotificationWithRemoteInput(entry, reply, true /* showSpinner */);
318 mEntryManager.updateNotification(newSbn, null /* ranking */);
319 });
320 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900321
Kevina5ff1fa2018-08-21 16:35:48 -0700322 /**
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000323 * Activates a given {@link RemoteInput}
324 *
325 * @param view The view of the action button or suggestion chip that was tapped.
326 * @param inputs The remote inputs that need to be sent to the app.
327 * @param input The remote input that needs to be activated.
328 * @param pendingIntent The pending intent to be sent to the app.
Milo Sredkov13d88112019-02-01 12:23:24 +0000329 * @param editedSuggestionInfo The smart reply that should be inserted in the remote input, or
330 * {@code null} if the user is not editing a smart reply.
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000331 * @return Whether the {@link RemoteInput} was activated.
332 */
333 public boolean activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input,
Milo Sredkov13d88112019-02-01 12:23:24 +0000334 PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo) {
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000335
336 ViewParent p = view.getParent();
337 RemoteInputView riv = null;
338 while (p != null) {
339 if (p instanceof View) {
340 View pv = (View) p;
341 if (pv.isRootNamespace()) {
342 riv = findRemoteInputView(pv);
343 break;
344 }
345 }
346 p = p.getParent();
347 }
348 ExpandableNotificationRow row = null;
349 while (p != null) {
350 if (p instanceof ExpandableNotificationRow) {
351 row = (ExpandableNotificationRow) p;
352 break;
353 }
354 p = p.getParent();
355 }
356
357 if (row == null) {
358 return false;
359 }
360
361 row.setUserExpanded(true);
362
363 if (!mLockscreenUserManager.shouldAllowLockscreenRemoteInput()) {
364 final int userId = pendingIntent.getCreatorUserHandle().getIdentifier();
365 if (mLockscreenUserManager.isLockscreenPublicMode(userId)) {
366 mCallback.onLockedRemoteInput(row, view);
367 return true;
368 }
369 if (mUserManager.getUserInfo(userId).isManagedProfile()
370 && mKeyguardManager.isDeviceLocked(userId)) {
371 mCallback.onLockedWorkRemoteInput(userId, row, view);
372 return true;
373 }
374 }
375
376 if (riv == null) {
377 riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild());
378 if (riv == null) {
379 return false;
380 }
381 if (!row.getPrivateLayout().getExpandedChild().isShown()) {
382 mCallback.onMakeExpandedVisibleForRemoteInput(row, view);
383 return true;
384 }
385 }
386
387 int width = view.getWidth();
388 if (view instanceof TextView) {
389 // Center the reveal on the text which might be off-center from the TextView
390 TextView tv = (TextView) view;
391 if (tv.getLayout() != null) {
392 int innerWidth = (int) tv.getLayout().getLineWidth(0);
393 innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight();
394 width = Math.min(width, innerWidth);
395 }
396 }
397 int cx = view.getLeft() + width / 2;
398 int cy = view.getTop() + view.getHeight() / 2;
399 int w = riv.getWidth();
400 int h = riv.getHeight();
401 int r = Math.max(
402 Math.max(cx + cy, cx + (h - cy)),
403 Math.max((w - cx) + cy, (w - cx) + (h - cy)));
404
405 riv.setRevealParameters(cx, cy, r);
406 riv.setPendingIntent(pendingIntent);
Milo Sredkov13d88112019-02-01 12:23:24 +0000407 riv.setRemoteInput(inputs, input, editedSuggestionInfo);
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000408 riv.focusAnimated();
409
410 return true;
411 }
412
413 private RemoteInputView findRemoteInputView(View v) {
414 if (v == null) {
415 return null;
416 }
417 return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG);
418 }
419
420 /**
Kevina5ff1fa2018-08-21 16:35:48 -0700421 * Adds all the notification lifetime extenders. Each extender represents a reason for the
422 * NotificationRemoteInputManager to keep a notification lifetime extended.
423 */
424 protected void addLifetimeExtenders() {
425 mLifetimeExtenders.add(new RemoteInputHistoryExtender());
426 mLifetimeExtenders.add(new SmartReplyHistoryExtender());
427 mLifetimeExtenders.add(new RemoteInputActiveExtender());
428 }
429
430 public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() {
431 return mLifetimeExtenders;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900432 }
433
434 public RemoteInputController getController() {
435 return mRemoteInputController;
436 }
437
Gus Prevas772e5322018-12-21 16:22:16 -0500438 @VisibleForTesting
Ned Burnsf81c4c42019-01-07 14:10:43 -0500439 void onPerformRemoveNotification(NotificationEntry entry, final String key) {
Gus Prevas772e5322018-12-21 16:22:16 -0500440 if (mKeysKeptForRemoteInputHistory.contains(key)) {
441 mKeysKeptForRemoteInputHistory.remove(key);
Kevina5ff1fa2018-08-21 16:35:48 -0700442 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900443 if (mRemoteInputController.isRemoteInputActive(entry)) {
444 mRemoteInputController.removeRemoteInput(entry, null);
445 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900446 }
447
Kevina5ff1fa2018-08-21 16:35:48 -0700448 public void onPanelCollapsed() {
449 for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500450 NotificationEntry entry = mEntriesKeptForRemoteInputActive.valueAt(i);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900451 mRemoteInputController.removeRemoteInput(entry, null);
Kevina5ff1fa2018-08-21 16:35:48 -0700452 if (mNotificationLifetimeFinishedCallback != null) {
453 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
454 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900455 }
Kevina5ff1fa2018-08-21 16:35:48 -0700456 mEntriesKeptForRemoteInputActive.clear();
457 }
458
459 public boolean isNotificationKeptForRemoteInputHistory(String key) {
460 return mKeysKeptForRemoteInputHistory.contains(key);
461 }
462
Ned Burnsf81c4c42019-01-07 14:10:43 -0500463 public boolean shouldKeepForRemoteInputHistory(NotificationEntry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700464 if (!FORCE_REMOTE_INPUT_HISTORY) {
465 return false;
466 }
467 return (mRemoteInputController.isSpinning(entry.key) || entry.hasJustSentRemoteInput());
468 }
469
Ned Burnsf81c4c42019-01-07 14:10:43 -0500470 public boolean shouldKeepForSmartReplyHistory(NotificationEntry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700471 if (!FORCE_REMOTE_INPUT_HISTORY) {
472 return false;
473 }
474 return mSmartReplyController.isSendingSmartReply(entry.key);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900475 }
476
477 public void checkRemoteInputOutside(MotionEvent event) {
478 if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
479 && event.getX() == 0 && event.getY() == 0 // a touch outside both bars
480 && mRemoteInputController.isRemoteInputActive()) {
481 mRemoteInputController.closeRemoteInputs();
482 }
483 }
484
Kevina5ff1fa2018-08-21 16:35:48 -0700485 @VisibleForTesting
486 StatusBarNotification rebuildNotificationForCanceledSmartReplies(
Ned Burnsf81c4c42019-01-07 14:10:43 -0500487 NotificationEntry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700488 return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */,
489 false /* showSpinner */);
490 }
491
492 @VisibleForTesting
Ned Burnsf81c4c42019-01-07 14:10:43 -0500493 StatusBarNotification rebuildNotificationWithRemoteInput(NotificationEntry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700494 CharSequence remoteInputText, boolean showSpinner) {
495 StatusBarNotification sbn = entry.notification;
496
497 Notification.Builder b = Notification.Builder
498 .recoverBuilder(mContext, sbn.getNotification().clone());
499 if (remoteInputText != null) {
500 CharSequence[] oldHistory = sbn.getNotification().extras
501 .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
502 CharSequence[] newHistory;
503 if (oldHistory == null) {
504 newHistory = new CharSequence[1];
505 } else {
506 newHistory = new CharSequence[oldHistory.length + 1];
507 System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length);
508 }
509 newHistory[0] = String.valueOf(remoteInputText);
510 b.setRemoteInputHistory(newHistory);
511 }
512 b.setShowRemoteInputSpinner(showSpinner);
513 b.setHideSmartReplies(true);
514
515 Notification newNotification = b.build();
516
517 // Undo any compatibility view inflation
518 newNotification.contentView = sbn.getNotification().contentView;
519 newNotification.bigContentView = sbn.getNotification().bigContentView;
520 newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
521
522 return new StatusBarNotification(
523 sbn.getPackageName(),
524 sbn.getOpPkg(),
525 sbn.getId(),
526 sbn.getTag(),
527 sbn.getUid(),
528 sbn.getInitialPid(),
529 newNotification,
530 sbn.getUser(),
531 sbn.getOverrideGroupKey(),
532 sbn.getPostTime());
533 }
534
Eliot Courtneye77edea2017-11-15 14:25:21 +0900535 @Override
536 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
537 pw.println("NotificationRemoteInputManager state:");
Kevina5ff1fa2018-08-21 16:35:48 -0700538 pw.print(" mKeysKeptForRemoteInputHistory: ");
539 pw.println(mKeysKeptForRemoteInputHistory);
540 pw.print(" mEntriesKeptForRemoteInputActive: ");
541 pw.println(mEntriesKeptForRemoteInputActive);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900542 }
543
544 public void bindRow(ExpandableNotificationRow row) {
545 row.setRemoteInputController(mRemoteInputController);
546 row.setRemoteViewClickHandler(mOnClickHandler);
547 }
548
Eliot Courtneye77edea2017-11-15 14:25:21 +0900549 @VisibleForTesting
Ned Burnsf81c4c42019-01-07 14:10:43 -0500550 public Set<NotificationEntry> getEntriesKeptForRemoteInputActive() {
Kevina5ff1fa2018-08-21 16:35:48 -0700551 return mEntriesKeptForRemoteInputActive;
552 }
553
554 /**
555 * NotificationRemoteInputManager has multiple reasons to keep notification lifetime extended
556 * so we implement multiple NotificationLifetimeExtenders
557 */
558 protected abstract class RemoteInputExtender implements NotificationLifetimeExtender {
559 @Override
560 public void setCallback(NotificationSafeToRemoveCallback callback) {
561 if (mNotificationLifetimeFinishedCallback == null) {
562 mNotificationLifetimeFinishedCallback = callback;
563 }
564 }
565 }
566
567 /**
568 * Notification is kept alive as it was cancelled in response to a remote input interaction.
569 * This allows us to show what you replied and allows you to continue typing into it.
570 */
571 protected class RemoteInputHistoryExtender extends RemoteInputExtender {
572 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500573 public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700574 return shouldKeepForRemoteInputHistory(entry);
575 }
576
577 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500578 public void setShouldManageLifetime(NotificationEntry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700579 boolean shouldExtend) {
580 if (shouldExtend) {
581 CharSequence remoteInputText = entry.remoteInputText;
582 if (TextUtils.isEmpty(remoteInputText)) {
583 remoteInputText = entry.remoteInputTextWhenReset;
584 }
585 StatusBarNotification newSbn = rebuildNotificationWithRemoteInput(entry,
586 remoteInputText, false /* showSpinner */);
587 entry.onRemoteInputInserted();
588
589 if (newSbn == null) {
590 return;
591 }
592
593 mEntryManager.updateNotification(newSbn, null);
594
595 // Ensure the entry hasn't already been removed. This can happen if there is an
596 // inflation exception while updating the remote history
Evan Laird94492852018-10-25 13:43:01 -0400597 if (entry.isRemoved()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700598 return;
599 }
600
601 if (Log.isLoggable(TAG, Log.DEBUG)) {
602 Log.d(TAG, "Keeping notification around after sending remote input "
603 + entry.key);
604 }
605
606 mKeysKeptForRemoteInputHistory.add(entry.key);
607 } else {
608 mKeysKeptForRemoteInputHistory.remove(entry.key);
609 }
610 }
611 }
612
613 /**
614 * Notification is kept alive for smart reply history. Similar to REMOTE_INPUT_HISTORY but with
615 * {@link SmartReplyController} specific logic
616 */
617 protected class SmartReplyHistoryExtender extends RemoteInputExtender {
618 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500619 public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700620 return shouldKeepForSmartReplyHistory(entry);
621 }
622
623 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500624 public void setShouldManageLifetime(NotificationEntry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700625 boolean shouldExtend) {
626 if (shouldExtend) {
627 StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);
628
629 if (newSbn == null) {
630 return;
631 }
632
633 mEntryManager.updateNotification(newSbn, null);
634
Evan Laird94492852018-10-25 13:43:01 -0400635 if (entry.isRemoved()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700636 return;
637 }
638
639 if (Log.isLoggable(TAG, Log.DEBUG)) {
640 Log.d(TAG, "Keeping notification around after sending smart reply "
641 + entry.key);
642 }
643
644 mKeysKeptForRemoteInputHistory.add(entry.key);
645 } else {
646 mKeysKeptForRemoteInputHistory.remove(entry.key);
647 mSmartReplyController.stopSending(entry);
648 }
649 }
650 }
651
652 /**
653 * Notification is kept alive because the user is still using the remote input
654 */
655 protected class RemoteInputActiveExtender extends RemoteInputExtender {
656 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500657 public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700658 return mRemoteInputController.isRemoteInputActive(entry);
659 }
660
661 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500662 public void setShouldManageLifetime(NotificationEntry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700663 boolean shouldExtend) {
664 if (shouldExtend) {
665 if (Log.isLoggable(TAG, Log.DEBUG)) {
666 Log.d(TAG, "Keeping notification around while remote input active "
667 + entry.key);
668 }
669 mEntriesKeptForRemoteInputActive.add(entry);
670 } else {
671 mEntriesKeptForRemoteInputActive.remove(entry);
672 }
673 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900674 }
675
676 /**
677 * Callback for various remote input related events, or for providing information that
678 * NotificationRemoteInputManager needs to know to decide what to do.
679 */
680 public interface Callback {
681
682 /**
683 * Called when remote input was activated but the device is locked.
684 *
685 * @param row
686 * @param clicked
687 */
688 void onLockedRemoteInput(ExpandableNotificationRow row, View clicked);
689
690 /**
691 * Called when remote input was activated but the device is locked and in a managed profile.
692 *
693 * @param userId
694 * @param row
695 * @param clicked
696 */
697 void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked);
698
699 /**
700 * Called when a row should be made expanded for the purposes of remote input.
701 *
702 * @param row
703 * @param clickedView
704 */
705 void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView);
706
707 /**
708 * Return whether or not remote input should be handled for this view.
709 *
710 * @param view
711 * @param pendingIntent
712 * @return true iff the remote input should be handled
713 */
714 boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent);
715
716 /**
717 * Performs any special handling for a remote view click. The default behaviour can be
718 * called through the defaultHandler parameter.
719 *
yoshiki iguchi67c166d2018-11-26 13:11:36 +0900720 * @param view
Eliot Courtneye77edea2017-11-15 14:25:21 +0900721 * @param pendingIntent
Eliot Courtneye77edea2017-11-15 14:25:21 +0900722 * @param defaultHandler
723 * @return true iff the click was handled
724 */
yoshiki iguchi67c166d2018-11-26 13:11:36 +0900725 boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
726 ClickHandler defaultHandler);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900727 }
728
729 /**
730 * Helper interface meant for passing the default on click behaviour to NotificationPresenter,
731 * so it may do its own handling before invoking the default behaviour.
732 */
733 public interface ClickHandler {
734 /**
735 * Tries to handle a click on a remote view.
736 *
737 * @return true iff the click was handled
738 */
739 boolean handleClick();
740 }
741}