diff options
7 files changed, 1503 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/utils/Watchable.java b/services/core/java/com/android/server/utils/Watchable.java new file mode 100644 index 000000000000..7c99274f3df2 --- /dev/null +++ b/services/core/java/com/android/server/utils/Watchable.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2020 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.server.utils; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +/** + * Notify registered {@link Watcher}s when the content changes. + */ +public interface Watchable { + + /** + * Ensures an observer is in the list, exactly once. The observer cannot be null. The + * function quietly returns if the observer is already in the list. + * + * @param observer The {@link Watcher} to be notified when the {@link Watchable} changes. + */ + public void registerObserver(@NonNull Watcher observer); + + /** + * Ensures an observer is not in the list. The observer must not be null. The function + * quietly returns if the objserver is not in the list. + * + * @param observer The {@link Watcher} that should not be in the notification list. + */ + public void unregisterObserver(@NonNull Watcher observer); + + /** + * Invokes {@link Watcher#onChange} on each registered observer. The method can be called + * with the {@link Watchable} that generated the event. In a tree of {@link Watchable}s, this + * is generally the first (deepest) {@link Watchable} to detect a change. + * + * @param what The {@link Watchable} that generated the event. + */ + public void dispatchChange(@Nullable Watchable what); +} diff --git a/services/core/java/com/android/server/utils/WatchableImpl.java b/services/core/java/com/android/server/utils/WatchableImpl.java new file mode 100644 index 000000000000..94ab1d49807f --- /dev/null +++ b/services/core/java/com/android/server/utils/WatchableImpl.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2020 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.server.utils; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Objects; + +/** + * A concrete implementation of {@link Watchable} + */ +public class WatchableImpl implements Watchable { + /** + * The list of observers. + */ + protected final ArrayList<Watcher> mObservers = new ArrayList<>(); + + /** + * Ensure the observer is the list. The observer cannot be null but it is okay if it + * is already in the list. + * + * @param observer The {@link} Watcher to be added to the notification list. + */ + @Override + public void registerObserver(@NonNull Watcher observer) { + Objects.requireNonNull(observer, "observer may not be null"); + synchronized (mObservers) { + if (!mObservers.contains(observer)) { + mObservers.add(observer); + } + } + } + + /** + * Removes a previously registered observer. The observer must not be null and it + * must already have been registered. + * + * @param observer The {@link} Watcher to be removed from the notification list. + */ + @Override + public void unregisterObserver(@NonNull Watcher observer) { + Objects.requireNonNull(observer, "observer may not be null"); + synchronized (mObservers) { + mObservers.remove(observer); + } + } + + /** + * Return the number of registered observers. + * + * @return The number of registered observers. + */ + public int registeredObserverCount() { + return mObservers.size(); + } + + /** + * Invokes {@link Watcher#onChange} on each observer. + * + * @param what The {@link Watchable} that generated the event + */ + @Override + public void dispatchChange(@Nullable Watchable what) { + synchronized (mObservers) { + final int end = mObservers.size(); + for (int i = 0; i < end; i++) { + mObservers.get(i).onChange(what); + } + } + } +} diff --git a/services/core/java/com/android/server/utils/WatchedArrayMap.java b/services/core/java/com/android/server/utils/WatchedArrayMap.java new file mode 100644 index 000000000000..7b3298086aba --- /dev/null +++ b/services/core/java/com/android/server/utils/WatchedArrayMap.java @@ -0,0 +1,389 @@ +/* + * Copyright (C) 2020 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.server.utils; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import android.util.ArrayMap; +import android.util.Log; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * WatchedArrayMap is an {@link android.util.ArrayMap} that can report changes to itself. If its + * values are {@link Watchable} then the WatchedArrayMap will also report changes to the values. + * A {@link Watchable} is notified only once, no matter how many times it is stored in the array. + */ +public class WatchedArrayMap<K, V> extends WatchableImpl implements Map<K, V> { + + // The storage + private final ArrayMap<K, V> mStorage; + + // If true, the array is watching its children + private boolean mWatching = false; + + // The local observer + private final Watcher mObserver = new Watcher() { + @Override + public void onChange(@Nullable Watchable what) { + WatchedArrayMap.this.dispatchChange(what); + } + }; + + /** + * A convenience function called when the elements are added to or removed from the storage. + * The watchable is always {@link this}. + */ + private void onChanged() { + dispatchChange(this); + } + + /** + * A convenience function. Register the object if it is {@link Watchable} and if the + * array is currently watching. Note that the watching flag must be true if this + * function is to succeed. Also note that if this is called with the same object + * twice, <this> is only registered once. + */ + private void registerChild(Object o) { + if (mWatching && o instanceof Watchable) { + ((Watchable) o).registerObserver(mObserver); + } + } + + /** + * A convenience function. Unregister the object if it is {@link Watchable} and if the + * array is currently watching. This unconditionally removes the object from the + * registered list. + */ + private void unregisterChild(Object o) { + if (mWatching && o instanceof Watchable) { + ((Watchable) o).unregisterObserver(mObserver); + } + } + + /** + * A convenience function. Unregister the object if it is {@link Watchable}, if the + * array is currently watching, and if there are no other instances of this object in + * the storage. Note that the watching flag must be true if this function is to + * succeed. The object must already have been removed from the storage before this + * method is called. + */ + private void unregisterChildIf(Object o) { + if (mWatching && o instanceof Watchable) { + if (!mStorage.containsValue(o)) { + ((Watchable) o).unregisterObserver(mObserver); + } + } + } + + /** + * Register a {@link Watcher} with the array. If this is the first Watcher than any + * array values that are {@link Watchable} are registered to the array itself. + */ + @Override + public void registerObserver(@NonNull Watcher observer) { + super.registerObserver(observer); + if (registeredObserverCount() == 1) { + // The watching flag must be set true before any children are registered. + mWatching = true; + final int end = mStorage.size(); + for (int i = 0; i < end; i++) { + registerChild(mStorage.valueAt(i)); + } + } + } + + /** + * Unregister a {@link Watcher} from the array. If this is the last Watcher than any + * array values that are {@link Watchable} are unregistered to the array itself. + */ + @Override + public void unregisterObserver(@NonNull Watcher observer) { + super.unregisterObserver(observer); + if (registeredObserverCount() == 0) { + final int end = mStorage.size(); + for (int i = 0; i < end; i++) { + unregisterChild(mStorage.valueAt(i)); + } + // The watching flag must be true while children are unregistered. + mWatching = false; + } + } + + /** + * Create a new empty {@link WatchedArrayMap}. The default capacity of an array map + * is 0, and will grow once items are added to it. + */ + public WatchedArrayMap() { + this(0, false); + } + + /** + * Create a new {@link WatchedArrayMap} with a given initial capacity. + */ + public WatchedArrayMap(int capacity) { + this(capacity, false); + } + + /** {@hide} */ + public WatchedArrayMap(int capacity, boolean identityHashCode) { + mStorage = new ArrayMap<K, V>(capacity, identityHashCode); + } + + /** + * Create a new {@link WatchedArrayMap} with the mappings from the given {@link Map}. + */ + public WatchedArrayMap(@Nullable Map<? extends K, ? extends V> map) { + mStorage = new ArrayMap<K, V>(); + if (map != null) { + putAll(map); + } + } + + /** + * Return the underlying storage. This breaks the wrapper but is necessary when + * passing the array to distant methods. + */ + public ArrayMap untrackedMap() { + return mStorage; + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() { + // The storage cannot be simply cleared. Each element in the storage must be + // unregistered. Deregistration is only needed if the array is actually + // watching. + if (mWatching) { + final int end = mStorage.size(); + for (int i = 0; i < end; i++) { + unregisterChild(mStorage.valueAt(i)); + } + } + mStorage.clear(); + onChanged(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean containsKey(Object key) { + return mStorage.containsKey(key); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean containsValue(Object value) { + return mStorage.containsValue(value); + } + + /** + * {@inheritDoc} + */ + @Override + public Set<Map.Entry<K, V>> entrySet() { + return Collections.unmodifiableSet(mStorage.entrySet()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (o instanceof WatchedArrayMap) { + WatchedArrayMap w = (WatchedArrayMap) o; + return mStorage.equals(w.mStorage); + } else { + return false; + } + } + + /** + * {@inheritDoc} + */ + @Override + public V get(Object key) { + return mStorage.get(key); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return mStorage.hashCode(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isEmpty() { + return mStorage.isEmpty(); + } + + /** + * {@inheritDoc} + */ + @Override + public Set<K> keySet() { + return Collections.unmodifiableSet(mStorage.keySet()); + } + + /** + * {@inheritDoc} + */ + @Override + public V put(K key, V value) { + final V result = mStorage.put(key, value); + registerChild(value); + onChanged(); + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public void putAll(@NonNull Map<? extends K, ? extends V> map) { + for (Map.Entry<? extends K, ? extends V> element : map.entrySet()) { + put(element.getKey(), element.getValue()); + } + } + + /** + * {@inheritDoc} + */ + @Override + public V remove(@NonNull Object key) { + final V result = mStorage.remove(key); + unregisterChildIf(result); + onChanged(); + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public int size() { + return mStorage.size(); + } + + /** + * {@inheritDoc} + */ + @Override + public Collection<V> values() { + return Collections.unmodifiableCollection(mStorage.values()); + } + + // Methods supported by ArrayMap that are not part of Map + + /** + * Return the key at the given index in the array. + * + * <p>For indices outside of the range <code>0...size()-1</code>, an + * {@link ArrayIndexOutOfBoundsException} is thrown.</p> + * + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the key stored at the given index. + */ + public K keyAt(int index) { + return mStorage.keyAt(index); + } + + /** + * Return the value at the given index in the array. + * + * <p>For indices outside of the range <code>0...size()-1</code>, an + * {@link ArrayIndexOutOfBoundsException} is thrown.</p> + * + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the value stored at the given index. + */ + public V valueAt(int index) { + return mStorage.valueAt(index); + } + + /** + * Remove an existing key from the array map. + * @param key The key of the mapping to remove. + * @return Returns the value that was stored under the key, or null if there + * was no such key. + */ + public int indexOfKey(K key) { + return mStorage.indexOfKey(key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified value, or a negative number if no keys map to the + * specified value. + * Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + */ + public int indexOfValue(V value) { + return mStorage.indexOfValue(value); + } + + /** + * Set the value at a given index in the array. + * + * <p>For indices outside of the range <code>0...size()-1</code>, an + * {@link ArrayIndexOutOfBoundsException} is thrown.</p> + * + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @param value The new value to store at this index. + * @return Returns the previous value at the given index. + */ + public V setValueAt(int index, V value) { + final V result = mStorage.setValueAt(index, value); + if (value != result) { + unregisterChildIf(result); + registerChild(value); + onChanged(); + } + return result; + } + + /** + * Remove the key/value mapping at the given index. + * + * <p>For indices outside of the range <code>0...size()-1</code>, an + * {@link ArrayIndexOutOfBoundsException} is thrown.</p> + * + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the value that was stored at this index. + */ + public V removeAt(int index) { + final V result = mStorage.removeAt(index); + unregisterChildIf(result); + onChanged(); + return result; + } +} diff --git a/services/core/java/com/android/server/utils/WatchedSparseArray.java b/services/core/java/com/android/server/utils/WatchedSparseArray.java new file mode 100644 index 000000000000..4d43b682e113 --- /dev/null +++ b/services/core/java/com/android/server/utils/WatchedSparseArray.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2020 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.server.utils; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import android.util.SparseArray; + +import java.util.ArrayList; + +/** + * A watched variant of SparseArray. If a {@link Watchable} is stored in the array, the + * array registers with the {@link Watchable}. The array registers only once with each + * {@link Watchable} no matter how many times the {@link Watchable} is stored in the + * array. + */ +public class WatchedSparseArray<E> extends WatchableImpl { + + // The storage + private final SparseArray<E> mStorage; + + // If true, the array is watching its children + private boolean mWatching = false; + + // The local observer + private final Watcher mObserver = new Watcher() { + @Override + public void onChange(@Nullable Watchable o) { + WatchedSparseArray.this.dispatchChange(o); + } + }; + + /** + * A private convenience function that notifies registered listeners that an element + * has been added to or removed from the array. The what parameter is the array itself. + */ + private void onChanged() { + dispatchChange(this); + } + + /** + * A convenience function. Register the object if it is {@link Watchable} and if the + * array is currently watching. Note that the watching flag must be true if this + * function is to succeed. + */ + private void registerChild(Object o) { + if (mWatching && o instanceof Watchable) { + ((Watchable) o).registerObserver(mObserver); + } + } + + /** + * A convenience function. Unregister the object if it is {@link Watchable} and if + * the array is currently watching. Note that the watching flag must be true if this + * function is to succeed. + */ + private void unregisterChild(Object o) { + if (mWatching && o instanceof Watchable) { + ((Watchable) o).unregisterObserver(mObserver); + } + } + + /** + * A convenience function. Unregister the object if it is {@link Watchable}, if the array is + * currently watching, and if the storage does not contain the object. Note that the watching + * flag must be true if this function is to succeed. This must be called after an object has + * been removed from the storage. + */ + private void unregisterChildIf(Object o) { + if (mWatching && o instanceof Watchable) { + if (mStorage.indexOfValue((E) o) == -1) { + ((Watchable) o).unregisterObserver(mObserver); + } + } + } + + /** + * Register a {@link Watcher} with the array. If this is the first Watcher than any + * array values that are {@link Watchable} are registered to the array itself. + */ + @Override + public void registerObserver(@NonNull Watcher observer) { + super.registerObserver(observer); + if (registeredObserverCount() == 1) { + // The watching flag must be set true before any children are registered. + mWatching = true; + final int end = mStorage.size(); + for (int i = 0; i < end; i++) { + registerChild(mStorage.valueAt(i)); + } + } + } + + /** + * Unregister a {@link Watcher} from the array. If this is the last Watcher than any + * array values that are {@link Watchable} are unregistered to the array itself. + */ + @Override + public void unregisterObserver(@NonNull Watcher observer) { + super.unregisterObserver(observer); + if (registeredObserverCount() == 0) { + final int end = mStorage.size(); + for (int i = 0; i < end; i++) { + unregisterChild(mStorage.valueAt(i)); + } + // The watching flag must be true while children are unregistered. + mWatching = false; + } + } + + /** + * Creates a new WatchedSparseArray containing no mappings. + */ + public WatchedSparseArray() { + mStorage = new SparseArray(); + } + + /** + * Creates a new WatchedSparseArray containing no mappings that + * will not require any additional memory allocation to store the + * specified number of mappings. If you supply an initial capacity of + * 0, the sparse array will be initialized with a light-weight + * representation not requiring any additional array allocations. + */ + public WatchedSparseArray(int initialCapacity) { + mStorage = new SparseArray(initialCapacity); + } + + /** + * The copy constructor does not copy the watcher data. + */ + public WatchedSparseArray(@NonNull WatchedSparseArray<E> r) { + mStorage = r.mStorage.clone(); + } + + /** + * Returns true if the key exists in the array. This is equivalent to + * {@link #indexOfKey(int)} >= 0. + * + * @param key Potential key in the mapping + * @return true if the key is defined in the mapping + */ + public boolean contains(int key) { + return mStorage.contains(key); + } + + /** + * Gets the Object mapped from the specified key, or <code>null</code> + * if no such mapping has been made. + */ + public E get(int key) { + return mStorage.get(key); + } + + /** + * Gets the Object mapped from the specified key, or the specified Object + * if no such mapping has been made. + */ + public E get(int key, E valueIfKeyNotFound) { + return mStorage.get(key, valueIfKeyNotFound); + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(int key) { + final E child = mStorage.get(key); + mStorage.delete(key); + unregisterChildIf(child); + onChanged(); + } + + /** + * @hide + * Removes the mapping from the specified key, if there was any, returning the old value. + */ + public E removeReturnOld(int key) { + final E result = mStorage.removeReturnOld(key); + unregisterChildIf(result); + return result; + } + + /** + * Alias for {@link #delete(int)}. + */ + public void remove(int key) { + delete(key); + } + + /** + * Removes the mapping at the specified index. + * + * <p>For indices outside of the range <code>0...size()-1</code>, an + * {@link ArrayIndexOutOfBoundsException} is thrown.</p> + */ + public void removeAt(int index) { + final E child = mStorage.valueAt(index); + mStorage.removeAt(index); + unregisterChildIf(child); + onChanged(); + } + + /** + * Remove a range of mappings as a batch. + * + * @param index Index to begin at + * @param size Number of mappings to remove + * + * <p>For indices outside of the range <code>0...size()-1</code>, + * the behavior is undefined.</p> + */ + public void removeAtRange(int index, int size) { + final ArrayList<E> children = new ArrayList<>(); + try { + for (int i = 0; i < size; i++) { + children.add(mStorage.valueAt(i + index)); + } + } catch (Exception e) { + // Ignore any exception and proceed with removal. + } + try { + mStorage.removeAtRange(index, size); + } finally { + // Even on exception, make sure to deregister children that have been + // removed. + for (int i = 0; i < size; i++) { + unregisterChildIf(children.get(i)); + } + } + onChanged(); + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(int key, E value) { + final E old = mStorage.get(key); + mStorage.put(key, value); + unregisterChildIf(old); + registerChild(value); + onChanged(); + } + + /** + * Returns the number of key-value mappings that this SparseArray + * currently stores. + */ + public int size() { + return mStorage.size(); + } + + /** + * Given an index in the range <code>0...size()-1</code>, returns + * the key from the <code>index</code>th key-value mapping that this + * SparseArray stores. + * + * <p>The keys corresponding to indices in ascending order are guaranteed to + * be in ascending order, e.g., <code>keyAt(0)</code> will return the + * smallest key and <code>keyAt(size()-1)</code> will return the largest + * key.</p> + * + * <p>For indices outside of the range <code>0...size()-1</code>, + * the behavior is undefined for apps targeting {@link android.os.Build.VERSION_CODES#P} and + * earlier, and an {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting + * {@link android.os.Build.VERSION_CODES#Q} and later.</p> + */ + public int keyAt(int index) { + return mStorage.keyAt(index); + } + + /** + * Given an index in the range <code>0...size()-1</code>, returns + * the value from the <code>index</code>th key-value mapping that this + * SparseArray stores. + * + * <p>The values corresponding to indices in ascending order are guaranteed + * to be associated with keys in ascending order, e.g., + * <code>valueAt(0)</code> will return the value associated with the + * smallest key and <code>valueAt(size()-1)</code> will return the value + * associated with the largest key.</p> + * + * <p>For indices outside of the range <code>0...size()-1</code>, + * the behavior is undefined for apps targeting {@link android.os.Build.VERSION_CODES#P} and + * earlier, and an {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting + * {@link android.os.Build.VERSION_CODES#Q} and later.</p> + */ + public E valueAt(int index) { + return mStorage.valueAt(index); + } + + /** + * Given an index in the range <code>0...size()-1</code>, sets a new + * value for the <code>index</code>th key-value mapping that this + * SparseArray stores. + * + * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for + * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an + * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting + * {@link android.os.Build.VERSION_CODES#Q} and later.</p> + */ + public void setValueAt(int index, E value) { + final E old = mStorage.valueAt(index); + mStorage.setValueAt(index, value); + if (value != old) { + unregisterChildIf(old); + registerChild(value); + onChanged(); + } + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(int key) { + return mStorage.indexOfKey(key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified value, or a negative number if no keys map to the + * specified value. + * <p>Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + * <p>Note also that unlike most collections' {@code indexOf} methods, + * this method compares values using {@code ==} rather than {@code equals}. + */ + public int indexOfValue(E value) { + return mStorage.indexOfValue(value); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified value, or a negative number if no keys map to the + * specified value. + * <p>Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + * <p>Note also that this method uses {@code equals} unlike {@code indexOfValue}. + * @hide + */ + public int indexOfValueByValue(E value) { + return mStorage.indexOfValueByValue(value); + } + + /** + * Removes all key-value mappings from this SparseArray. + */ + public void clear() { + // The storage cannot be simply cleared. Each element in the storage must be + // unregistered. Deregistration is only needed if the array is actually + // watching. + if (mWatching) { + final int end = mStorage.size(); + for (int i = 0; i < end; i++) { + unregisterChild(mStorage.valueAt(i)); + } + } + mStorage.clear(); + onChanged(); + } + + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(int key, E value) { + mStorage.append(key, value); + registerChild(value); + onChanged(); + } + + /** + * <p>This implementation composes a string by iterating over its mappings. If + * this map contains itself as a value, the string "(this Map)" + * will appear in its place. + */ + @Override + public String toString() { + return mStorage.toString(); + } +} diff --git a/services/core/java/com/android/server/utils/WatchedSparseBooleanArray.java b/services/core/java/com/android/server/utils/WatchedSparseBooleanArray.java new file mode 100644 index 000000000000..edf7d27b61dd --- /dev/null +++ b/services/core/java/com/android/server/utils/WatchedSparseBooleanArray.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2020 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.server.utils; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import android.util.SparseBooleanArray; + +/** + * A watched variant of SparseBooleanArray. Changes to the array are notified to + * registered {@link Watcher}s. + */ +public class WatchedSparseBooleanArray extends WatchableImpl { + + // The storage + private final SparseBooleanArray mStorage; + + // A private convenience function + private void dispatchChange() { + dispatchChange(this); + } + + /** + * Creates a new WatchedSparseBooleanArray containing no mappings. + */ + public WatchedSparseBooleanArray() { + mStorage = new SparseBooleanArray(); + } + + /** + * Creates a new WatchedSparseBooleanArray containing no mappings that + * will not require any additional memory allocation to store the + * specified number of mappings. If you supply an initial capacity of + * 0, the sparse array will be initialized with a light-weight + * representation not requiring any additional array allocations. + */ + public WatchedSparseBooleanArray(int initialCapacity) { + mStorage = new SparseBooleanArray(initialCapacity); + } + + /** + * The copy constructor does not copy the watcher data. + */ + public WatchedSparseBooleanArray(@NonNull WatchedSparseBooleanArray r) { + mStorage = r.mStorage.clone(); + } + + /** + * Gets the boolean mapped from the specified key, or <code>false</code> + * if no such mapping has been made. + */ + public boolean get(int key) { + return mStorage.get(key); + } + + /** + * Gets the boolean mapped from the specified key, or the specified value + * if no such mapping has been made. + */ + public boolean get(int key, boolean valueIfKeyNotFound) { + return mStorage.get(key, valueIfKeyNotFound); + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(int key) { + mStorage.delete(key); + dispatchChange(); + } + + /** + * Removes the mapping at the specified index. + * <p> + * For indices outside of the range {@code 0...size()-1}, the behavior is undefined. + */ + public void removeAt(int index) { + mStorage.removeAt(index); + dispatchChange(); + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(int key, boolean value) { + if (mStorage.get(key) != value) { + mStorage.put(key, value); + dispatchChange(); + } + } + + /** + * Returns the number of key-value mappings that this SparseBooleanArray + * currently stores. + */ + public int size() { + return mStorage.size(); + } + + /** + * Given an index in the range <code>0...size()-1</code>, returns + * the key from the <code>index</code>th key-value mapping that this + * SparseBooleanArray stores. + * + * <p>The keys corresponding to indices in ascending order are guaranteed to + * be in ascending order, e.g., <code>keyAt(0)</code> will return the + * smallest key and <code>keyAt(size()-1)</code> will return the largest + * key.</p> + * + * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for + * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an + * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting + * {@link android.os.Build.VERSION_CODES#Q} and later.</p> + */ + public int keyAt(int index) { + return mStorage.keyAt(index); + } + + /** + * Given an index in the range <code>0...size()-1</code>, returns + * the value from the <code>index</code>th key-value mapping that this + * SparseBooleanArray stores. + * + * <p>The values corresponding to indices in ascending order are guaranteed + * to be associated with keys in ascending order, e.g., + * <code>valueAt(0)</code> will return the value associated with the + * smallest key and <code>valueAt(size()-1)</code> will return the value + * associated with the largest key.</p> + * + * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for + * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an + * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting + * {@link android.os.Build.VERSION_CODES#Q} and later.</p> + */ + public boolean valueAt(int index) { + return mStorage.valueAt(index); + } + + /** + * Directly set the value at a particular index. + * + * <p>For indices outside of the range <code>0...size()-1</code>, the behavior is undefined for + * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an + * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting + * {@link android.os.Build.VERSION_CODES#Q} and later.</p> + */ + public void setValueAt(int index, boolean value) { + if (mStorage.valueAt(index) != value) { + mStorage.setValueAt(index, value); + dispatchChange(); + } + } + + /** @hide */ + public void setKeyAt(int index, int key) { + if (mStorage.keyAt(index) != key) { + mStorage.setKeyAt(index, key); + dispatchChange(); + } + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(int key) { + return mStorage.indexOfKey(key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + * Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + */ + public int indexOfValue(boolean value) { + return mStorage.indexOfValue(value); + } + + /** + * Removes all key-value mappings from this SparseBooleanArray. + */ + public void clear() { + mStorage.clear(); + dispatchChange(); + } + + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(int key, boolean value) { + mStorage.append(key, value); + dispatchChange(); + } + + @Override + public int hashCode() { + return mStorage.hashCode(); + } + + @Override + public boolean equals(Object that) { + return this == that || mStorage.equals(that); + } + + /** + * {@inheritDoc} + * + * <p>This implementation composes a string by iterating over its mappings. + */ + @Override + public String toString() { + return mStorage.toString(); + } +} diff --git a/services/core/java/com/android/server/utils/Watcher.java b/services/core/java/com/android/server/utils/Watcher.java new file mode 100644 index 000000000000..a9bfbfa0c7dc --- /dev/null +++ b/services/core/java/com/android/server/utils/Watcher.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2020 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.server.utils; + +import android.annotation.Nullable; + +/** + * This class receives notifications when watched objects detect changes. + * Must be implemented by objects which are registered to a {@link Watchable}. + */ +public abstract class Watcher { + + /** + * This method is called when {@link Watchable} detects a change. The <what> + * parameter indicates what changed, but it may be null. The parameter is intended + * for debugging. + * + * @param what The {@link Watchable} that changed. + */ + public abstract void onChange(@Nullable Watchable what); +} diff --git a/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java b/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java new file mode 100644 index 000000000000..40575e4cf16f --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java @@ -0,0 +1,304 @@ +/** + * Copyright (c) 2020, 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.server.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import com.android.internal.util.Preconditions; +import com.android.internal.util.TraceBuffer; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Test class for {@link Watcher}, {@link Watchable}, {@link WatchableImpl}, + * {@link WatchedArrayMap}, {@link WatchedSparseArray}, and + * {@link WatchedSparseBooleanArray}. + * + * Build/Install/Run: + * atest FrameworksServicesTests:WatcherTest + */ +@SmallTest +public class WatcherTest { + + // A small Watchable leaf node + private class Leaf extends WatchableImpl { + private int datum = 0; + void set(int i) { + if (datum != i) { + datum = i; + dispatchChange(this); + } + } + void tick() { + set(datum + 1); + } + } + + // A top-most watcher. It counts the number of notifications that it receives. + private class Tester extends Watcher { + // The count of changes. + public int changes = 0; + + // The single Watchable that this monitors. + public final Watchable mWatched; + + // The key, used for messages + public String mKey; + + // Create the Tester with a Watcher + public Tester(Watchable w, String k) { + mWatched = w; + mKey = k; + } + + // Listen for events + public void register() { + mWatched.registerObserver(this); + } + + // Stop listening for events + public void unregister() { + mWatched.unregisterObserver(this); + } + + // Count the number of notifications received. + @Override + public void onChange(Watchable what) { + changes++; + } + + // Verify the count. + public void verify(int want, String msg) { + assertEquals(mKey + " " + msg, want, changes); + } + } + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void test_notify() { + + Tester tester; + + // Create a few leaves + Leaf a = new Leaf(); + Leaf b = new Leaf(); + Leaf c = new Leaf(); + Leaf d = new Leaf(); + + // Basic test. Create a leaf and verify that changes to the leaf get notified to + // the tester. + tester = new Tester(a, "Leaf"); + tester.verify(0, "Initial leaf - no registration"); + a.tick(); + tester.verify(0, "Updates with no registration"); + tester.register(); + a.tick(); + tester.verify(1, "Updates with registration"); + a.tick(); + a.tick(); + tester.verify(3, "Updates with registration"); + + // Add the same leaf to more than one tester. Verify that a change to the leaf is seen by + // all registered listeners. + Tester buddy1 = new Tester(a, "Leaf2"); + Tester buddy2 = new Tester(a, "Leaf3"); + buddy1.verify(0, "Initial leaf - no registration"); + buddy2.verify(0, "Initial leaf - no registration"); + a.tick(); + tester.verify(4, "Updates with buddies"); + buddy1.verify(0, "Updates - no registration"); + buddy2.verify(0, "Updates - no registration"); + buddy1.register(); + buddy2.register(); + buddy1.verify(0, "No updates - registered"); + buddy2.verify(0, "No updates - registered"); + a.tick(); + buddy1.verify(1, "First update"); + buddy2.verify(1, "First update"); + buddy1.unregister(); + a.tick(); + buddy1.verify(1, "Second update - unregistered"); + buddy2.verify(2, "Second update"); + + buddy1 = null; + buddy2 = null; + + final int INDEX_A = 1; + final int INDEX_B = 2; + final int INDEX_C = 3; + final int INDEX_D = 4; + + // Test WatchedArrayMap + WatchedArrayMap<Integer, Leaf> am = new WatchedArrayMap<>(); + am.put(INDEX_A, a); + am.put(INDEX_B, b); + tester = new Tester(am, "WatchedArrayMap"); + tester.verify(0, "Initial array - no registration"); + a.tick(); + tester.verify(0, "Updates with no registration"); + tester.register(); + tester.verify(0, "Updates with no registration"); + a.tick(); + tester.verify(1, "Updates with registration"); + b.tick(); + tester.verify(2, "Updates with registration"); + am.remove(INDEX_B); + tester.verify(3, "Removed b"); + b.tick(); + tester.verify(3, "Updates with b not watched"); + am.put(INDEX_B, b); + am.put(INDEX_C, b); + tester.verify(5, "Added b twice"); + b.tick(); + tester.verify(6, "Changed b - single notification"); + am.remove(INDEX_C); + tester.verify(7, "Removed first b"); + b.tick(); + tester.verify(8, "Changed b - single notification"); + am.remove(INDEX_B); + tester.verify(9, "Removed second b"); + b.tick(); + tester.verify(9, "Updated b - no change"); + am.clear(); + tester.verify(10, "Cleared array"); + b.tick(); + tester.verify(10, "Change to b not in array"); + + // Special methods + am.put(INDEX_C, c); + tester.verify(11, "Added c"); + c.tick(); + tester.verify(12, "Ticked c"); + am.setValueAt(am.indexOfKey(INDEX_C), d); + tester.verify(13, "Replaced c with d"); + c.tick(); + d.tick(); + tester.verify(14, "Ticked d and c (c not registered)"); + + am = null; + + // Test WatchedSparseArray + WatchedSparseArray<Leaf> sa = new WatchedSparseArray<>(); + sa.put(INDEX_A, a); + sa.put(INDEX_B, b); + tester = new Tester(sa, "WatchedSparseArray"); + tester.verify(0, "Initial array - no registration"); + a.tick(); + tester.verify(0, "Updates with no registration"); + tester.register(); + tester.verify(0, "Updates with no registration"); + a.tick(); + tester.verify(1, "Updates with registration"); + b.tick(); + tester.verify(2, "Updates with registration"); + sa.remove(INDEX_B); + tester.verify(3, "Removed b"); + b.tick(); + tester.verify(3, "Updates with b not watched"); + sa.put(INDEX_B, b); + sa.put(INDEX_C, b); + tester.verify(5, "Added b twice"); + b.tick(); + tester.verify(6, "Changed b - single notification"); + sa.remove(INDEX_C); + tester.verify(7, "Removed first b"); + b.tick(); + tester.verify(8, "Changed b - single notification"); + sa.remove(INDEX_B); + tester.verify(9, "Removed second b"); + b.tick(); + tester.verify(9, "Updated b - no change"); + sa.clear(); + tester.verify(10, "Cleared array"); + b.tick(); + tester.verify(10, "Change to b not in array"); + + // Special methods + sa.put(INDEX_A, a); + sa.put(INDEX_B, b); + sa.put(INDEX_C, c); + tester.verify(13, "Added c"); + c.tick(); + tester.verify(14, "Ticked c"); + sa.setValueAt(sa.indexOfKey(INDEX_C), d); + tester.verify(15, "Replaced c with d"); + c.tick(); + d.tick(); + tester.verify(16, "Ticked d and c (c not registered)"); + sa.append(INDEX_D, c); + tester.verify(17, "Append c"); + c.tick(); + d.tick(); + tester.verify(19, "Ticked d and c"); + assertEquals("Verify four elements", 4, sa.size()); + // Figure out which elements are at which indices. + Leaf[] x = new Leaf[4]; + for (int i = 0; i < 4; i++) { + x[i] = sa.valueAt(i); + } + sa.removeAtRange(0, 2); + tester.verify(20, "Removed two elements in one operation"); + x[0].tick(); + x[1].tick(); + tester.verify(20, "Ticked two removed elements"); + x[2].tick(); + x[3].tick(); + tester.verify(22, "Ticked two remaining elements"); + + sa = null; + + // Test WatchedSparseBooleanArray + WatchedSparseBooleanArray sb = new WatchedSparseBooleanArray(); + tester = new Tester(sb, "WatchedSparseBooleanArray"); + tester.verify(0, "Initial array - no registration"); + sb.put(INDEX_A, true); + tester.verify(0, "Updates with no registration"); + tester.register(); + tester.verify(0, "Updates with no registration"); + sb.put(INDEX_B, true); + tester.verify(1, "Updates with registration"); + sb.put(INDEX_B, true); + tester.verify(1, "Null update"); + sb.put(INDEX_B, false); + sb.put(INDEX_C, true); + tester.verify(3, "Updates with registration"); + // Special methods + sb.put(INDEX_C, true); + tester.verify(3, "Added true, no change"); + sb.setValueAt(sb.indexOfKey(INDEX_C), false); + tester.verify(4, "Replaced true with false"); + sb.append(INDEX_D, true); + tester.verify(5, "Append true"); + + sb = null; + } +} |