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.HeadsUpManager; 23 import com.android.systemui.statusbar.policy.RemoteInputView; 24 25 import android.util.ArrayMap; 26 import android.util.ArraySet; 27 import android.util.Pair; 28 29 import java.lang.ref.WeakReference; 30 import java.util.ArrayList; 31 32 /** 33 * Keeps track of the currently active {@link RemoteInputView}s. 34 */ 35 public 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 RemoteInputController(HeadsUpManager headsUpManager)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 */ addRemoteInput(NotificationData.Entry entry, Object token)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 */ removeRemoteInput(NotificationData.Entry entry, Object token)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 */ addSpinning(String key, Object token)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 */ removeSpinning(String key, Object token)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 isSpinning(String key)112 public boolean isSpinning(String key) { 113 return mSpinning.containsKey(key); 114 } 115 apply(NotificationData.Entry entry)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 */ isRemoteInputActive(NotificationData.Entry entry)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 */ isRemoteInputActive()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 */ pruneWeakThenRemoveAndContains( NotificationData.Entry contains, NotificationData.Entry remove, Object removeToken)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 addCallback(Callback callback)173 public void addCallback(Callback callback) { 174 Preconditions.checkNotNull(callback); 175 mCallbacks.add(callback); 176 } 177 remoteInputSent(NotificationData.Entry entry)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 closeRemoteInputs()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 { onRemoteInputActive(boolean active)208 default void onRemoteInputActive(boolean active) {} 209 onRemoteInputSent(NotificationData.Entry entry)210 default void onRemoteInputSent(NotificationData.Entry entry) {} 211 } 212 } 213