blob: b820dc09657a16c8178462aef5bcd0d4d9f60ad4 [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) {
Evan Laird94492852018-10-25 13:43:01 -0400464 if (entry.isDismissed()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700465 return false;
466 }
467 if (!FORCE_REMOTE_INPUT_HISTORY) {
468 return false;
469 }
470 return (mRemoteInputController.isSpinning(entry.key) || entry.hasJustSentRemoteInput());
471 }
472
Ned Burnsf81c4c42019-01-07 14:10:43 -0500473 public boolean shouldKeepForSmartReplyHistory(NotificationEntry entry) {
Evan Laird94492852018-10-25 13:43:01 -0400474 if (entry.isDismissed()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700475 return false;
476 }
477 if (!FORCE_REMOTE_INPUT_HISTORY) {
478 return false;
479 }
480 return mSmartReplyController.isSendingSmartReply(entry.key);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900481 }
482
483 public void checkRemoteInputOutside(MotionEvent event) {
484 if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
485 && event.getX() == 0 && event.getY() == 0 // a touch outside both bars
486 && mRemoteInputController.isRemoteInputActive()) {
487 mRemoteInputController.closeRemoteInputs();
488 }
489 }
490
Kevina5ff1fa2018-08-21 16:35:48 -0700491 @VisibleForTesting
492 StatusBarNotification rebuildNotificationForCanceledSmartReplies(
Ned Burnsf81c4c42019-01-07 14:10:43 -0500493 NotificationEntry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700494 return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */,
495 false /* showSpinner */);
496 }
497
498 @VisibleForTesting
Ned Burnsf81c4c42019-01-07 14:10:43 -0500499 StatusBarNotification rebuildNotificationWithRemoteInput(NotificationEntry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700500 CharSequence remoteInputText, boolean showSpinner) {
501 StatusBarNotification sbn = entry.notification;
502
503 Notification.Builder b = Notification.Builder
504 .recoverBuilder(mContext, sbn.getNotification().clone());
505 if (remoteInputText != null) {
506 CharSequence[] oldHistory = sbn.getNotification().extras
507 .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
508 CharSequence[] newHistory;
509 if (oldHistory == null) {
510 newHistory = new CharSequence[1];
511 } else {
512 newHistory = new CharSequence[oldHistory.length + 1];
513 System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length);
514 }
515 newHistory[0] = String.valueOf(remoteInputText);
516 b.setRemoteInputHistory(newHistory);
517 }
518 b.setShowRemoteInputSpinner(showSpinner);
519 b.setHideSmartReplies(true);
520
521 Notification newNotification = b.build();
522
523 // Undo any compatibility view inflation
524 newNotification.contentView = sbn.getNotification().contentView;
525 newNotification.bigContentView = sbn.getNotification().bigContentView;
526 newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
527
528 return new StatusBarNotification(
529 sbn.getPackageName(),
530 sbn.getOpPkg(),
531 sbn.getId(),
532 sbn.getTag(),
533 sbn.getUid(),
534 sbn.getInitialPid(),
535 newNotification,
536 sbn.getUser(),
537 sbn.getOverrideGroupKey(),
538 sbn.getPostTime());
539 }
540
Eliot Courtneye77edea2017-11-15 14:25:21 +0900541 @Override
542 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
543 pw.println("NotificationRemoteInputManager state:");
Kevina5ff1fa2018-08-21 16:35:48 -0700544 pw.print(" mKeysKeptForRemoteInputHistory: ");
545 pw.println(mKeysKeptForRemoteInputHistory);
546 pw.print(" mEntriesKeptForRemoteInputActive: ");
547 pw.println(mEntriesKeptForRemoteInputActive);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900548 }
549
550 public void bindRow(ExpandableNotificationRow row) {
551 row.setRemoteInputController(mRemoteInputController);
552 row.setRemoteViewClickHandler(mOnClickHandler);
553 }
554
Eliot Courtneye77edea2017-11-15 14:25:21 +0900555 @VisibleForTesting
Ned Burnsf81c4c42019-01-07 14:10:43 -0500556 public Set<NotificationEntry> getEntriesKeptForRemoteInputActive() {
Kevina5ff1fa2018-08-21 16:35:48 -0700557 return mEntriesKeptForRemoteInputActive;
558 }
559
560 /**
561 * NotificationRemoteInputManager has multiple reasons to keep notification lifetime extended
562 * so we implement multiple NotificationLifetimeExtenders
563 */
564 protected abstract class RemoteInputExtender implements NotificationLifetimeExtender {
565 @Override
566 public void setCallback(NotificationSafeToRemoveCallback callback) {
567 if (mNotificationLifetimeFinishedCallback == null) {
568 mNotificationLifetimeFinishedCallback = callback;
569 }
570 }
571 }
572
573 /**
574 * Notification is kept alive as it was cancelled in response to a remote input interaction.
575 * This allows us to show what you replied and allows you to continue typing into it.
576 */
577 protected class RemoteInputHistoryExtender extends RemoteInputExtender {
578 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500579 public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700580 return shouldKeepForRemoteInputHistory(entry);
581 }
582
583 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500584 public void setShouldManageLifetime(NotificationEntry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700585 boolean shouldExtend) {
586 if (shouldExtend) {
587 CharSequence remoteInputText = entry.remoteInputText;
588 if (TextUtils.isEmpty(remoteInputText)) {
589 remoteInputText = entry.remoteInputTextWhenReset;
590 }
591 StatusBarNotification newSbn = rebuildNotificationWithRemoteInput(entry,
592 remoteInputText, false /* showSpinner */);
593 entry.onRemoteInputInserted();
594
595 if (newSbn == null) {
596 return;
597 }
598
599 mEntryManager.updateNotification(newSbn, null);
600
601 // Ensure the entry hasn't already been removed. This can happen if there is an
602 // inflation exception while updating the remote history
Evan Laird94492852018-10-25 13:43:01 -0400603 if (entry.isRemoved()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700604 return;
605 }
606
607 if (Log.isLoggable(TAG, Log.DEBUG)) {
608 Log.d(TAG, "Keeping notification around after sending remote input "
609 + entry.key);
610 }
611
612 mKeysKeptForRemoteInputHistory.add(entry.key);
613 } else {
614 mKeysKeptForRemoteInputHistory.remove(entry.key);
615 }
616 }
617 }
618
619 /**
620 * Notification is kept alive for smart reply history. Similar to REMOTE_INPUT_HISTORY but with
621 * {@link SmartReplyController} specific logic
622 */
623 protected class SmartReplyHistoryExtender extends RemoteInputExtender {
624 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500625 public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700626 return shouldKeepForSmartReplyHistory(entry);
627 }
628
629 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500630 public void setShouldManageLifetime(NotificationEntry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700631 boolean shouldExtend) {
632 if (shouldExtend) {
633 StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);
634
635 if (newSbn == null) {
636 return;
637 }
638
639 mEntryManager.updateNotification(newSbn, null);
640
Evan Laird94492852018-10-25 13:43:01 -0400641 if (entry.isRemoved()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700642 return;
643 }
644
645 if (Log.isLoggable(TAG, Log.DEBUG)) {
646 Log.d(TAG, "Keeping notification around after sending smart reply "
647 + entry.key);
648 }
649
650 mKeysKeptForRemoteInputHistory.add(entry.key);
651 } else {
652 mKeysKeptForRemoteInputHistory.remove(entry.key);
653 mSmartReplyController.stopSending(entry);
654 }
655 }
656 }
657
658 /**
659 * Notification is kept alive because the user is still using the remote input
660 */
661 protected class RemoteInputActiveExtender extends RemoteInputExtender {
662 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500663 public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
Evan Laird94492852018-10-25 13:43:01 -0400664 if (entry.isDismissed()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700665 return false;
666 }
667 return mRemoteInputController.isRemoteInputActive(entry);
668 }
669
670 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500671 public void setShouldManageLifetime(NotificationEntry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700672 boolean shouldExtend) {
673 if (shouldExtend) {
674 if (Log.isLoggable(TAG, Log.DEBUG)) {
675 Log.d(TAG, "Keeping notification around while remote input active "
676 + entry.key);
677 }
678 mEntriesKeptForRemoteInputActive.add(entry);
679 } else {
680 mEntriesKeptForRemoteInputActive.remove(entry);
681 }
682 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900683 }
684
685 /**
686 * Callback for various remote input related events, or for providing information that
687 * NotificationRemoteInputManager needs to know to decide what to do.
688 */
689 public interface Callback {
690
691 /**
692 * Called when remote input was activated but the device is locked.
693 *
694 * @param row
695 * @param clicked
696 */
697 void onLockedRemoteInput(ExpandableNotificationRow row, View clicked);
698
699 /**
700 * Called when remote input was activated but the device is locked and in a managed profile.
701 *
702 * @param userId
703 * @param row
704 * @param clicked
705 */
706 void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked);
707
708 /**
709 * Called when a row should be made expanded for the purposes of remote input.
710 *
711 * @param row
712 * @param clickedView
713 */
714 void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView);
715
716 /**
717 * Return whether or not remote input should be handled for this view.
718 *
719 * @param view
720 * @param pendingIntent
721 * @return true iff the remote input should be handled
722 */
723 boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent);
724
725 /**
726 * Performs any special handling for a remote view click. The default behaviour can be
727 * called through the defaultHandler parameter.
728 *
yoshiki iguchi67c166d2018-11-26 13:11:36 +0900729 * @param view
Eliot Courtneye77edea2017-11-15 14:25:21 +0900730 * @param pendingIntent
Eliot Courtneye77edea2017-11-15 14:25:21 +0900731 * @param defaultHandler
732 * @return true iff the click was handled
733 */
yoshiki iguchi67c166d2018-11-26 13:11:36 +0900734 boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
735 ClickHandler defaultHandler);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900736 }
737
738 /**
739 * Helper interface meant for passing the default on click behaviour to NotificationPresenter,
740 * so it may do its own handling before invoking the default behaviour.
741 */
742 public interface ClickHandler {
743 /**
744 * Tries to handle a click on a remote view.
745 *
746 * @return true iff the click was handled
747 */
748 boolean handleClick();
749 }
750}