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