blob: 5070dd1675d38f559847d268a4b66d98f4242fb3 [file] [log] [blame]
/*
* 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.ArraySet;
/**
* WatchedArraySet is an {@link android.util.ArraySet} that can report changes to itself. If its
* values are {@link Watchable} then the WatchedArraySet 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.
* @param <E> The element type
*/
public class WatchedArraySet<E> extends WatchableImpl
implements Snappable {
// The storage
private final ArraySet<E> mStorage;
// If true, the array is watching its children
private volatile boolean mWatching = false;
// The local observer
private final Watcher mObserver = new Watcher() {
@Override
public void onChange(@Nullable Watchable what) {
WatchedArraySet.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.contains(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 WatchedArraySet}. The default capacity of an array map
* is 0, and will grow once items are added to it.
*/
public WatchedArraySet() {
this(0, false);
}
/**
* Create a new {@link WatchedArraySet} with a given initial capacity.
*/
public WatchedArraySet(int capacity) {
this(capacity, false);
}
/** {@hide} */
public WatchedArraySet(int capacity, boolean identityHashCode) {
mStorage = new ArraySet<E>(capacity, identityHashCode);
}
/**
* Create a new {@link WatchedArraySet} with items from the given array
*/
public WatchedArraySet(@Nullable E[] array) {
mStorage = new ArraySet(array);
}
/**
* Create a {@link WatchedArraySet} from an {@link ArraySet}
*/
public WatchedArraySet(@NonNull ArraySet<E> c) {
mStorage = new ArraySet<>(c);
}
/**
* Create a {@link WatchedArraySet} from an {@link WatchedArraySet}
*/
public WatchedArraySet(@NonNull WatchedArraySet<E> c) {
mStorage = new ArraySet<>(c.mStorage);
}
/**
* Make <this> a copy of src. Any data in <this> is discarded.
*/
public void copyFrom(@NonNull ArraySet<E> src) {
clear();
final int end = src.size();
mStorage.ensureCapacity(end);
for (int i = 0; i < end; i++) {
add(src.valueAt(i));
}
}
/**
* Make dst a copy of <this>. Any previous data in dst is discarded.
*/
public void copyTo(@NonNull ArraySet<E> dst) {
dst.clear();
final int end = size();
dst.ensureCapacity(end);
for (int i = 0; i < end; i++) {
dst.add(valueAt(i));
}
}
/**
* Return the underlying storage. This breaks the wrapper but is necessary when
* passing the array to distant methods.
*/
public ArraySet<E> untrackedStorage() {
return mStorage;
}
/**
* Make the array map empty. All storage is released.
*/
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();
}
/**
* Check whether a value exists in the set.
*
* @param key The value to search for.
* @return Returns true if the value exists, else false.
*/
public boolean contains(Object key) {
return mStorage.contains(key);
}
/**
* Returns the index of a value in the set.
*
* @param key The value to search for.
* @return Returns the index of the value if it exists, else a negative integer.
*/
public int indexOf(Object key) {
return mStorage.indexOf(key);
}
/**
* 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 E valueAt(int index) {
return mStorage.valueAt(index);
}
/**
* Return true if the array map contains no items.
*/
public boolean isEmpty() {
return mStorage.isEmpty();
}
/**
* Adds the specified object to this set. The set is not modified if it
* already contains the object.
*
* @param value the object to add.
* @return {@code true} if this set is modified, {@code false} otherwise.
*/
public boolean add(E value) {
final boolean result = mStorage.add(value);
registerChild(value);
onChanged();
return result;
}
/**
* Special fast path for appending items to the end of the array without validation.
* The array must already be large enough to contain the item.
* @hide
*/
public void append(E value) {
mStorage.append(value);
registerChild(value);
onChanged();
}
/**
* Perform a {@link #add(Object)} of all values in <var>array</var>
* @param array The array whose contents are to be retrieved.
*/
public void addAll(ArraySet<? extends E> array) {
final int end = array.size();
for (int i = 0; i < end; i++) {
add(array.valueAt(i));
}
}
/**
* Perform a {@link #add(Object)} of all values in <var>array</var>
* @param array The array whose contents are to be retrieved.
*/
public void addAll(WatchedArraySet<? extends E> array) {
final int end = array.size();
for (int i = 0; i < end; i++) {
add(array.valueAt(i));
}
}
/**
* Removes the specified object from this set.
*
* @param o the object to remove.
* @return {@code true} if this set was modified, {@code false} otherwise.
*/
public boolean remove(Object o) {
if (mStorage.remove(o)) {
unregisterChildIf(o);
onChanged();
return true;
}
return false;
}
/**
* 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 E removeAt(int index) {
final E result = mStorage.removeAt(index);
unregisterChildIf(result);
onChanged();
return result;
}
/**
* Perform a {@link #remove(Object)} of all values in <var>array</var>
* @param array The array whose contents are to be removed.
*/
public boolean removeAll(ArraySet<? extends E> array) {
final int end = array.size();
boolean any = false;
for (int i = 0; i < end; i++) {
any = remove(array.valueAt(i)) || any;
}
return any;
}
/**
* Return the number of items in this array map.
*/
public int size() {
return mStorage.size();
}
/**
* {@inheritDoc}
*
* <p>This implementation returns false if the object is not a set, or
* if the sets have different sizes. Otherwise, for each value in this
* set, it checks to make sure the value also exists in the other set.
* If any value doesn't exist, the method returns false; otherwise, it
* returns true.
*/
@Override
public boolean equals(@Nullable Object object) {
if (object instanceof WatchedArraySet) {
return mStorage.equals(((WatchedArraySet) object).mStorage);
} else {
return mStorage.equals(object);
}
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return mStorage.hashCode();
}
/**
* {@inheritDoc}
*
* <p>This implementation composes a string by iterating over its values. If
* this set contains itself as a value, the string "(this Set)"
* will appear in its place.
*/
@Override
public String toString() {
return mStorage.toString();
}
/**
* Create a copy of the array. If the element is a subclass of Snapper then the copy
* contains snapshots of the elements. Otherwise the copy contains references to the
* elements. The returned snapshot is immutable.
* @return A new array whose elements are the elements of <this>.
*/
public WatchedArraySet<E> snapshot() {
WatchedArraySet<E> l = new WatchedArraySet<>();
snapshot(l, this);
return l;
}
/**
* Make <this> a snapshot of the argument. Note that <this> is immutable when the
* method returns. <this> must be empty when the function is called.
* @param r The source array, which is copied into <this>
*/
public void snapshot(@NonNull WatchedArraySet<E> r) {
snapshot(this, r);
}
/**
* Make the destination a copy of the source. If the element is a subclass of Snapper then the
* copy contains snapshots of the elements. Otherwise the copy contains references to the
* elements. The destination must be initially empty. Upon return, the destination is
* immutable.
* @param dst The destination array. It must be empty.
* @param src The source array. It is not modified.
*/
public static <E> void snapshot(@NonNull WatchedArraySet<E> dst,
@NonNull WatchedArraySet<E> src) {
if (dst.size() != 0) {
throw new IllegalArgumentException("snapshot destination is not empty");
}
final int end = src.size();
dst.mStorage.ensureCapacity(end);
for (int i = 0; i < end; i++) {
final E val = Snapshots.maybeSnapshot(src.valueAt(i));
dst.append(val);
}
dst.seal();
}
}