blob: 998cf523d3df1e250b53fa19ff8131fac13c60cd [file] [log] [blame]
Adrian Roos1c0ca502015-10-07 12:20:42 -07001/*
2 * Copyright (C) 2015 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 */
16
17package com.android.systemui.statusbar;
18
Eliot Courtney21bc05f2017-10-19 17:03:34 +090019import android.app.Notification;
20import android.app.RemoteInput;
21import android.content.Context;
22import android.os.SystemProperties;
Adrian Roos7813dd72016-09-23 17:12:17 -070023import android.util.ArrayMap;
Adrian Roos7813dd72016-09-23 17:12:17 -070024import android.util.Pair;
Adrian Roosc0a579e2016-03-30 16:43:58 -070025
Gus Prevasab336792018-11-14 13:52:20 -050026import com.android.internal.util.Preconditions;
Ned Burnsf81c4c42019-01-07 14:10:43 -050027import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Gus Prevasab336792018-11-14 13:52:20 -050028import com.android.systemui.statusbar.policy.RemoteInputView;
29
Adrian Roos1c0ca502015-10-07 12:20:42 -070030import java.lang.ref.WeakReference;
31import java.util.ArrayList;
Eliot Courtney21bc05f2017-10-19 17:03:34 +090032import java.util.List;
Adrian Roos1c0ca502015-10-07 12:20:42 -070033
34/**
35 * Keeps track of the currently active {@link RemoteInputView}s.
36 */
37public class RemoteInputController {
Eliot Courtney21bc05f2017-10-19 17:03:34 +090038 private static final boolean ENABLE_REMOTE_INPUT =
39 SystemProperties.getBoolean("debug.enable_remote_input", true);
Adrian Roos1c0ca502015-10-07 12:20:42 -070040
Ned Burnsf81c4c42019-01-07 14:10:43 -050041 private final ArrayList<Pair<WeakReference<NotificationEntry>, Object>> mOpen
Adrian Roos7813dd72016-09-23 17:12:17 -070042 = new ArrayList<>();
43 private final ArrayMap<String, Object> mSpinning = new ArrayMap<>();
Adrian Roosd28ccd72016-01-06 15:23:14 +010044 private final ArrayList<Callback> mCallbacks = new ArrayList<>(3);
yoshiki iguchiff4696c2017-11-22 16:47:41 +090045 private final Delegate mDelegate;
Adrian Roos1c0ca502015-10-07 12:20:42 -070046
yoshiki iguchiff4696c2017-11-22 16:47:41 +090047 public RemoteInputController(Delegate delegate) {
yoshiki iguchiff4696c2017-11-22 16:47:41 +090048 mDelegate = delegate;
Adrian Roos1c0ca502015-10-07 12:20:42 -070049 }
50
Adrian Roos7813dd72016-09-23 17:12:17 -070051 /**
Eliot Courtney21bc05f2017-10-19 17:03:34 +090052 * Adds RemoteInput actions from the WearableExtender; to be removed once more apps support this
53 * via first-class API.
54 *
55 * TODO: Remove once enough apps specify remote inputs on their own.
56 */
57 public static void processForRemoteInput(Notification n, Context context) {
58 if (!ENABLE_REMOTE_INPUT) {
59 return;
60 }
61
62 if (n.extras != null && n.extras.containsKey("android.wearable.EXTENSIONS") &&
63 (n.actions == null || n.actions.length == 0)) {
64 Notification.Action viableAction = null;
65 Notification.WearableExtender we = new Notification.WearableExtender(n);
66
67 List<Notification.Action> actions = we.getActions();
68 final int numActions = actions.size();
69
70 for (int i = 0; i < numActions; i++) {
71 Notification.Action action = actions.get(i);
72 if (action == null) {
73 continue;
74 }
75 RemoteInput[] remoteInputs = action.getRemoteInputs();
76 if (remoteInputs == null) {
77 continue;
78 }
79 for (RemoteInput ri : remoteInputs) {
80 if (ri.getAllowFreeFormInput()) {
81 viableAction = action;
82 break;
83 }
84 }
85 if (viableAction != null) {
86 break;
87 }
88 }
89
90 if (viableAction != null) {
91 Notification.Builder rebuilder = Notification.Builder.recoverBuilder(context, n);
92 rebuilder.setActions(viableAction);
93 rebuilder.build(); // will rewrite n
94 }
95 }
96 }
97
98 /**
Adrian Roos7813dd72016-09-23 17:12:17 -070099 * Adds a currently active remote input.
100 *
101 * @param entry the entry for which a remote input is now active.
102 * @param token a token identifying the view that is managing the remote input
103 */
Ned Burnsf81c4c42019-01-07 14:10:43 -0500104 public void addRemoteInput(NotificationEntry entry, Object token) {
Adrian Roos1c0ca502015-10-07 12:20:42 -0700105 Preconditions.checkNotNull(entry);
Adrian Roos7813dd72016-09-23 17:12:17 -0700106 Preconditions.checkNotNull(token);
Adrian Roos1c0ca502015-10-07 12:20:42 -0700107
108 boolean found = pruneWeakThenRemoveAndContains(
Adrian Roos7813dd72016-09-23 17:12:17 -0700109 entry /* contains */, null /* remove */, token /* removeToken */);
Adrian Roos1c0ca502015-10-07 12:20:42 -0700110 if (!found) {
Adrian Roos7813dd72016-09-23 17:12:17 -0700111 mOpen.add(new Pair<>(new WeakReference<>(entry), token));
Adrian Roos1c0ca502015-10-07 12:20:42 -0700112 }
113
114 apply(entry);
115 }
116
Adrian Roos7813dd72016-09-23 17:12:17 -0700117 /**
118 * Removes a currently active remote input.
119 *
120 * @param entry the entry for which a remote input should be removed.
121 * @param token a token identifying the view that is requesting the removal. If non-null,
122 * the entry is only removed if the token matches the last added token for this
123 * entry. If null, the entry is removed regardless.
124 */
Ned Burnsf81c4c42019-01-07 14:10:43 -0500125 public void removeRemoteInput(NotificationEntry entry, Object token) {
Adrian Roos1c0ca502015-10-07 12:20:42 -0700126 Preconditions.checkNotNull(entry);
127
Adrian Roos7813dd72016-09-23 17:12:17 -0700128 pruneWeakThenRemoveAndContains(null /* contains */, entry /* remove */, token);
Adrian Roos1c0ca502015-10-07 12:20:42 -0700129
130 apply(entry);
131 }
132
Adrian Roos7813dd72016-09-23 17:12:17 -0700133 /**
134 * Adds a currently spinning (i.e. sending) remote input.
135 *
136 * @param key the key of the entry that's spinning.
137 * @param token the token of the view managing the remote input.
138 */
139 public void addSpinning(String key, Object token) {
140 Preconditions.checkNotNull(key);
141 Preconditions.checkNotNull(token);
142
143 mSpinning.put(key, token);
Adrian Roosc0a579e2016-03-30 16:43:58 -0700144 }
145
Adrian Roos7813dd72016-09-23 17:12:17 -0700146 /**
147 * Removes a currently spinning remote input.
148 *
149 * @param key the key of the entry for which a remote input should be removed.
150 * @param token a token identifying the view that is requesting the removal. If non-null,
151 * the entry is only removed if the token matches the last added token for this
152 * entry. If null, the entry is removed regardless.
153 */
154 public void removeSpinning(String key, Object token) {
155 Preconditions.checkNotNull(key);
156
157 if (token == null || mSpinning.get(key) == token) {
158 mSpinning.remove(key);
159 }
Adrian Roosc0a579e2016-03-30 16:43:58 -0700160 }
161
162 public boolean isSpinning(String key) {
Adrian Roos7813dd72016-09-23 17:12:17 -0700163 return mSpinning.containsKey(key);
Adrian Roosc0a579e2016-03-30 16:43:58 -0700164 }
165
Selim Cinek09b442e2018-05-04 15:46:18 -0700166 /**
167 * Same as {@link #isSpinning}, but also verifies that the token is the same
168 * @param key the key that is spinning
169 * @param token the token that needs to be the same
170 * @return if this key with a given token is spinning
171 */
172 public boolean isSpinning(String key, Object token) {
173 return mSpinning.get(key) == token;
174 }
175
Ned Burnsf81c4c42019-01-07 14:10:43 -0500176 private void apply(NotificationEntry entry) {
yoshiki iguchiff4696c2017-11-22 16:47:41 +0900177 mDelegate.setRemoteInputActive(entry, isRemoteInputActive(entry));
Adrian Roosd28ccd72016-01-06 15:23:14 +0100178 boolean remoteInputActive = isRemoteInputActive();
179 int N = mCallbacks.size();
180 for (int i = 0; i < N; i++) {
181 mCallbacks.get(i).onRemoteInputActive(remoteInputActive);
182 }
Adrian Roos1c0ca502015-10-07 12:20:42 -0700183 }
184
185 /**
186 * @return true if {@param entry} has an active RemoteInput
187 */
Ned Burnsf81c4c42019-01-07 14:10:43 -0500188 public boolean isRemoteInputActive(NotificationEntry entry) {
Adrian Roos7813dd72016-09-23 17:12:17 -0700189 return pruneWeakThenRemoveAndContains(entry /* contains */, null /* remove */,
190 null /* removeToken */);
Adrian Roos1c0ca502015-10-07 12:20:42 -0700191 }
192
193 /**
194 * @return true if any entry has an active RemoteInput
195 */
196 public boolean isRemoteInputActive() {
Adrian Roos7813dd72016-09-23 17:12:17 -0700197 pruneWeakThenRemoveAndContains(null /* contains */, null /* remove */,
198 null /* removeToken */);
Adrian Roosc0a579e2016-03-30 16:43:58 -0700199 return !mOpen.isEmpty();
Adrian Roos1c0ca502015-10-07 12:20:42 -0700200 }
201
202 /**
203 * Prunes dangling weak references, removes entries referring to {@param remove} and returns
204 * whether {@param contains} is part of the array in a single loop.
205 * @param remove if non-null, removes this entry from the active remote inputs
Adrian Roos7813dd72016-09-23 17:12:17 -0700206 * @param removeToken if non-null, only removes an entry if this matches the token when the
207 * entry was added.
Adrian Roos1c0ca502015-10-07 12:20:42 -0700208 * @return true if {@param contains} is in the set of active remote inputs
209 */
210 private boolean pruneWeakThenRemoveAndContains(
Ned Burnsf81c4c42019-01-07 14:10:43 -0500211 NotificationEntry contains, NotificationEntry remove, Object removeToken) {
Adrian Roos1c0ca502015-10-07 12:20:42 -0700212 boolean found = false;
Adrian Roosc0a579e2016-03-30 16:43:58 -0700213 for (int i = mOpen.size() - 1; i >= 0; i--) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500214 NotificationEntry item = mOpen.get(i).first.get();
Adrian Roos7813dd72016-09-23 17:12:17 -0700215 Object itemToken = mOpen.get(i).second;
216 boolean removeTokenMatches = (removeToken == null || itemToken == removeToken);
217
218 if (item == null || (item == remove && removeTokenMatches)) {
Adrian Roosc0a579e2016-03-30 16:43:58 -0700219 mOpen.remove(i);
Adrian Roos1c0ca502015-10-07 12:20:42 -0700220 } else if (item == contains) {
Adrian Roos7813dd72016-09-23 17:12:17 -0700221 if (removeToken != null && removeToken != itemToken) {
222 // We need to update the token. Remove here and let caller reinsert it.
223 mOpen.remove(i);
224 } else {
225 found = true;
226 }
Adrian Roos1c0ca502015-10-07 12:20:42 -0700227 }
228 }
229 return found;
230 }
231
232
Adrian Roosd28ccd72016-01-06 15:23:14 +0100233 public void addCallback(Callback callback) {
234 Preconditions.checkNotNull(callback);
235 mCallbacks.add(callback);
236 }
237
Ned Burnsf81c4c42019-01-07 14:10:43 -0500238 public void remoteInputSent(NotificationEntry entry) {
Adrian Roosc0a579e2016-03-30 16:43:58 -0700239 int N = mCallbacks.size();
240 for (int i = 0; i < N; i++) {
241 mCallbacks.get(i).onRemoteInputSent(entry);
242 }
243 }
244
Adrian Roos07215352016-06-23 11:19:28 -0700245 public void closeRemoteInputs() {
246 if (mOpen.size() == 0) {
247 return;
248 }
249
250 // Make a copy because closing the remote inputs will modify mOpen.
Ned Burnsf81c4c42019-01-07 14:10:43 -0500251 ArrayList<NotificationEntry> list = new ArrayList<>(mOpen.size());
Adrian Roos07215352016-06-23 11:19:28 -0700252 for (int i = mOpen.size() - 1; i >= 0; i--) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500253 NotificationEntry entry = mOpen.get(i).first.get();
Evan Laird94492852018-10-25 13:43:01 -0400254 if (entry != null && entry.rowExists()) {
255 list.add(entry);
Adrian Roos07215352016-06-23 11:19:28 -0700256 }
257 }
258
259 for (int i = list.size() - 1; i >= 0; i--) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500260 NotificationEntry entry = list.get(i);
Evan Laird94492852018-10-25 13:43:01 -0400261 if (entry.rowExists()) {
262 entry.closeRemoteInput();
Adrian Roos07215352016-06-23 11:19:28 -0700263 }
264 }
265 }
266
yoshiki iguchiff4696c2017-11-22 16:47:41 +0900267 public void requestDisallowLongPressAndDismiss() {
268 mDelegate.requestDisallowLongPressAndDismiss();
269 }
270
Ned Burnsf81c4c42019-01-07 14:10:43 -0500271 public void lockScrollTo(NotificationEntry entry) {
yoshiki iguchiff4696c2017-11-22 16:47:41 +0900272 mDelegate.lockScrollTo(entry);
273 }
274
Adrian Roosd28ccd72016-01-06 15:23:14 +0100275 public interface Callback {
Adrian Roosc0a579e2016-03-30 16:43:58 -0700276 default void onRemoteInputActive(boolean active) {}
277
Ned Burnsf81c4c42019-01-07 14:10:43 -0500278 default void onRemoteInputSent(NotificationEntry entry) {}
Adrian Roosd28ccd72016-01-06 15:23:14 +0100279 }
yoshiki iguchiff4696c2017-11-22 16:47:41 +0900280
281 public interface Delegate {
282 /**
283 * Activate remote input if necessary.
284 */
Ned Burnsf81c4c42019-01-07 14:10:43 -0500285 void setRemoteInputActive(NotificationEntry entry, boolean remoteInputActive);
yoshiki iguchiff4696c2017-11-22 16:47:41 +0900286
Ned Burnsf81c4c42019-01-07 14:10:43 -0500287 /**
288 * Request that the view does not dismiss nor perform long press for the current touch.
289 */
290 void requestDisallowLongPressAndDismiss();
yoshiki iguchiff4696c2017-11-22 16:47:41 +0900291
Ned Burnsf81c4c42019-01-07 14:10:43 -0500292 /**
293 * Request that the view is made visible by scrolling to it, and keep the scroll locked until
294 * the user scrolls, or {@param entry} loses focus or is detached.
295 */
296 void lockScrollTo(NotificationEntry entry);
yoshiki iguchiff4696c2017-11-22 16:47:41 +0900297 }
Adrian Roos1c0ca502015-10-07 12:20:42 -0700298}