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