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