blob: c838ac5315a79e2b9bc3482ea8566a4b85c11349 [file] [log] [blame]
Eliot Courtneye77edea2017-11-15 14:25:21 +09001/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16package com.android.systemui.statusbar;
17
18import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;
19
Kevina5ff1fa2018-08-21 16:35:48 -070020import android.annotation.NonNull;
Ned Burns1dd6b402019-01-02 15:25:23 -050021import android.annotation.Nullable;
Eliot Courtneye77edea2017-11-15 14:25:21 +090022import android.app.ActivityManager;
Sunny Goyal02794532018-08-22 15:18:37 -070023import android.app.ActivityOptions;
Jason Monk297c04e2018-08-23 17:16:59 -040024import android.app.KeyguardManager;
Kevina5ff1fa2018-08-21 16:35:48 -070025import android.app.Notification;
Eliot Courtneye77edea2017-11-15 14:25:21 +090026import android.app.PendingIntent;
27import android.app.RemoteInput;
28import android.content.Context;
29import android.content.Intent;
Gus Prevas772e5322018-12-21 16:22:16 -050030import android.os.Handler;
Eliot Courtneye77edea2017-11-15 14:25:21 +090031import android.os.RemoteException;
32import android.os.ServiceManager;
33import android.os.SystemClock;
34import android.os.SystemProperties;
35import android.os.UserManager;
36import android.service.notification.StatusBarNotification;
Kevina5ff1fa2018-08-21 16:35:48 -070037import android.text.TextUtils;
Eliot Courtneye77edea2017-11-15 14:25:21 +090038import android.util.ArraySet;
39import android.util.Log;
Sunny Goyal43c97042018-08-23 15:21:26 -070040import android.util.Pair;
Eliot Courtneye77edea2017-11-15 14:25:21 +090041import android.view.MotionEvent;
42import android.view.View;
43import android.view.ViewGroup;
44import android.view.ViewParent;
45import android.widget.RemoteViews;
46import android.widget.TextView;
47
48import com.android.internal.annotations.VisibleForTesting;
49import com.android.internal.statusbar.IStatusBarService;
Dieter Hsud39f0d52018-04-14 02:08:30 +080050import com.android.internal.statusbar.NotificationVisibility;
Eliot Courtneye77edea2017-11-15 14:25:21 +090051import com.android.systemui.Dumpable;
Selim Cinekf7d88932019-05-01 17:31:25 -070052import com.android.systemui.R;
Dave Mankofff4736812019-10-18 17:25:50 -040053import com.android.systemui.dagger.qualifiers.MainHandler;
Selim Cinekd17b3502019-07-02 20:38:32 -070054import com.android.systemui.plugins.statusbar.StatusBarStateController;
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;
Ned Burnsf81c4c42019-01-07 14:10:43 -050057import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Milo Sredkov13d88112019-02-01 12:23:24 +000058import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo;
Gustav Senntonf892fe92019-01-22 15:31:42 +000059import com.android.systemui.statusbar.notification.logging.NotificationLogger;
Rohan Shah20790b82018-07-02 17:21:04 -070060import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
Jason Monk297c04e2018-08-23 17:16:59 -040061import com.android.systemui.statusbar.phone.ShadeController;
Eliot Courtneye77edea2017-11-15 14:25:21 +090062import com.android.systemui.statusbar.policy.RemoteInputView;
63
64import java.io.FileDescriptor;
65import java.io.PrintWriter;
Kevina5ff1fa2018-08-21 16:35:48 -070066import java.util.ArrayList;
Tony Mak4fce1202019-03-27 17:03:45 +000067import java.util.Objects;
Eliot Courtneye77edea2017-11-15 14:25:21 +090068import java.util.Set;
69
Jason Monk27d01a622018-12-10 15:57:09 -050070import javax.inject.Inject;
71import javax.inject.Singleton;
72
Gus Prevas772e5322018-12-21 16:22:16 -050073import dagger.Lazy;
74
Eliot Courtneye77edea2017-11-15 14:25:21 +090075/**
76 * Class for handling remote input state over a set of notifications. This class handles things
77 * like keeping notifications temporarily that were cancelled as a response to a remote input
78 * interaction, keeping track of notifications to remove when NotificationPresenter is collapsed,
79 * and handling clicks on remote views.
80 */
Jason Monk27d01a622018-12-10 15:57:09 -050081@Singleton
Eliot Courtneye77edea2017-11-15 14:25:21 +090082public class NotificationRemoteInputManager implements Dumpable {
83 public static final boolean ENABLE_REMOTE_INPUT =
84 SystemProperties.getBoolean("debug.enable_remote_input", true);
Kevina5ff1fa2018-08-21 16:35:48 -070085 public static boolean FORCE_REMOTE_INPUT_HISTORY =
Eliot Courtneye77edea2017-11-15 14:25:21 +090086 SystemProperties.getBoolean("debug.force_remoteinput_history", true);
87 private static final boolean DEBUG = false;
Kevina5ff1fa2018-08-21 16:35:48 -070088 private static final String TAG = "NotifRemoteInputManager";
Eliot Courtneye77edea2017-11-15 14:25:21 +090089
90 /**
91 * How long to wait before auto-dismissing a notification that was kept for remote input, and
92 * has now sent a remote input. We auto-dismiss, because the app may not see a reason to cancel
93 * these given that they technically don't exist anymore. We wait a bit in case the app issues
94 * an update.
95 */
96 private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
97
Kevina5ff1fa2018-08-21 16:35:48 -070098 /**
99 * Notifications that are already removed but are kept around because we want to show the
100 * remote input history. See {@link RemoteInputHistoryExtender} and
101 * {@link SmartReplyHistoryExtender}.
102 */
103 protected final ArraySet<String> mKeysKeptForRemoteInputHistory = new ArraySet<>();
104
105 /**
106 * Notifications that are already removed but are kept around because the remote input is
107 * actively being used (i.e. user is typing in it). See {@link RemoteInputActiveExtender}.
108 */
Ned Burnsf81c4c42019-01-07 14:10:43 -0500109 protected final ArraySet<NotificationEntry> mEntriesKeptForRemoteInputActive =
Eliot Courtneye77edea2017-11-15 14:25:21 +0900110 new ArraySet<>();
Eliot Courtney6c313d32017-12-14 19:57:51 +0900111
112 // Dependencies:
Gus Prevas772e5322018-12-21 16:22:16 -0500113 private final NotificationLockscreenUserManager mLockscreenUserManager;
114 private final SmartReplyController mSmartReplyController;
115 private final NotificationEntryManager mEntryManager;
116 private final Handler mMainHandler;
Jason Monk297c04e2018-08-23 17:16:59 -0400117
Gus Prevas772e5322018-12-21 16:22:16 -0500118 private final Lazy<ShadeController> mShadeController;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900119
Eliot Courtneye77edea2017-11-15 14:25:21 +0900120 protected final Context mContext;
121 private final UserManager mUserManager;
Jason Monk297c04e2018-08-23 17:16:59 -0400122 private final KeyguardManager mKeyguardManager;
Selim Cinekd17b3502019-07-02 20:38:32 -0700123 private final StatusBarStateController mStatusBarStateController;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900124
125 protected RemoteInputController mRemoteInputController;
Kevina5ff1fa2018-08-21 16:35:48 -0700126 protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback
127 mNotificationLifetimeFinishedCallback;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900128 protected IStatusBarService mBarService;
129 protected Callback mCallback;
Kevina5ff1fa2018-08-21 16:35:48 -0700130 protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
Eliot Courtneye77edea2017-11-15 14:25:21 +0900131
132 private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() {
133
134 @Override
135 public boolean onClickHandler(
Sunny Goyal43c97042018-08-23 15:21:26 -0700136 View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) {
Michael Wrighte3001042019-02-05 00:13:14 +0000137 mShadeController.get().wakeUpIfDozing(SystemClock.uptimeMillis(), view,
138 "NOTIFICATION_CLICK");
Eliot Courtneye77edea2017-11-15 14:25:21 +0900139
140 if (handleRemoteInput(view, pendingIntent)) {
141 return true;
142 }
143
144 if (DEBUG) {
145 Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
146 }
Tony Mak4fce1202019-03-27 17:03:45 +0000147 logActionClick(view, pendingIntent);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900148 // The intent we are sending is for the application, which
149 // won't have permission to immediately start an activity after
150 // the user switches to home. We know it is safe to do at this
151 // point, so make sure new activity switches are now allowed.
152 try {
153 ActivityManager.getService().resumeAppSwitches();
154 } catch (RemoteException e) {
155 }
yoshiki iguchi67c166d2018-11-26 13:11:36 +0900156 return mCallback.handleRemoteViewClick(view, pendingIntent, () -> {
Sunny Goyal43c97042018-08-23 15:21:26 -0700157 Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
158 options.second.setLaunchWindowingMode(
159 WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
160 return RemoteViews.startPendingIntent(view, pendingIntent, options);
161 });
Eliot Courtneye77edea2017-11-15 14:25:21 +0900162 }
163
Tony Mak4fce1202019-03-27 17:03:45 +0000164 private void logActionClick(View view, PendingIntent actionIntent) {
Tony Mak7d4b3a52018-11-27 17:29:36 +0000165 Integer actionIndex = (Integer)
166 view.getTag(com.android.internal.R.id.notification_action_index_tag);
167 if (actionIndex == null) {
Tony Mak4fce1202019-03-27 17:03:45 +0000168 // Custom action button, not logging.
Tony Mak7d4b3a52018-11-27 17:29:36 +0000169 return;
170 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900171 ViewParent parent = view.getParent();
Tony Mak7d4b3a52018-11-27 17:29:36 +0000172 StatusBarNotification statusBarNotification = getNotificationForParent(parent);
173 if (statusBarNotification == null) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900174 Log.w(TAG, "Couldn't determine notification for click.");
175 return;
176 }
Tony Mak7d4b3a52018-11-27 17:29:36 +0000177 String key = statusBarNotification.getKey();
178 int buttonIndex = -1;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900179 // If this is a default template, determine the index of the button.
180 if (view.getId() == com.android.internal.R.id.action0 &&
181 parent != null && parent instanceof ViewGroup) {
182 ViewGroup actionGroup = (ViewGroup) parent;
Tony Mak7d4b3a52018-11-27 17:29:36 +0000183 buttonIndex = actionGroup.indexOfChild(view);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900184 }
Dieter Hsud39f0d52018-04-14 02:08:30 +0800185 final int count = mEntryManager.getNotificationData().getActiveNotifications().size();
186 final int rank = mEntryManager.getNotificationData().getRank(key);
Tony Mak4fce1202019-03-27 17:03:45 +0000187
188 // Notification may be updated before this function is executed, and thus play safe
189 // here and verify that the action object is still the one that where the click happens.
190 Notification.Action[] actions = statusBarNotification.getNotification().actions;
191 if (actions == null || actionIndex >= actions.length) {
192 Log.w(TAG, "statusBarNotification.getNotification().actions is null or invalid");
193 return;
194 }
Tony Mak7d4b3a52018-11-27 17:29:36 +0000195 final Notification.Action action =
196 statusBarNotification.getNotification().actions[actionIndex];
Tony Mak621471f2019-05-25 08:42:37 +0100197 if (!Objects.equals(action.actionIntent, actionIntent)) {
Tony Mak4fce1202019-03-27 17:03:45 +0000198 Log.w(TAG, "actionIntent does not match");
199 return;
200 }
Gustav Senntonf892fe92019-01-22 15:31:42 +0000201 NotificationVisibility.NotificationLocation location =
202 NotificationLogger.getNotificationLocation(
203 mEntryManager.getNotificationData().get(key));
204 final NotificationVisibility nv =
205 NotificationVisibility.obtain(key, rank, count, true, location);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900206 try {
Tony Mak7d4b3a52018-11-27 17:29:36 +0000207 mBarService.onNotificationActionClick(key, buttonIndex, action, nv, false);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900208 } catch (RemoteException e) {
209 // Ignore
210 }
211 }
212
Tony Mak7d4b3a52018-11-27 17:29:36 +0000213 private StatusBarNotification getNotificationForParent(ViewParent parent) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900214 while (parent != null) {
215 if (parent instanceof ExpandableNotificationRow) {
Tony Mak7d4b3a52018-11-27 17:29:36 +0000216 return ((ExpandableNotificationRow) parent).getStatusBarNotification();
Eliot Courtneye77edea2017-11-15 14:25:21 +0900217 }
218 parent = parent.getParent();
219 }
220 return null;
221 }
222
Eliot Courtneye77edea2017-11-15 14:25:21 +0900223 private boolean handleRemoteInput(View view, PendingIntent pendingIntent) {
224 if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) {
225 return true;
226 }
227
228 Object tag = view.getTag(com.android.internal.R.id.remote_input_tag);
229 RemoteInput[] inputs = null;
230 if (tag instanceof RemoteInput[]) {
231 inputs = (RemoteInput[]) tag;
232 }
233
234 if (inputs == null) {
235 return false;
236 }
237
238 RemoteInput input = null;
239
240 for (RemoteInput i : inputs) {
241 if (i.getAllowFreeFormInput()) {
242 input = i;
243 }
244 }
245
246 if (input == null) {
247 return false;
248 }
249
Milo Sredkov13d88112019-02-01 12:23:24 +0000250 return activateRemoteInput(view, inputs, input, pendingIntent,
251 null /* editedSuggestionInfo */);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900252 }
253 };
254
Jason Monk27d01a622018-12-10 15:57:09 -0500255 @Inject
Gus Prevas772e5322018-12-21 16:22:16 -0500256 public NotificationRemoteInputManager(
257 Context context,
258 NotificationLockscreenUserManager lockscreenUserManager,
259 SmartReplyController smartReplyController,
260 NotificationEntryManager notificationEntryManager,
261 Lazy<ShadeController> shadeController,
Selim Cinekd17b3502019-07-02 20:38:32 -0700262 StatusBarStateController statusBarStateController,
Dave Mankofff4736812019-10-18 17:25:50 -0400263 @MainHandler Handler mainHandler) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900264 mContext = context;
Gus Prevas772e5322018-12-21 16:22:16 -0500265 mLockscreenUserManager = lockscreenUserManager;
266 mSmartReplyController = smartReplyController;
267 mEntryManager = notificationEntryManager;
268 mShadeController = shadeController;
269 mMainHandler = mainHandler;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900270 mBarService = IStatusBarService.Stub.asInterface(
271 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
272 mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
Kevina5ff1fa2018-08-21 16:35:48 -0700273 addLifetimeExtenders();
Jason Monk297c04e2018-08-23 17:16:59 -0400274 mKeyguardManager = context.getSystemService(KeyguardManager.class);
Selim Cinekd17b3502019-07-02 20:38:32 -0700275 mStatusBarStateController = statusBarStateController;
Gus Prevas772e5322018-12-21 16:22:16 -0500276
277 notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
278 @Override
Gustav Sennton36fc2662019-04-30 16:06:34 +0100279 public void onPreEntryUpdated(NotificationEntry entry) {
280 // Mark smart replies as sent whenever a notification is updated - otherwise the
281 // smart replies are never marked as sent.
282 mSmartReplyController.stopSending(entry);
283 }
284
285 @Override
Gus Prevas772e5322018-12-21 16:22:16 -0500286 public void onEntryRemoved(
Ned Burnsf81c4c42019-01-07 14:10:43 -0500287 @Nullable NotificationEntry entry,
Gus Prevasca1b6f72018-12-28 10:53:11 -0500288 NotificationVisibility visibility,
Gus Prevas772e5322018-12-21 16:22:16 -0500289 boolean removedByUser) {
Gustav Sennton36fc2662019-04-30 16:06:34 +0100290 // We're removing the notification, the smart controller can forget about it.
291 mSmartReplyController.stopSending(entry);
292
Ned Burns1dd6b402019-01-02 15:25:23 -0500293 if (removedByUser && entry != null) {
Ned Burns00b4b2d2019-10-17 22:09:27 -0400294 onPerformRemoveNotification(entry, entry.getKey());
Gus Prevas772e5322018-12-21 16:22:16 -0500295 }
296 }
297 });
Eliot Courtneye77edea2017-11-15 14:25:21 +0900298 }
299
Gus Prevas21437b32018-12-05 10:36:13 -0500300 /** Initializes this component with the provided dependencies. */
301 public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900302 mCallback = callback;
303 mRemoteInputController = new RemoteInputController(delegate);
304 mRemoteInputController.addCallback(new RemoteInputController.Callback() {
305 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500306 public void onRemoteInputSent(NotificationEntry entry) {
Kenny Guy8cc15d22018-05-09 09:50:55 +0100307 if (FORCE_REMOTE_INPUT_HISTORY
Ned Burns00b4b2d2019-10-17 22:09:27 -0400308 && isNotificationKeptForRemoteInputHistory(entry.getKey())) {
309 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
Kevina5ff1fa2018-08-21 16:35:48 -0700310 } else if (mEntriesKeptForRemoteInputActive.contains(entry)) {
Eliot Courtneye77edea2017-11-15 14:25:21 +0900311 // We're currently holding onto this notification, but from the apps point of
312 // view it is already canceled, so we'll need to cancel it on the apps behalf
313 // after sending - unless the app posts an update in the mean time, so wait a
314 // bit.
Gus Prevas772e5322018-12-21 16:22:16 -0500315 mMainHandler.postDelayed(() -> {
Kevina5ff1fa2018-08-21 16:35:48 -0700316 if (mEntriesKeptForRemoteInputActive.remove(entry)) {
Ned Burns00b4b2d2019-10-17 22:09:27 -0400317 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
Eliot Courtneye77edea2017-11-15 14:25:21 +0900318 }
319 }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
320 }
Amith Yamasani129b4812018-02-08 13:55:59 -0800321 try {
Ned Burns00b4b2d2019-10-17 22:09:27 -0400322 mBarService.onNotificationDirectReplied(entry.getSbn().getKey());
Milo Sredkov13d88112019-02-01 12:23:24 +0000323 if (entry.editedSuggestionInfo != null) {
324 boolean modifiedBeforeSending =
325 !TextUtils.equals(entry.remoteInputText,
326 entry.editedSuggestionInfo.originalText);
327 mBarService.onNotificationSmartReplySent(
Ned Burns00b4b2d2019-10-17 22:09:27 -0400328 entry.getSbn().getKey(),
Milo Sredkov13d88112019-02-01 12:23:24 +0000329 entry.editedSuggestionInfo.index,
330 entry.editedSuggestionInfo.originalText,
331 NotificationLogger
332 .getNotificationLocation(entry)
333 .toMetricsEventEnum(),
334 modifiedBeforeSending);
335 }
Amith Yamasani129b4812018-02-08 13:55:59 -0800336 } catch (RemoteException e) {
337 // Nothing to do, system going down
338 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900339 }
340 });
Kevina5ff1fa2018-08-21 16:35:48 -0700341 mSmartReplyController.setCallback((entry, reply) -> {
342 StatusBarNotification newSbn =
343 rebuildNotificationWithRemoteInput(entry, reply, true /* showSpinner */);
344 mEntryManager.updateNotification(newSbn, null /* ranking */);
345 });
346 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900347
Kevina5ff1fa2018-08-21 16:35:48 -0700348 /**
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000349 * Activates a given {@link RemoteInput}
350 *
351 * @param view The view of the action button or suggestion chip that was tapped.
352 * @param inputs The remote inputs that need to be sent to the app.
353 * @param input The remote input that needs to be activated.
354 * @param pendingIntent The pending intent to be sent to the app.
Milo Sredkov13d88112019-02-01 12:23:24 +0000355 * @param editedSuggestionInfo The smart reply that should be inserted in the remote input, or
356 * {@code null} if the user is not editing a smart reply.
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000357 * @return Whether the {@link RemoteInput} was activated.
358 */
359 public boolean activateRemoteInput(View view, RemoteInput[] inputs, RemoteInput input,
Milo Sredkov13d88112019-02-01 12:23:24 +0000360 PendingIntent pendingIntent, @Nullable EditedSuggestionInfo editedSuggestionInfo) {
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000361
362 ViewParent p = view.getParent();
363 RemoteInputView riv = null;
Selim Cinekf7d88932019-05-01 17:31:25 -0700364 ExpandableNotificationRow row = null;
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000365 while (p != null) {
366 if (p instanceof View) {
367 View pv = (View) p;
368 if (pv.isRootNamespace()) {
369 riv = findRemoteInputView(pv);
Selim Cinekf7d88932019-05-01 17:31:25 -0700370 row = (ExpandableNotificationRow) pv.getTag(R.id.row_tag_for_content_view);
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000371 break;
372 }
373 }
374 p = p.getParent();
375 }
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000376
377 if (row == null) {
378 return false;
379 }
380
381 row.setUserExpanded(true);
382
383 if (!mLockscreenUserManager.shouldAllowLockscreenRemoteInput()) {
384 final int userId = pendingIntent.getCreatorUserHandle().getIdentifier();
Selim Cinekd17b3502019-07-02 20:38:32 -0700385 if (mLockscreenUserManager.isLockscreenPublicMode(userId)
386 || mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
387 // Even if we don't have security we should go through this flow, otherwise we won't
388 // go to the shade
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000389 mCallback.onLockedRemoteInput(row, view);
390 return true;
391 }
392 if (mUserManager.getUserInfo(userId).isManagedProfile()
393 && mKeyguardManager.isDeviceLocked(userId)) {
394 mCallback.onLockedWorkRemoteInput(userId, row, view);
395 return true;
396 }
397 }
398
Selim Cinek19734232019-07-11 20:02:40 -0700399 if (riv != null && !riv.isAttachedToWindow()) {
Selim Cinekd17b3502019-07-02 20:38:32 -0700400 // the remoteInput isn't attached to the window anymore :/ Let's focus on the expanded
401 // one instead if it's available
402 riv = null;
403 }
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000404 if (riv == null) {
405 riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild());
406 if (riv == null) {
407 return false;
408 }
Selim Cinekf7d88932019-05-01 17:31:25 -0700409 }
410 if (riv == row.getPrivateLayout().getExpandedRemoteInput()
411 && !row.getPrivateLayout().getExpandedChild().isShown()) {
412 // The expanded layout is selected, but it's not shown yet, let's wait on it to
413 // show before we do the animation.
414 mCallback.onMakeExpandedVisibleForRemoteInput(row, view);
415 return true;
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000416 }
417
Selim Cinekd17b3502019-07-02 20:38:32 -0700418 if (!riv.isAttachedToWindow()) {
419 // if we still didn't find a view that is attached, let's abort.
420 return false;
421 }
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000422 int width = view.getWidth();
423 if (view instanceof TextView) {
424 // Center the reveal on the text which might be off-center from the TextView
425 TextView tv = (TextView) view;
426 if (tv.getLayout() != null) {
427 int innerWidth = (int) tv.getLayout().getLineWidth(0);
428 innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight();
429 width = Math.min(width, innerWidth);
430 }
431 }
432 int cx = view.getLeft() + width / 2;
433 int cy = view.getTop() + view.getHeight() / 2;
434 int w = riv.getWidth();
435 int h = riv.getHeight();
436 int r = Math.max(
437 Math.max(cx + cy, cx + (h - cy)),
438 Math.max((w - cx) + cy, (w - cx) + (h - cy)));
439
440 riv.setRevealParameters(cx, cy, r);
441 riv.setPendingIntent(pendingIntent);
Milo Sredkov13d88112019-02-01 12:23:24 +0000442 riv.setRemoteInput(inputs, input, editedSuggestionInfo);
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000443 riv.focusAnimated();
444
445 return true;
446 }
447
448 private RemoteInputView findRemoteInputView(View v) {
449 if (v == null) {
450 return null;
451 }
452 return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG);
453 }
454
455 /**
Kevina5ff1fa2018-08-21 16:35:48 -0700456 * Adds all the notification lifetime extenders. Each extender represents a reason for the
457 * NotificationRemoteInputManager to keep a notification lifetime extended.
458 */
459 protected void addLifetimeExtenders() {
460 mLifetimeExtenders.add(new RemoteInputHistoryExtender());
461 mLifetimeExtenders.add(new SmartReplyHistoryExtender());
462 mLifetimeExtenders.add(new RemoteInputActiveExtender());
463 }
464
465 public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() {
466 return mLifetimeExtenders;
Eliot Courtneye77edea2017-11-15 14:25:21 +0900467 }
468
469 public RemoteInputController getController() {
470 return mRemoteInputController;
471 }
472
Gus Prevas772e5322018-12-21 16:22:16 -0500473 @VisibleForTesting
Ned Burnsf81c4c42019-01-07 14:10:43 -0500474 void onPerformRemoveNotification(NotificationEntry entry, final String key) {
Gus Prevas772e5322018-12-21 16:22:16 -0500475 if (mKeysKeptForRemoteInputHistory.contains(key)) {
476 mKeysKeptForRemoteInputHistory.remove(key);
Kevina5ff1fa2018-08-21 16:35:48 -0700477 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900478 if (mRemoteInputController.isRemoteInputActive(entry)) {
479 mRemoteInputController.removeRemoteInput(entry, null);
480 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900481 }
482
Kevina5ff1fa2018-08-21 16:35:48 -0700483 public void onPanelCollapsed() {
484 for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500485 NotificationEntry entry = mEntriesKeptForRemoteInputActive.valueAt(i);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900486 mRemoteInputController.removeRemoteInput(entry, null);
Kevina5ff1fa2018-08-21 16:35:48 -0700487 if (mNotificationLifetimeFinishedCallback != null) {
Ned Burns00b4b2d2019-10-17 22:09:27 -0400488 mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
Kevina5ff1fa2018-08-21 16:35:48 -0700489 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900490 }
Kevina5ff1fa2018-08-21 16:35:48 -0700491 mEntriesKeptForRemoteInputActive.clear();
492 }
493
494 public boolean isNotificationKeptForRemoteInputHistory(String key) {
495 return mKeysKeptForRemoteInputHistory.contains(key);
496 }
497
Ned Burnsf81c4c42019-01-07 14:10:43 -0500498 public boolean shouldKeepForRemoteInputHistory(NotificationEntry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700499 if (!FORCE_REMOTE_INPUT_HISTORY) {
500 return false;
501 }
Ned Burns00b4b2d2019-10-17 22:09:27 -0400502 return (mRemoteInputController.isSpinning(entry.getKey())
503 || entry.hasJustSentRemoteInput());
Kevina5ff1fa2018-08-21 16:35:48 -0700504 }
505
Ned Burnsf81c4c42019-01-07 14:10:43 -0500506 public boolean shouldKeepForSmartReplyHistory(NotificationEntry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700507 if (!FORCE_REMOTE_INPUT_HISTORY) {
508 return false;
509 }
Ned Burns00b4b2d2019-10-17 22:09:27 -0400510 return mSmartReplyController.isSendingSmartReply(entry.getKey());
Eliot Courtneye77edea2017-11-15 14:25:21 +0900511 }
512
513 public void checkRemoteInputOutside(MotionEvent event) {
514 if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar
515 && event.getX() == 0 && event.getY() == 0 // a touch outside both bars
516 && mRemoteInputController.isRemoteInputActive()) {
517 mRemoteInputController.closeRemoteInputs();
518 }
519 }
520
Kevina5ff1fa2018-08-21 16:35:48 -0700521 @VisibleForTesting
522 StatusBarNotification rebuildNotificationForCanceledSmartReplies(
Ned Burnsf81c4c42019-01-07 14:10:43 -0500523 NotificationEntry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700524 return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */,
525 false /* showSpinner */);
526 }
527
528 @VisibleForTesting
Ned Burnsf81c4c42019-01-07 14:10:43 -0500529 StatusBarNotification rebuildNotificationWithRemoteInput(NotificationEntry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700530 CharSequence remoteInputText, boolean showSpinner) {
Ned Burns00b4b2d2019-10-17 22:09:27 -0400531 StatusBarNotification sbn = entry.getSbn();
Kevina5ff1fa2018-08-21 16:35:48 -0700532
533 Notification.Builder b = Notification.Builder
534 .recoverBuilder(mContext, sbn.getNotification().clone());
535 if (remoteInputText != null) {
536 CharSequence[] oldHistory = sbn.getNotification().extras
537 .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
538 CharSequence[] newHistory;
539 if (oldHistory == null) {
540 newHistory = new CharSequence[1];
541 } else {
542 newHistory = new CharSequence[oldHistory.length + 1];
543 System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length);
544 }
545 newHistory[0] = String.valueOf(remoteInputText);
546 b.setRemoteInputHistory(newHistory);
547 }
548 b.setShowRemoteInputSpinner(showSpinner);
549 b.setHideSmartReplies(true);
550
551 Notification newNotification = b.build();
552
553 // Undo any compatibility view inflation
554 newNotification.contentView = sbn.getNotification().contentView;
555 newNotification.bigContentView = sbn.getNotification().bigContentView;
556 newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;
557
558 return new StatusBarNotification(
559 sbn.getPackageName(),
560 sbn.getOpPkg(),
561 sbn.getId(),
562 sbn.getTag(),
563 sbn.getUid(),
564 sbn.getInitialPid(),
565 newNotification,
566 sbn.getUser(),
567 sbn.getOverrideGroupKey(),
568 sbn.getPostTime());
569 }
570
Eliot Courtneye77edea2017-11-15 14:25:21 +0900571 @Override
572 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
573 pw.println("NotificationRemoteInputManager state:");
Kevina5ff1fa2018-08-21 16:35:48 -0700574 pw.print(" mKeysKeptForRemoteInputHistory: ");
575 pw.println(mKeysKeptForRemoteInputHistory);
576 pw.print(" mEntriesKeptForRemoteInputActive: ");
577 pw.println(mEntriesKeptForRemoteInputActive);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900578 }
579
580 public void bindRow(ExpandableNotificationRow row) {
581 row.setRemoteInputController(mRemoteInputController);
582 row.setRemoteViewClickHandler(mOnClickHandler);
583 }
584
Eliot Courtneye77edea2017-11-15 14:25:21 +0900585 @VisibleForTesting
Ned Burnsf81c4c42019-01-07 14:10:43 -0500586 public Set<NotificationEntry> getEntriesKeptForRemoteInputActive() {
Kevina5ff1fa2018-08-21 16:35:48 -0700587 return mEntriesKeptForRemoteInputActive;
588 }
589
590 /**
591 * NotificationRemoteInputManager has multiple reasons to keep notification lifetime extended
592 * so we implement multiple NotificationLifetimeExtenders
593 */
594 protected abstract class RemoteInputExtender implements NotificationLifetimeExtender {
595 @Override
596 public void setCallback(NotificationSafeToRemoveCallback callback) {
597 if (mNotificationLifetimeFinishedCallback == null) {
598 mNotificationLifetimeFinishedCallback = callback;
599 }
600 }
601 }
602
603 /**
604 * Notification is kept alive as it was cancelled in response to a remote input interaction.
605 * This allows us to show what you replied and allows you to continue typing into it.
606 */
607 protected class RemoteInputHistoryExtender extends RemoteInputExtender {
608 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500609 public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700610 return shouldKeepForRemoteInputHistory(entry);
611 }
612
613 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500614 public void setShouldManageLifetime(NotificationEntry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700615 boolean shouldExtend) {
616 if (shouldExtend) {
617 CharSequence remoteInputText = entry.remoteInputText;
618 if (TextUtils.isEmpty(remoteInputText)) {
619 remoteInputText = entry.remoteInputTextWhenReset;
620 }
621 StatusBarNotification newSbn = rebuildNotificationWithRemoteInput(entry,
622 remoteInputText, false /* showSpinner */);
623 entry.onRemoteInputInserted();
624
625 if (newSbn == null) {
626 return;
627 }
628
629 mEntryManager.updateNotification(newSbn, null);
630
631 // Ensure the entry hasn't already been removed. This can happen if there is an
632 // inflation exception while updating the remote history
Evan Laird94492852018-10-25 13:43:01 -0400633 if (entry.isRemoved()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700634 return;
635 }
636
637 if (Log.isLoggable(TAG, Log.DEBUG)) {
638 Log.d(TAG, "Keeping notification around after sending remote input "
Ned Burns00b4b2d2019-10-17 22:09:27 -0400639 + entry.getKey());
Kevina5ff1fa2018-08-21 16:35:48 -0700640 }
641
Ned Burns00b4b2d2019-10-17 22:09:27 -0400642 mKeysKeptForRemoteInputHistory.add(entry.getKey());
Kevina5ff1fa2018-08-21 16:35:48 -0700643 } else {
Ned Burns00b4b2d2019-10-17 22:09:27 -0400644 mKeysKeptForRemoteInputHistory.remove(entry.getKey());
Kevina5ff1fa2018-08-21 16:35:48 -0700645 }
646 }
647 }
648
649 /**
650 * Notification is kept alive for smart reply history. Similar to REMOTE_INPUT_HISTORY but with
651 * {@link SmartReplyController} specific logic
652 */
653 protected class SmartReplyHistoryExtender extends RemoteInputExtender {
654 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500655 public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700656 return shouldKeepForSmartReplyHistory(entry);
657 }
658
659 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500660 public void setShouldManageLifetime(NotificationEntry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700661 boolean shouldExtend) {
662 if (shouldExtend) {
663 StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);
664
665 if (newSbn == null) {
666 return;
667 }
668
669 mEntryManager.updateNotification(newSbn, null);
670
Evan Laird94492852018-10-25 13:43:01 -0400671 if (entry.isRemoved()) {
Kevina5ff1fa2018-08-21 16:35:48 -0700672 return;
673 }
674
675 if (Log.isLoggable(TAG, Log.DEBUG)) {
676 Log.d(TAG, "Keeping notification around after sending smart reply "
Ned Burns00b4b2d2019-10-17 22:09:27 -0400677 + entry.getKey());
Kevina5ff1fa2018-08-21 16:35:48 -0700678 }
679
Ned Burns00b4b2d2019-10-17 22:09:27 -0400680 mKeysKeptForRemoteInputHistory.add(entry.getKey());
Kevina5ff1fa2018-08-21 16:35:48 -0700681 } else {
Ned Burns00b4b2d2019-10-17 22:09:27 -0400682 mKeysKeptForRemoteInputHistory.remove(entry.getKey());
Kevina5ff1fa2018-08-21 16:35:48 -0700683 mSmartReplyController.stopSending(entry);
684 }
685 }
686 }
687
688 /**
689 * Notification is kept alive because the user is still using the remote input
690 */
691 protected class RemoteInputActiveExtender extends RemoteInputExtender {
692 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500693 public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
Kevina5ff1fa2018-08-21 16:35:48 -0700694 return mRemoteInputController.isRemoteInputActive(entry);
695 }
696
697 @Override
Ned Burnsf81c4c42019-01-07 14:10:43 -0500698 public void setShouldManageLifetime(NotificationEntry entry,
Kevina5ff1fa2018-08-21 16:35:48 -0700699 boolean shouldExtend) {
700 if (shouldExtend) {
701 if (Log.isLoggable(TAG, Log.DEBUG)) {
702 Log.d(TAG, "Keeping notification around while remote input active "
Ned Burns00b4b2d2019-10-17 22:09:27 -0400703 + entry.getKey());
Kevina5ff1fa2018-08-21 16:35:48 -0700704 }
705 mEntriesKeptForRemoteInputActive.add(entry);
706 } else {
707 mEntriesKeptForRemoteInputActive.remove(entry);
708 }
709 }
Eliot Courtneye77edea2017-11-15 14:25:21 +0900710 }
711
712 /**
713 * Callback for various remote input related events, or for providing information that
714 * NotificationRemoteInputManager needs to know to decide what to do.
715 */
716 public interface Callback {
717
718 /**
719 * Called when remote input was activated but the device is locked.
720 *
721 * @param row
722 * @param clicked
723 */
724 void onLockedRemoteInput(ExpandableNotificationRow row, View clicked);
725
726 /**
727 * Called when remote input was activated but the device is locked and in a managed profile.
728 *
729 * @param userId
730 * @param row
731 * @param clicked
732 */
733 void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked);
734
735 /**
736 * Called when a row should be made expanded for the purposes of remote input.
737 *
738 * @param row
739 * @param clickedView
740 */
741 void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView);
742
743 /**
744 * Return whether or not remote input should be handled for this view.
745 *
746 * @param view
747 * @param pendingIntent
748 * @return true iff the remote input should be handled
749 */
750 boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent);
751
752 /**
753 * Performs any special handling for a remote view click. The default behaviour can be
754 * called through the defaultHandler parameter.
755 *
yoshiki iguchi67c166d2018-11-26 13:11:36 +0900756 * @param view
Eliot Courtneye77edea2017-11-15 14:25:21 +0900757 * @param pendingIntent
Eliot Courtneye77edea2017-11-15 14:25:21 +0900758 * @param defaultHandler
759 * @return true iff the click was handled
760 */
yoshiki iguchi67c166d2018-11-26 13:11:36 +0900761 boolean handleRemoteViewClick(View view, PendingIntent pendingIntent,
762 ClickHandler defaultHandler);
Eliot Courtneye77edea2017-11-15 14:25:21 +0900763 }
764
765 /**
766 * Helper interface meant for passing the default on click behaviour to NotificationPresenter,
767 * so it may do its own handling before invoking the default behaviour.
768 */
769 public interface ClickHandler {
770 /**
771 * Tries to handle a click on a remote view.
772 *
773 * @return true iff the click was handled
774 */
775 boolean handleClick();
776 }
777}