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
17package com.android.systemui.statusbar;
18
19import com.android.internal.util.Preconditions;
20import com.android.systemui.Dependency;
21import com.android.systemui.statusbar.phone.StatusBarWindowManager;
22import com.android.systemui.statusbar.policy.HeadsUpManager;
23import com.android.systemui.statusbar.policy.RemoteInputView;
24
25import android.util.ArrayMap;
26import android.util.ArraySet;
27import android.util.Pair;
28
29import java.lang.ref.WeakReference;
30import java.util.ArrayList;
31
32/**
33 * Keeps track of the currently active {@link RemoteInputView}s.
34 */
35public class RemoteInputController {
36
37    private final ArrayList<Pair<WeakReference<NotificationData.Entry>, Object>> mOpen
38            = new ArrayList<>();
39    private final ArrayMap<String, Object> mSpinning = new ArrayMap<>();
40    private final ArrayList<Callback> mCallbacks = new ArrayList<>(3);
41    private final HeadsUpManager mHeadsUpManager;
42
43    public RemoteInputController(HeadsUpManager headsUpManager) {
44        addCallback(Dependency.get(StatusBarWindowManager.class));
45        mHeadsUpManager = headsUpManager;
46    }
47
48    /**
49     * Adds a currently active remote input.
50     *
51     * @param entry the entry for which a remote input is now active.
52     * @param token a token identifying the view that is managing the remote input
53     */
54    public void addRemoteInput(NotificationData.Entry entry, Object token) {
55        Preconditions.checkNotNull(entry);
56        Preconditions.checkNotNull(token);
57
58        boolean found = pruneWeakThenRemoveAndContains(
59                entry /* contains */, null /* remove */, token /* removeToken */);
60        if (!found) {
61            mOpen.add(new Pair<>(new WeakReference<>(entry), token));
62        }
63
64        apply(entry);
65    }
66
67    /**
68     * Removes a currently active remote input.
69     *
70     * @param entry the entry for which a remote input should be removed.
71     * @param token a token identifying the view that is requesting the removal. If non-null,
72     *              the entry is only removed if the token matches the last added token for this
73     *              entry. If null, the entry is removed regardless.
74     */
75    public void removeRemoteInput(NotificationData.Entry entry, Object token) {
76        Preconditions.checkNotNull(entry);
77
78        pruneWeakThenRemoveAndContains(null /* contains */, entry /* remove */, token);
79
80        apply(entry);
81    }
82
83    /**
84     * Adds a currently spinning (i.e. sending) remote input.
85     *
86     * @param key the key of the entry that's spinning.
87     * @param token the token of the view managing the remote input.
88     */
89    public void addSpinning(String key, Object token) {
90        Preconditions.checkNotNull(key);
91        Preconditions.checkNotNull(token);
92
93        mSpinning.put(key, token);
94    }
95
96    /**
97     * Removes a currently spinning remote input.
98     *
99     * @param key the key of the entry for which a remote input should be removed.
100     * @param token a token identifying the view that is requesting the removal. If non-null,
101     *              the entry is only removed if the token matches the last added token for this
102     *              entry. If null, the entry is removed regardless.
103     */
104    public void removeSpinning(String key, Object token) {
105        Preconditions.checkNotNull(key);
106
107        if (token == null || mSpinning.get(key) == token) {
108            mSpinning.remove(key);
109        }
110    }
111
112    public boolean isSpinning(String key) {
113        return mSpinning.containsKey(key);
114    }
115
116    private void apply(NotificationData.Entry entry) {
117        mHeadsUpManager.setRemoteInputActive(entry, isRemoteInputActive(entry));
118        boolean remoteInputActive = isRemoteInputActive();
119        int N = mCallbacks.size();
120        for (int i = 0; i < N; i++) {
121            mCallbacks.get(i).onRemoteInputActive(remoteInputActive);
122        }
123    }
124
125    /**
126     * @return true if {@param entry} has an active RemoteInput
127     */
128    public boolean isRemoteInputActive(NotificationData.Entry entry) {
129        return pruneWeakThenRemoveAndContains(entry /* contains */, null /* remove */,
130                null /* removeToken */);
131    }
132
133    /**
134     * @return true if any entry has an active RemoteInput
135     */
136    public boolean isRemoteInputActive() {
137        pruneWeakThenRemoveAndContains(null /* contains */, null /* remove */,
138                null /* removeToken */);
139        return !mOpen.isEmpty();
140    }
141
142    /**
143     * Prunes dangling weak references, removes entries referring to {@param remove} and returns
144     * whether {@param contains} is part of the array in a single loop.
145     * @param remove if non-null, removes this entry from the active remote inputs
146     * @param removeToken if non-null, only removes an entry if this matches the token when the
147     *                    entry was added.
148     * @return true if {@param contains} is in the set of active remote inputs
149     */
150    private boolean pruneWeakThenRemoveAndContains(
151            NotificationData.Entry contains, NotificationData.Entry remove, Object removeToken) {
152        boolean found = false;
153        for (int i = mOpen.size() - 1; i >= 0; i--) {
154            NotificationData.Entry item = mOpen.get(i).first.get();
155            Object itemToken = mOpen.get(i).second;
156            boolean removeTokenMatches = (removeToken == null || itemToken == removeToken);
157
158            if (item == null || (item == remove && removeTokenMatches)) {
159                mOpen.remove(i);
160            } else if (item == contains) {
161                if (removeToken != null && removeToken != itemToken) {
162                    // We need to update the token. Remove here and let caller reinsert it.
163                    mOpen.remove(i);
164                } else {
165                    found = true;
166                }
167            }
168        }
169        return found;
170    }
171
172
173    public void addCallback(Callback callback) {
174        Preconditions.checkNotNull(callback);
175        mCallbacks.add(callback);
176    }
177
178    public void remoteInputSent(NotificationData.Entry entry) {
179        int N = mCallbacks.size();
180        for (int i = 0; i < N; i++) {
181            mCallbacks.get(i).onRemoteInputSent(entry);
182        }
183    }
184
185    public void closeRemoteInputs() {
186        if (mOpen.size() == 0) {
187            return;
188        }
189
190        // Make a copy because closing the remote inputs will modify mOpen.
191        ArrayList<NotificationData.Entry> list = new ArrayList<>(mOpen.size());
192        for (int i = mOpen.size() - 1; i >= 0; i--) {
193            NotificationData.Entry item = mOpen.get(i).first.get();
194            if (item != null && item.row != null) {
195                list.add(item);
196            }
197        }
198
199        for (int i = list.size() - 1; i >= 0; i--) {
200            NotificationData.Entry item = list.get(i);
201            if (item.row != null) {
202                item.row.closeRemoteInput();
203            }
204        }
205    }
206
207    public interface Callback {
208        default void onRemoteInputActive(boolean active) {}
209
210        default void onRemoteInputSent(NotificationData.Entry entry) {}
211    }
212}
213