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