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