blob: 886d99eeff176806aa35cbd279c00ed9bca82db9 [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;
Rohan Shah20790b82018-07-02 17:21:04 -070054import com.android.systemui.statusbar.notification.NotificationData;
Gus Prevas772e5322018-12-21 16:22:16 -050055import com.android.systemui.statusbar.notification.NotificationEntryListener;
Rohan Shah20790b82018-07-02 17:21:04 -070056import com.android.systemui.statusbar.notification.NotificationEntryManager;
57import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
Jason Monk297c04e2018-08-23 17:16:59 -040058import com.android.systemui.statusbar.phone.ShadeController;
Eliot Courtneye77edea2017-11-15 14:25:21 +090059import com.android.systemui.statusbar.policy.RemoteInputView;
60
61import java.io.FileDescriptor;
62import java.io.PrintWriter;
Kevina5ff1fa2018-08-21 16:35:48 -070063import java.util.ArrayList;
Eliot Courtneye77edea2017-11-15 14:25:21 +090064import java.util.Set;
65
Jason Monk27d01a622018-12-10 15:57:09 -050066import javax.inject.Inject;
Gus Prevas772e5322018-12-21 16:22:16 -050067import javax.inject.Named;
Jason Monk27d01a622018-12-10 15:57:09 -050068import javax.inject.Singleton;
69
Gus Prevas772e5322018-12-21 16:22:16 -050070import dagger.Lazy;
71
Eliot Courtneye77edea2017-11-15 14:25:21 +090072/**
73 * Class for handling remote input state over a set of notifications. This class handles things
74 * like keeping notifications temporarily that were cancelled as a response to a remote input
75 * interaction, keeping track of notifications to remove when NotificationPresenter is collapsed,
76 * and handling clicks on remote views.
77 */
Jason Monk27d01a622018-12-10 15:57:09 -050078@Singleton
Eliot Courtneye77edea2017-11-15 14:25:21 +090079public class NotificationRemoteInputManager implements Dumpable {
80 public static final boolean ENABLE_REMOTE_INPUT =
81 SystemProperties.getBoolean("debug.enable_remote_input", true);
Kevina5ff1fa2018-08-21 16:35:48 -070082 public static boolean FORCE_REMOTE_INPUT_HISTORY =
Eliot Courtneye77edea2017-11-15 14:25:21 +090083 SystemProperties.getBoolean("debug.force_remoteinput_history", true);
84 private static final boolean DEBUG = false;
Kevina5ff1fa2018-08-21 16:35:48 -070085 private static final String TAG = "NotifRemoteInputManager";
Eliot Courtneye77edea2017-11-15 14:25:21 +090086
87 /**
88 * How long to wait before auto-dismissing a notification that was kept for remote input, and
89 * has now sent a remote input. We auto-dismiss, because the app may not see a reason to cancel
90 * these given that they technically don't exist anymore. We wait a bit in case the app issues
91 * an update.
92 */
93 private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
94
Kevina5ff1fa2018-08-21 16:35:48 -070095 /**
96 * Notifications that are already removed but are kept around because we want to show the
97 * remote input history. See {@link RemoteInputHistoryExtender} and
98 * {@link SmartReplyHistoryExtender}.
99 */
100 protected final ArraySet<String> mKeysKeptForRemoteInputHistory = new ArraySet<>();
101
102 /**
103 * Notifications that are already removed but are kept around because the remote input is
104 * actively being used (i.e. user is typing in it). See {@link RemoteInputActiveExtender}.
105 */
106 protected final ArraySet<NotificationData.Entry> mEntriesKeptForRemoteInputActive =
Eliot Courtneye77edea2017-11-15 14:25:21 +0900107 new ArraySet<>();
Eliot Courtney6c313d32017-12-14 19:57:51 +0900108
109 // Dependencies:
Gus Prevas772e5322018-12-21 16:22:16 -0500110 private final NotificationLockscreenUserManager mLockscreenUserManager;
111 private final SmartReplyController mSmartReplyController;
112 private final NotificationEntryManager mEntryManager;
113 private final Handler mMainHandler;
Jason Monk297c04e2018-08-23 17:16:59 -0400114
Gus Prevas772e5322018-12-21 16:22:16 -0500115 private final Lazy<ShadeController> mShadeController;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900116
Eliot Courtneye77edea2017-11-15 14:25:21 +0900117 protected final Context mContext;
118 private final UserManager mUserManager;
Jason Monk297c04e2018-08-23 17:16:59 -0400119 private final KeyguardManager mKeyguardManager;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900120
121 protected RemoteInputController mRemoteInputController;
Kevina5ff1fa2018-08-21 16:35:48 -0700122 protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback
123 mNotificationLifetimeFinishedCallback;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900124 protected IStatusBarService mBarService;
125 protected Callback mCallback;
Kevina5ff1fa2018-08-21 16:35:48 -0700126 protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
Eliot Courtneye77edea2017-11-15 14:25:21 +0900127
128 private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() {
129
130 @Override
131 public boolean onClickHandler(
Sunny Goyal43c97042018-08-23 15:21:26 -0700132 View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) {
Gus Prevas772e5322018-12-21 16:22:16 -0500133 mShadeController.get().wakeUpIfDozing(SystemClock.uptimeMillis(), view);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900134
135 if (handleRemoteInput(view, pendingIntent)) {
136 return true;
137 }
138
139 if (DEBUG) {
140 Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
141 }
142 logActionClick(view);
143 // The intent we are sending is for the application, which
144 // won't have permission to immediately start an activity after
145 // the user switches to home. We know it is safe to do at this
146 // point, so make sure new activity switches are now allowed.
147 try {
148 ActivityManager.getService().resumeAppSwitches();
149 } catch (RemoteException e) {
150 }
yoshiki iguchi67c166d2018-11-26 13:11:36 +0900151 return mCallback.handleRemoteViewClick(view, pendingIntent, () -> {
Sunny Goyal43c97042018-08-23 15:21:26 -0700152 Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
153 options.second.setLaunchWindowingMode(
154 WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
155 return RemoteViews.startPendingIntent(view, pendingIntent, options);
156 });
Eliot Courtneye77edea2017-11-15 14:25:21 +0900157 }
158
159 private void logActionClick(View view) {
Tony Mak7d4b3a52018-11-27 17:29:36 +0000160 Integer actionIndex = (Integer)
161 view.getTag(com.android.internal.R.id.notification_action_index_tag);
162 if (actionIndex == null) {
163 Log.e(TAG, "Couldn't retrieve the actionIndex from the clicked button");
164 return;
165 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900166 ViewParent parent = view.getParent();
Tony Mak7d4b3a52018-11-27 17:29:36 +0000167 StatusBarNotification statusBarNotification = getNotificationForParent(parent);
168 if (statusBarNotification == null) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900169 Log.w(TAG, "Couldn't determine notification for click.");
170 return;
171 }
Tony Mak7d4b3a52018-11-27 17:29:36 +0000172 String key = statusBarNotification.getKey();
173 int buttonIndex = -1;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900174 // If this is a default template, determine the index of the button.
175 if (view.getId() == com.android.internal.R.id.action0 &&
176 parent != null && parent instanceof ViewGroup) {
177 ViewGroup actionGroup = (ViewGroup) parent;
Tony Mak7d4b3a52018-11-27 17:29:36 +0000178 buttonIndex = actionGroup.indexOfChild(view);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900179 }
Dieter Hsud39f0d52018-04-14 02:08:30 +0800180 final int count = mEntryManager.getNotificationData().getActiveNotifications().size();
181 final int rank = mEntryManager.getNotificationData().getRank(key);
Tony Mak7d4b3a52018-11-27 17:29:36 +0000182 final Notification.Action action =
183 statusBarNotification.getNotification().actions[actionIndex];
Dieter Hsud39f0d52018-04-14 02:08:30 +0800184 final NotificationVisibility nv = NotificationVisibility.obtain(key, rank, count, true);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900185 try {
Tony Mak7d4b3a52018-11-27 17:29:36 +0000186 mBarService.onNotificationActionClick(key, buttonIndex, action, nv, false);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900187 } catch (RemoteException e) {
188 // Ignore
189 }
190 }
191
Tony Mak7d4b3a52018-11-27 17:29:36 +0000192 private StatusBarNotification getNotificationForParent(ViewParent parent) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900193 while (parent != null) {
194 if (parent instanceof ExpandableNotificationRow) {
Tony Mak7d4b3a52018-11-27 17:29:36 +0000195 return ((ExpandableNotificationRow) parent).getStatusBarNotification();
Eliot Courtneye77edea2017-11-15 14:25:21 +0900196 }
197 parent = parent.getParent();
198 }
199 return null;
200 }
201
Eliot Courtneye77edea2017-11-15 14:25:21 +0900202 private boolean handleRemoteInput(View view, PendingIntent pendingIntent) {
203 if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) {
204 return true;
205 }
206
207 Object tag = view.getTag(com.android.internal.R.id.remote_input_tag);
208 RemoteInput[] inputs = null;
209 if (tag instanceof RemoteInput[]) {
210 inputs = (RemoteInput[]) tag;
211 }
212
213 if (inputs == null) {
214 return false;
215 }
216
217 RemoteInput input = null;
218
219 for (RemoteInput i : inputs) {
220 if (i.getAllowFreeFormInput()) {
221 input = i;
222 }
223 }
224
225 if (input == null) {
226 return false;
227 }
228
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000229 return activateRemoteInput(view, inputs, input, pendingIntent);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900230 }
231 };
232
Jason Monk27d01a622018-12-10 15:57:09 -0500233 @Inject
Gus Prevas772e5322018-12-21 16:22:16 -0500234 public NotificationRemoteInputManager(
235 Context context,
236 NotificationLockscreenUserManager lockscreenUserManager,
237 SmartReplyController smartReplyController,
238 NotificationEntryManager notificationEntryManager,
239 Lazy<ShadeController> shadeController,
240 @Named(MAIN_HANDLER_NAME) Handler mainHandler) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900241 mContext = context;
Gus Prevas772e5322018-12-21 16:22:16 -0500242 mLockscreenUserManager = lockscreenUserManager;
243 mSmartReplyController = smartReplyController;
244 mEntryManager = notificationEntryManager;
245 mShadeController = shadeController;
246 mMainHandler = mainHandler;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900247 mBarService = IStatusBarService.Stub.asInterface(
248 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
249 mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
Kevina5ff1fa2018-08-21 16:35:48 -0700250 addLifetimeExtenders();
Jason Monk297c04e2018-08-23 17:16:59 -0400251 mKeyguardManager = context.getSystemService(KeyguardManager.class);
Gus Prevas772e5322018-12-21 16:22:16 -0500252
253 notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
254 @Override
255 public void onEntryRemoved(
Ned Burns1dd6b402019-01-02 15:25:23 -0500256 @Nullable NotificationData.Entry entry,
Gus Prevas772e5322018-12-21 16:22:16 -0500257 String key,
258 StatusBarNotification old,
Gus Prevasca1b6f72018-12-28 10:53:11 -0500259 NotificationVisibility visibility,
Gus Prevas772e5322018-12-21 16:22:16 -0500260 boolean lifetimeExtended,
261 boolean removedByUser) {
Ned Burns1dd6b402019-01-02 15:25:23 -0500262 if (removedByUser && entry != null) {
Gus Prevas772e5322018-12-21 16:22:16 -0500263 onPerformRemoveNotification(entry, key);
264 }
265 }
266 });
Eliot Courtneye77edea2017-11-15 14:25:21 +0900267 }
268
Gus Prevas21437b32018-12-05 10:36:13 -0500269 /** Initializes this component with the provided dependencies. */
270 public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900271 mCallback = callback;
272 mRemoteInputController = new RemoteInputController(delegate);
273 mRemoteInputController.addCallback(new RemoteInputController.Callback() {
274 @Override
275 public void onRemoteInputSent(NotificationData.Entry entry) {
Kenny Guy8cc15d22018-05-09 09:50:55 +0100276 if (FORCE_REMOTE_INPUT_HISTORY
Kevina5ff1fa2018-08-21 16:35:48 -0700277 && isNotificationKeptForRemoteInputHistory(entry.key)) {
278 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
279 } else if (mEntriesKeptForRemoteInputActive.contains(entry)) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900280 // We're currently holding onto this notification, but from the apps point of
281 // view it is already canceled, so we'll need to cancel it on the apps behalf
282 // after sending - unless the app posts an update in the mean time, so wait a
283 // bit.
Gus Prevas772e5322018-12-21 16:22:16 -0500284 mMainHandler.postDelayed(() -> {
Kevina5ff1fa2018-08-21 16:35:48 -0700285 if (mEntriesKeptForRemoteInputActive.remove(entry)) {
286 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900287 }
288 }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
289 }
Amith Yamasani129b4812018-02-08 13:55:59 -0800290 try {
291 mBarService.onNotificationDirectReplied(entry.notification.getKey());
292 } catch (RemoteException e) {
293 // Nothing to do, system going down
294 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900295 }
296 });
Kevina5ff1fa2018-08-21 16:35:48 -0700297 mSmartReplyController.setCallback((entry, reply) -> {
298 StatusBarNotification newSbn =
299 rebuildNotificationWithRemoteInput(entry, reply, true /* showSpinner */);
300 mEntryManager.updateNotification(newSbn, null /* ranking */);
301 });
302 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900303
Kevina5ff1fa2018-08-21 16:35:48 -0700304 /**
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000305 * Activates a given {@link RemoteInput}
306 *
307 * @param view The view of the action button or suggestion chip that was tapped.
308 * @param inputs The remote inputs that need to be sent to the app.
309 * @param input The remote input that needs to be activated.
310 * @param pendingIntent The pending intent to be sent to the app.
311 * @return Whether the {@link RemoteInput} was activated.
312 */
313 public boolean activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input,
314 PendingIntent pendingIntent) {
315
316 ViewParent p = view.getParent();
317 RemoteInputView riv = null;
318 while (p != null) {
319 if (p instanceof View) {
320 View pv = (View) p;
321 if (pv.isRootNamespace()) {
322 riv = findRemoteInputView(pv);
323 break;
324 }
325 }
326 p = p.getParent();
327 }
328 ExpandableNotificationRow row = null;
329 while (p != null) {
330 if (p instanceof ExpandableNotificationRow) {
331 row = (ExpandableNotificationRow) p;
332 break;
333 }
334 p = p.getParent();
335 }
336
337 if (row == null) {
338 return false;
339 }
340
341 row.setUserExpanded(true);
342
343 if (!mLockscreenUserManager.shouldAllowLockscreenRemoteInput()) {
344 final int userId = pendingIntent.getCreatorUserHandle().getIdentifier();
345 if (mLockscreenUserManager.isLockscreenPublicMode(userId)) {
346 mCallback.onLockedRemoteInput(row, view);
347 return true;
348 }
349 if (mUserManager.getUserInfo(userId).isManagedProfile()
350 && mKeyguardManager.isDeviceLocked(userId)) {
351 mCallback.onLockedWorkRemoteInput(userId, row, view);
352 return true;
353 }
354 }
355
356 if (riv == null) {
357 riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild());
358 if (riv == null) {
359 return false;
360 }
361 if (!row.getPrivateLayout().getExpandedChild().isShown()) {
362 mCallback.onMakeExpandedVisibleForRemoteInput(row, view);
363 return true;
364 }
365 }
366
367 int width = view.getWidth();
368 if (view instanceof TextView) {
369 // Center the reveal on the text which might be off-center from the TextView
370 TextView tv = (TextView) view;
371 if (tv.getLayout() != null) {
372 int innerWidth = (int) tv.getLayout().getLineWidth(0);
373 innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight();
374 width = Math.min(width, innerWidth);
375 }
376 }
377 int cx = view.getLeft() + width / 2;
378 int cy = view.getTop() + view.getHeight() / 2;
379 int w = riv.getWidth();
380 int h = riv.getHeight();
381 int r = Math.max(
382 Math.max(cx + cy, cx + (h - cy)),
383 Math.max((w - cx) + cy, (w - cx) + (h - cy)));
384
385 riv.setRevealParameters(cx, cy, r);
386 riv.setPendingIntent(pendingIntent);
387 riv.setRemoteInput(inputs, input);
388 riv.focusAnimated();
389
390 return true;
391 }
392
393 private RemoteInputView findRemoteInputView(View v) {
394 if (v == null) {
395 return null;
396 }
397 return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG);
398 }
399
400 /**
Kevina5ff1fa2018-08-21 16:35:48 -0700401 * Adds all the notification lifetime extenders. Each extender represents a reason for the
402 * NotificationRemoteInputManager to keep a notification lifetime extended.
403 */
404 protected void addLifetimeExtenders() {
405 mLifetimeExtenders.add(new RemoteInputHistoryExtender());
406 mLifetimeExtenders.add(new SmartReplyHistoryExtender());
407 mLifetimeExtenders.add(new RemoteInputActiveExtender());
408 }
409
410 public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() {
411 return mLifetimeExtenders;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900412 }
413
414 public RemoteInputController getController() {
415 return mRemoteInputController;
416 }
417
Gus Prevas772e5322018-12-21 16:22:16 -0500418 @VisibleForTesting
419 void onPerformRemoveNotification(NotificationData.Entry entry, final String key) {
420 if (mKeysKeptForRemoteInputHistory.contains(key)) {
421 mKeysKeptForRemoteInputHistory.remove(key);
Kevina5ff1fa2018-08-21 16:35:48 -0700422 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900423 if (mRemoteInputController.isRemoteInputActive(entry)) {
424 mRemoteInputController.removeRemoteInput(entry, null);
425 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900426 }
427
Kevina5ff1fa2018-08-21 16:35:48 -0700428 public void onPanelCollapsed() {
429 for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) {
430 NotificationData.Entry entry = mEntriesKeptForRemoteInputActive.valueAt(i);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900431 mRemoteInputController.removeRemoteInput(entry, null);
Kevina5ff1fa2018-08-21 16:35:48 -0700432 if (mNotificationLifetimeFinishedCallback != null) {
433 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
434 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900435 }
Kevina5ff1fa2018-08-21 16:35:48 -0700436 mEntriesKeptForRemoteInputActive.clear();
437 }
438
439 public boolean isNotificationKeptForRemoteInputHistory(String key) {
440 return mKeysKeptForRemoteInputHistory.contains(key);
441 }
442
443 public boolean shouldKeepForRemoteInputHistory(NotificationData.Entry entry) {
Evan Laird94492852018-10-25 13:43:01 -0400444 if (entry.isDismissed()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700445 return false;
446 }
447 if (!FORCE_REMOTE_INPUT_HISTORY) {
448 return false;
449 }
450 return (mRemoteInputController.isSpinning(entry.key) || entry.hasJustSentRemoteInput());
451 }
452
453 public boolean shouldKeepForSmartReplyHistory(NotificationData.Entry entry) {
Evan Laird94492852018-10-25 13:43:01 -0400454 if (entry.isDismissed()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700455 return false;
456 }
457 if (!FORCE_REMOTE_INPUT_HISTORY) {
458 return false;
459 }
460 return mSmartReplyController.isSendingSmartReply(entry.key);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900461 }
462
463 public void checkRemoteInputOutside(MotionEvent event) {
464 if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
465 && event.getX() == 0 && event.getY() == 0 // a touch outside both bars
466 && mRemoteInputController.isRemoteInputActive()) {
467 mRemoteInputController.closeRemoteInputs();
468 }
469 }
470
Kevina5ff1fa2018-08-21 16:35:48 -0700471 @VisibleForTesting
472 StatusBarNotification rebuildNotificationForCanceledSmartReplies(
473 NotificationData.Entry entry) {
474 return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */,
475 false /* showSpinner */);
476 }
477
478 @VisibleForTesting
479 StatusBarNotification rebuildNotificationWithRemoteInput(NotificationData.Entry entry,
480 CharSequence remoteInputText, boolean showSpinner) {
481 StatusBarNotification sbn = entry.notification;
482
483 Notification.Builder b = Notification.Builder
484 .recoverBuilder(mContext, sbn.getNotification().clone());
485 if (remoteInputText != null) {
486 CharSequence[] oldHistory = sbn.getNotification().extras
487 .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
488 CharSequence[] newHistory;
489 if (oldHistory == null) {
490 newHistory = new CharSequence[1];
491 } else {
492 newHistory = new CharSequence[oldHistory.length + 1];
493 System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length);
494 }
495 newHistory[0] = String.valueOf(remoteInputText);
496 b.setRemoteInputHistory(newHistory);
497 }
498 b.setShowRemoteInputSpinner(showSpinner);
499 b.setHideSmartReplies(true);
500
501 Notification newNotification = b.build();
502
503 // Undo any compatibility view inflation
504 newNotification.contentView = sbn.getNotification().contentView;
505 newNotification.bigContentView = sbn.getNotification().bigContentView;
506 newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
507
508 return new StatusBarNotification(
509 sbn.getPackageName(),
510 sbn.getOpPkg(),
511 sbn.getId(),
512 sbn.getTag(),
513 sbn.getUid(),
514 sbn.getInitialPid(),
515 newNotification,
516 sbn.getUser(),
517 sbn.getOverrideGroupKey(),
518 sbn.getPostTime());
519 }
520
Eliot Courtneye77edea2017-11-15 14:25:21 +0900521 @Override
522 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
523 pw.println("NotificationRemoteInputManager state:");
Kevina5ff1fa2018-08-21 16:35:48 -0700524 pw.print(" mKeysKeptForRemoteInputHistory: ");
525 pw.println(mKeysKeptForRemoteInputHistory);
526 pw.print(" mEntriesKeptForRemoteInputActive: ");
527 pw.println(mEntriesKeptForRemoteInputActive);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900528 }
529
530 public void bindRow(ExpandableNotificationRow row) {
531 row.setRemoteInputController(mRemoteInputController);
532 row.setRemoteViewClickHandler(mOnClickHandler);
533 }
534
Eliot Courtneye77edea2017-11-15 14:25:21 +0900535 @VisibleForTesting
Kevina5ff1fa2018-08-21 16:35:48 -0700536 public Set<NotificationData.Entry> getEntriesKeptForRemoteInputActive() {
537 return mEntriesKeptForRemoteInputActive;
538 }
539
540 /**
541 * NotificationRemoteInputManager has multiple reasons to keep notification lifetime extended
542 * so we implement multiple NotificationLifetimeExtenders
543 */
544 protected abstract class RemoteInputExtender implements NotificationLifetimeExtender {
545 @Override
546 public void setCallback(NotificationSafeToRemoveCallback callback) {
547 if (mNotificationLifetimeFinishedCallback == null) {
548 mNotificationLifetimeFinishedCallback = callback;
549 }
550 }
551 }
552
553 /**
554 * Notification is kept alive as it was cancelled in response to a remote input interaction.
555 * This allows us to show what you replied and allows you to continue typing into it.
556 */
557 protected class RemoteInputHistoryExtender extends RemoteInputExtender {
558 @Override
559 public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) {
560 return shouldKeepForRemoteInputHistory(entry);
561 }
562
563 @Override
Kevine9e938c2018-09-06 13:38:11 -0700564 public void setShouldManageLifetime(NotificationData.Entry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700565 boolean shouldExtend) {
566 if (shouldExtend) {
567 CharSequence remoteInputText = entry.remoteInputText;
568 if (TextUtils.isEmpty(remoteInputText)) {
569 remoteInputText = entry.remoteInputTextWhenReset;
570 }
571 StatusBarNotification newSbn = rebuildNotificationWithRemoteInput(entry,
572 remoteInputText, false /* showSpinner */);
573 entry.onRemoteInputInserted();
574
575 if (newSbn == null) {
576 return;
577 }
578
579 mEntryManager.updateNotification(newSbn, null);
580
581 // Ensure the entry hasn't already been removed. This can happen if there is an
582 // inflation exception while updating the remote history
Evan Laird94492852018-10-25 13:43:01 -0400583 if (entry.isRemoved()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700584 return;
585 }
586
587 if (Log.isLoggable(TAG, Log.DEBUG)) {
588 Log.d(TAG, "Keeping notification around after sending remote input "
589 + entry.key);
590 }
591
592 mKeysKeptForRemoteInputHistory.add(entry.key);
593 } else {
594 mKeysKeptForRemoteInputHistory.remove(entry.key);
595 }
596 }
597 }
598
599 /**
600 * Notification is kept alive for smart reply history. Similar to REMOTE_INPUT_HISTORY but with
601 * {@link SmartReplyController} specific logic
602 */
603 protected class SmartReplyHistoryExtender extends RemoteInputExtender {
604 @Override
605 public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) {
606 return shouldKeepForSmartReplyHistory(entry);
607 }
608
609 @Override
Kevine9e938c2018-09-06 13:38:11 -0700610 public void setShouldManageLifetime(NotificationData.Entry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700611 boolean shouldExtend) {
612 if (shouldExtend) {
613 StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);
614
615 if (newSbn == null) {
616 return;
617 }
618
619 mEntryManager.updateNotification(newSbn, null);
620
Evan Laird94492852018-10-25 13:43:01 -0400621 if (entry.isRemoved()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700622 return;
623 }
624
625 if (Log.isLoggable(TAG, Log.DEBUG)) {
626 Log.d(TAG, "Keeping notification around after sending smart reply "
627 + entry.key);
628 }
629
630 mKeysKeptForRemoteInputHistory.add(entry.key);
631 } else {
632 mKeysKeptForRemoteInputHistory.remove(entry.key);
633 mSmartReplyController.stopSending(entry);
634 }
635 }
636 }
637
638 /**
639 * Notification is kept alive because the user is still using the remote input
640 */
641 protected class RemoteInputActiveExtender extends RemoteInputExtender {
642 @Override
643 public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) {
Evan Laird94492852018-10-25 13:43:01 -0400644 if (entry.isDismissed()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700645 return false;
646 }
647 return mRemoteInputController.isRemoteInputActive(entry);
648 }
649
650 @Override
Kevine9e938c2018-09-06 13:38:11 -0700651 public void setShouldManageLifetime(NotificationData.Entry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700652 boolean shouldExtend) {
653 if (shouldExtend) {
654 if (Log.isLoggable(TAG, Log.DEBUG)) {
655 Log.d(TAG, "Keeping notification around while remote input active "
656 + entry.key);
657 }
658 mEntriesKeptForRemoteInputActive.add(entry);
659 } else {
660 mEntriesKeptForRemoteInputActive.remove(entry);
661 }
662 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900663 }
664
665 /**
666 * Callback for various remote input related events, or for providing information that
667 * NotificationRemoteInputManager needs to know to decide what to do.
668 */
669 public interface Callback {
670
671 /**
672 * Called when remote input was activated but the device is locked.
673 *
674 * @param row
675 * @param clicked
676 */
677 void onLockedRemoteInput(ExpandableNotificationRow row, View clicked);
678
679 /**
680 * Called when remote input was activated but the device is locked and in a managed profile.
681 *
682 * @param userId
683 * @param row
684 * @param clicked
685 */
686 void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked);
687
688 /**
689 * Called when a row should be made expanded for the purposes of remote input.
690 *
691 * @param row
692 * @param clickedView
693 */
694 void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView);
695
696 /**
697 * Return whether or not remote input should be handled for this view.
698 *
699 * @param view
700 * @param pendingIntent
701 * @return true iff the remote input should be handled
702 */
703 boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent);
704
705 /**
706 * Performs any special handling for a remote view click. The default behaviour can be
707 * called through the defaultHandler parameter.
708 *
yoshiki iguchi67c166d2018-11-26 13:11:36 +0900709 * @param view
Eliot Courtneye77edea2017-11-15 14:25:21 +0900710 * @param pendingIntent
Eliot Courtneye77edea2017-11-15 14:25:21 +0900711 * @param defaultHandler
712 * @return true iff the click was handled
713 */
yoshiki iguchi67c166d2018-11-26 13:11:36 +0900714 boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
715 ClickHandler defaultHandler);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900716 }
717
718 /**
719 * Helper interface meant for passing the default on click behaviour to NotificationPresenter,
720 * so it may do its own handling before invoking the default behaviour.
721 */
722 public interface ClickHandler {
723 /**
724 * Tries to handle a click on a remote view.
725 *
726 * @return true iff the click was handled
727 */
728 boolean handleClick();
729 }
730}