• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.systemui.statusbar;
18 
19 import android.app.Notification;
20 import android.app.RemoteInput;
21 import android.content.Context;
22 import android.net.Uri;
23 import android.os.SystemProperties;
24 import android.service.notification.StatusBarNotification;
25 import android.util.ArrayMap;
26 import android.util.Pair;
27 
28 import com.android.internal.util.Preconditions;
29 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
30 import com.android.systemui.statusbar.policy.RemoteInputUriController;
31 import com.android.systemui.statusbar.policy.RemoteInputView;
32 
33 import java.lang.ref.WeakReference;
34 import java.util.ArrayList;
35 import java.util.List;
36 import java.util.Objects;
37 
38 /**
39  * Keeps track of the currently active {@link RemoteInputView}s.
40  */
41 public class RemoteInputController {
42     private static final boolean ENABLE_REMOTE_INPUT =
43             SystemProperties.getBoolean("debug.enable_remote_input", true);
44 
45     private final ArrayList<Pair<WeakReference<NotificationEntry>, Object>> mOpen
46             = new ArrayList<>();
47     private final ArrayMap<String, Object> mSpinning = new ArrayMap<>();
48     private final ArrayList<Callback> mCallbacks = new ArrayList<>(3);
49     private final Delegate mDelegate;
50     private final RemoteInputUriController mRemoteInputUriController;
51 
RemoteInputController(Delegate delegate, RemoteInputUriController remoteInputUriController)52     public RemoteInputController(Delegate delegate,
53             RemoteInputUriController remoteInputUriController) {
54         mDelegate = delegate;
55         mRemoteInputUriController = remoteInputUriController;
56     }
57 
58     /**
59      * Adds RemoteInput actions from the WearableExtender; to be removed once more apps support this
60      * via first-class API.
61      *
62      * TODO: Remove once enough apps specify remote inputs on their own.
63      */
processForRemoteInput(Notification n, Context context)64     public static void processForRemoteInput(Notification n, Context context) {
65         if (!ENABLE_REMOTE_INPUT) {
66             return;
67         }
68 
69         if (n.extras != null && n.extras.containsKey("android.wearable.EXTENSIONS") &&
70                 (n.actions == null || n.actions.length == 0)) {
71             Notification.Action viableAction = null;
72             Notification.WearableExtender we = new Notification.WearableExtender(n);
73 
74             List<Notification.Action> actions = we.getActions();
75             final int numActions = actions.size();
76 
77             for (int i = 0; i < numActions; i++) {
78                 Notification.Action action = actions.get(i);
79                 if (action == null) {
80                     continue;
81                 }
82                 RemoteInput[] remoteInputs = action.getRemoteInputs();
83                 if (remoteInputs == null) {
84                     continue;
85                 }
86                 for (RemoteInput ri : remoteInputs) {
87                     if (ri.getAllowFreeFormInput()) {
88                         viableAction = action;
89                         break;
90                     }
91                 }
92                 if (viableAction != null) {
93                     break;
94                 }
95             }
96 
97             if (viableAction != null) {
98                 Notification.Builder rebuilder = Notification.Builder.recoverBuilder(context, n);
99                 rebuilder.setActions(viableAction);
100                 rebuilder.build(); // will rewrite n
101             }
102         }
103     }
104 
105     /**
106      * Adds a currently active remote input.
107      *
108      * @param entry the entry for which a remote input is now active.
109      * @param token a token identifying the view that is managing the remote input
110      */
addRemoteInput(NotificationEntry entry, Object token)111     public void addRemoteInput(NotificationEntry entry, Object token) {
112         Objects.requireNonNull(entry);
113         Objects.requireNonNull(token);
114 
115         boolean found = pruneWeakThenRemoveAndContains(
116                 entry /* contains */, null /* remove */, token /* removeToken */);
117         if (!found) {
118             mOpen.add(new Pair<>(new WeakReference<>(entry), token));
119         }
120 
121         apply(entry);
122     }
123 
124     /**
125      * Removes a currently active remote input.
126      *
127      * @param entry the entry for which a remote input should be removed.
128      * @param token a token identifying the view that is requesting the removal. If non-null,
129      *              the entry is only removed if the token matches the last added token for this
130      *              entry. If null, the entry is removed regardless.
131      */
removeRemoteInput(NotificationEntry entry, Object token)132     public void removeRemoteInput(NotificationEntry entry, Object token) {
133         Objects.requireNonNull(entry);
134         if (entry.mRemoteEditImeVisible && entry.mRemoteEditImeAnimatingAway) return;
135         // If the view is being removed, this may be called even though we're not active
136         if (!isRemoteInputActive(entry)) return;
137 
138         pruneWeakThenRemoveAndContains(null /* contains */, entry /* remove */, token);
139 
140         apply(entry);
141     }
142 
143     /**
144      * Adds a currently spinning (i.e. sending) remote input.
145      *
146      * @param key the key of the entry that's spinning.
147      * @param token the token of the view managing the remote input.
148      */
addSpinning(String key, Object token)149     public void addSpinning(String key, Object token) {
150         Objects.requireNonNull(key);
151         Objects.requireNonNull(token);
152 
153         mSpinning.put(key, token);
154     }
155 
156     /**
157      * Removes a currently spinning remote input.
158      *
159      * @param key the key of the entry for which a remote input should be removed.
160      * @param token a token identifying the view that is requesting the removal. If non-null,
161      *              the entry is only removed if the token matches the last added token for this
162      *              entry. If null, the entry is removed regardless.
163      */
removeSpinning(String key, Object token)164     public void removeSpinning(String key, Object token) {
165         Objects.requireNonNull(key);
166 
167         if (token == null || mSpinning.get(key) == token) {
168             mSpinning.remove(key);
169         }
170     }
171 
isSpinning(String key)172     public boolean isSpinning(String key) {
173         return mSpinning.containsKey(key);
174     }
175 
176     /**
177      * Same as {@link #isSpinning}, but also verifies that the token is the same
178      * @param key the key that is spinning
179      * @param token the token that needs to be the same
180      * @return if this key with a given token is spinning
181      */
isSpinning(String key, Object token)182     public boolean isSpinning(String key, Object token) {
183         return mSpinning.get(key) == token;
184     }
185 
apply(NotificationEntry entry)186     private void apply(NotificationEntry entry) {
187         mDelegate.setRemoteInputActive(entry, isRemoteInputActive(entry));
188         boolean remoteInputActive = isRemoteInputActive();
189         int N = mCallbacks.size();
190         for (int i = 0; i < N; i++) {
191             mCallbacks.get(i).onRemoteInputActive(remoteInputActive);
192         }
193     }
194 
195     /**
196      * @return true if {@param entry} has an active RemoteInput
197      */
isRemoteInputActive(NotificationEntry entry)198     public boolean isRemoteInputActive(NotificationEntry entry) {
199         return pruneWeakThenRemoveAndContains(entry /* contains */, null /* remove */,
200                 null /* removeToken */);
201     }
202 
203     /**
204      * @return true if any entry has an active RemoteInput
205      */
isRemoteInputActive()206     public boolean isRemoteInputActive() {
207         pruneWeakThenRemoveAndContains(null /* contains */, null /* remove */,
208                 null /* removeToken */);
209         return !mOpen.isEmpty();
210     }
211 
212     /**
213      * Prunes dangling weak references, removes entries referring to {@param remove} and returns
214      * whether {@param contains} is part of the array in a single loop.
215      * @param remove if non-null, removes this entry from the active remote inputs
216      * @param removeToken if non-null, only removes an entry if this matches the token when the
217      *                    entry was added.
218      * @return true if {@param contains} is in the set of active remote inputs
219      */
pruneWeakThenRemoveAndContains( NotificationEntry contains, NotificationEntry remove, Object removeToken)220     private boolean pruneWeakThenRemoveAndContains(
221             NotificationEntry contains, NotificationEntry remove, Object removeToken) {
222         boolean found = false;
223         for (int i = mOpen.size() - 1; i >= 0; i--) {
224             NotificationEntry item = mOpen.get(i).first.get();
225             Object itemToken = mOpen.get(i).second;
226             boolean removeTokenMatches = (removeToken == null || itemToken == removeToken);
227 
228             if (item == null || (item == remove && removeTokenMatches)) {
229                 mOpen.remove(i);
230             } else if (item == contains) {
231                 if (removeToken != null && removeToken != itemToken) {
232                     // We need to update the token. Remove here and let caller reinsert it.
233                     mOpen.remove(i);
234                 } else {
235                     found = true;
236                 }
237             }
238         }
239         return found;
240     }
241 
242 
addCallback(Callback callback)243     public void addCallback(Callback callback) {
244         Objects.requireNonNull(callback);
245         mCallbacks.add(callback);
246     }
247 
remoteInputSent(NotificationEntry entry)248     public void remoteInputSent(NotificationEntry entry) {
249         int N = mCallbacks.size();
250         for (int i = 0; i < N; i++) {
251             mCallbacks.get(i).onRemoteInputSent(entry);
252         }
253     }
254 
closeRemoteInputs()255     public void closeRemoteInputs() {
256         if (mOpen.size() == 0) {
257             return;
258         }
259 
260         // Make a copy because closing the remote inputs will modify mOpen.
261         ArrayList<NotificationEntry> list = new ArrayList<>(mOpen.size());
262         for (int i = mOpen.size() - 1; i >= 0; i--) {
263             NotificationEntry entry = mOpen.get(i).first.get();
264             if (entry != null && entry.rowExists()) {
265                 list.add(entry);
266             }
267         }
268 
269         for (int i = list.size() - 1; i >= 0; i--) {
270             NotificationEntry entry = list.get(i);
271             if (entry.rowExists()) {
272                 entry.closeRemoteInput();
273             }
274         }
275     }
276 
requestDisallowLongPressAndDismiss()277     public void requestDisallowLongPressAndDismiss() {
278         mDelegate.requestDisallowLongPressAndDismiss();
279     }
280 
lockScrollTo(NotificationEntry entry)281     public void lockScrollTo(NotificationEntry entry) {
282         mDelegate.lockScrollTo(entry);
283     }
284 
285     /**
286      * Create a temporary grant which allows the app that submitted the notification access to the
287      * specified URI.
288      */
grantInlineReplyUriPermission(StatusBarNotification sbn, Uri data)289     public void grantInlineReplyUriPermission(StatusBarNotification sbn, Uri data) {
290         mRemoteInputUriController.grantInlineReplyUriPermission(sbn, data);
291     }
292 
293     public interface Callback {
onRemoteInputActive(boolean active)294         default void onRemoteInputActive(boolean active) {}
295 
onRemoteInputSent(NotificationEntry entry)296         default void onRemoteInputSent(NotificationEntry entry) {}
297     }
298 
299     public interface Delegate {
300         /**
301          * Activate remote input if necessary.
302          */
setRemoteInputActive(NotificationEntry entry, boolean remoteInputActive)303         void setRemoteInputActive(NotificationEntry entry, boolean remoteInputActive);
304 
305         /**
306          * Request that the view does not dismiss nor perform long press for the current touch.
307          */
requestDisallowLongPressAndDismiss()308         void requestDisallowLongPressAndDismiss();
309 
310         /**
311          * Request that the view is made visible by scrolling to it, and keep the scroll locked until
312          * the user scrolls, or {@param entry} loses focus or is detached.
313          */
lockScrollTo(NotificationEntry entry)314         void lockScrollTo(NotificationEntry entry);
315     }
316 }
317