diff options
12 files changed, 1720 insertions, 35 deletions
diff --git a/services/core/java/com/android/server/pm/AppsFilter.java b/services/core/java/com/android/server/pm/AppsFilter.java index 094be0622bab..b76fff579918 100644 --- a/services/core/java/com/android/server/pm/AppsFilter.java +++ b/services/core/java/com/android/server/pm/AppsFilter.java @@ -443,7 +443,7 @@ public class AppsFilter implements Watchable, Snappable { } final StateProvider stateProvider = command -> { synchronized (injector.getLock()) { - command.currentState(injector.getSettings().getPackagesLocked().untrackedMap(), + command.currentState(injector.getSettings().getPackagesLocked().untrackedStorage(), injector.getUserManagerInternal().getUserInfos()); } }; @@ -979,7 +979,7 @@ public class AppsFilter implements Watchable, Snappable { @Nullable SparseArray<int[]> getVisibilityAllowList(PackageSetting setting, int[] users, WatchedArrayMap<String, PackageSetting> existingSettings) { - return getVisibilityAllowList(setting, users, existingSettings.untrackedMap()); + return getVisibilityAllowList(setting, users, existingSettings.untrackedStorage()); } /** diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 7c4dadea89eb..f2aaee2e529f 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -471,7 +471,7 @@ public final class Settings implements Watchable, Snappable { private final File mSystemDir; public final KeySetManagerService mKeySetManagerService = - new KeySetManagerService(mPackages.untrackedMap()); + new KeySetManagerService(mPackages.untrackedStorage()); /** Settings and other information about permissions */ final LegacyPermissionSettings mPermissions; diff --git a/services/core/java/com/android/server/utils/WatchableImpl.java b/services/core/java/com/android/server/utils/WatchableImpl.java index 16400b186ab0..d17fca1d7a54 100644 --- a/services/core/java/com/android/server/utils/WatchableImpl.java +++ b/services/core/java/com/android/server/utils/WatchableImpl.java @@ -100,7 +100,7 @@ public class WatchableImpl implements Watchable { /** * Freeze the {@link Watchable}. - **/ + */ public void seal() { synchronized (mObservers) { mSealed = true; diff --git a/services/core/java/com/android/server/utils/Watched.java b/services/core/java/com/android/server/utils/Watched.java new file mode 100644 index 000000000000..d4a68ee735fd --- /dev/null +++ b/services/core/java/com/android/server/utils/Watched.java @@ -0,0 +1,32 @@ +/* + * 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 java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation type to mark an attribute that is monitored for change detection and + * snapshot creation. + * TODO(b/176923052) Automate validation of @Watchable attributes. + */ +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.CLASS) +public @interface Watched { +} diff --git a/services/core/java/com/android/server/utils/WatchedArrayList.java b/services/core/java/com/android/server/utils/WatchedArrayList.java new file mode 100644 index 000000000000..bb0ba1329d86 --- /dev/null +++ b/services/core/java/com/android/server/utils/WatchedArrayList.java @@ -0,0 +1,416 @@ +/* + * 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.Collection; + +/** + * 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. + * @param <E> The element type, stored in the array. + */ +public class WatchedArrayList<E> extends WatchableImpl + implements Snappable { + + // The storage + private final ArrayList<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) { + WatchedArrayList.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.get(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.get(i)); + } + // The watching flag must be true while children are unregistered. + mWatching = false; + } + } + + /** + * Create a new empty {@link WatchedArrayList}. The default capacity of an array map + * is 0, and will grow once items are added to it. + */ + public WatchedArrayList() { + this(0); + } + + /** + * Create a new {@link WatchedArrayList} with a given initial capacity. + */ + public WatchedArrayList(int capacity) { + mStorage = new ArrayList<E>(capacity); + } + + /** + * Create a new {@link WatchedArrayList} with the content of the collection. + */ + public WatchedArrayList(@Nullable Collection<? extends E> c) { + mStorage = new ArrayList<E>(); + if (c != null) { + // There is no need to register children because the WatchedArrayList starts + // life unobserved. + mStorage.addAll(c); + } + } + + /** + * Create a {@link WatchedArrayList} from an {@link ArrayList} + */ + public WatchedArrayList(@NonNull ArrayList<E> c) { + mStorage = new ArrayList<>(c); + } + + /** + * Create a {@link WatchedArrayList} from an {@link WatchedArrayList} + */ + public WatchedArrayList(@NonNull WatchedArrayList<E> c) { + mStorage = new ArrayList<>(c.mStorage); + } + + /** + * Make <this> a copy of src. Any data in <this> is discarded. + */ + public void copyFrom(@NonNull ArrayList<E> src) { + clear(); + final int end = src.size(); + mStorage.ensureCapacity(end); + for (int i = 0; i < end; i++) { + add(src.get(i)); + } + } + + /** + * Make dst a copy of <this>. Any previous data in dst is discarded. + */ + public void copyTo(@NonNull ArrayList<E> dst) { + dst.clear(); + final int end = size(); + dst.ensureCapacity(end); + for (int i = 0; i < end; i++) { + dst.add(get(i)); + } + } + + /** + * Return the underlying storage. This breaks the wrapper but is necessary when + * passing the array to distant methods. + */ + public ArrayList<E> untrackedStorage() { + return mStorage; + } + + /** + * Append the specified element to the end of the list + */ + public boolean add(E value) { + final boolean result = mStorage.add(value); + registerChild(value); + onChanged(); + return result; + } + + /** + * Insert the element into the list + */ + public void add(int index, E value) { + mStorage.add(index, value); + registerChild(value); + onChanged(); + } + + /** + * Append the elements of the collection to the list. + */ + public boolean addAll(Collection<? extends E> c) { + if (c.size() > 0) { + for (E e: c) { + mStorage.add(e); + } + onChanged(); + return true; + } else { + return false; + } + } + + /** + * Insert the elements of the collection into the list at the index. + */ + public boolean addAll(int index, Collection<? extends E> c) { + if (c.size() > 0) { + for (E e: c) { + mStorage.add(index++, e); + } + onChanged(); + return true; + } else { + return false; + } + } + + + /** + * Remove all elements from the list. + */ + 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.get(i)); + } + } + mStorage.clear(); + onChanged(); + } + + /** + * Return true if the object is in the array. + */ + public boolean contains(Object o) { + return mStorage.contains(o); + } + + /** + * Ensure capacity. + */ + public void ensureCapacity(int min) { + mStorage.ensureCapacity(min); + } + + /** + * Retrieve the element at the specified index. + */ + public E get(int index) { + return mStorage.get(index); + } + + /** + * Return the index of the object. -1 is returned if the object is not in the list. + */ + public int indexOf(Object o) { + return mStorage.indexOf(o); + } + + /** + * True if the list has no elements + */ + public boolean isEmpty() { + return mStorage.isEmpty(); + } + + /** + * Return the index of the last occurrence of the object. + */ + public int lastIndexOf(Object o) { + return mStorage.lastIndexOf(o); + } + + /** + * Remove and return the element at the specified position. + */ + public E remove(int index) { + final E result = mStorage.remove(index); + unregisterChildIf(result); + onChanged(); + return result; + } + + /** + * Remove the first occurrence of the object in the list. Return true if the object + * was actually in the list and false otherwise. + */ + public boolean remove(Object o) { + if (mStorage.remove(o)) { + unregisterChildIf(o); + onChanged(); + return true; + } + return false; + } + + /** + * Replace the object at the index. + */ + public E set(int index, E value) { + final E result = mStorage.set(index, value); + if (value != result) { + unregisterChildIf(result); + registerChild(value); + onChanged(); + } + return result; + } + + /** + * Return the number of elements in the list. + */ + public int size() { + return mStorage.size(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(@Nullable Object o) { + if (o instanceof WatchedArrayList) { + WatchedArrayList w = (WatchedArrayList) o; + return mStorage.equals(w.mStorage); + } else { + return false; + } + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return mStorage.hashCode(); + } + + /** + * 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 WatchedArrayList<E> snapshot() { + WatchedArrayList<E> l = new WatchedArrayList<>(size()); + 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 WatchedArrayList<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 WatchedArrayList<E> dst, + @NonNull WatchedArrayList<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.get(i)); + dst.add(i, val); + } + dst.seal(); + } +} diff --git a/services/core/java/com/android/server/utils/WatchedArrayMap.java b/services/core/java/com/android/server/utils/WatchedArrayMap.java index e8065f140af7..7c1cde8502bd 100644 --- a/services/core/java/com/android/server/utils/WatchedArrayMap.java +++ b/services/core/java/com/android/server/utils/WatchedArrayMap.java @@ -160,10 +160,48 @@ public class WatchedArrayMap<K, V> extends WatchableImpl } /** + * Create a {@link WatchedArrayMap} from an {@link ArrayMap} + */ + public WatchedArrayMap(@NonNull ArrayMap<K, V> c) { + mStorage = new ArrayMap<>(c); + } + + /** + * Create a {@link WatchedArrayMap} from an {@link WatchedArrayMap} + */ + public WatchedArrayMap(@NonNull WatchedArrayMap<K, V> c) { + mStorage = new ArrayMap<>(c.mStorage); + } + + /** + * Make <this> a copy of src. Any data in <this> is discarded. + */ + public void copyFrom(@NonNull ArrayMap<K, V> src) { + clear(); + final int end = src.size(); + mStorage.ensureCapacity(end); + for (int i = 0; i < end; i++) { + put(src.keyAt(i), src.valueAt(i)); + } + } + + /** + * Make dst a copy of <this>. Any previous data in dst is discarded. + */ + public void copyTo(@NonNull ArrayMap<K, V> dst) { + dst.clear(); + final int end = size(); + dst.ensureCapacity(end); + for (int i = 0; i < end; i++) { + dst.put(keyAt(i), valueAt(i)); + } + } + + /** * Return the underlying storage. This breaks the wrapper but is necessary when * passing the array to distant methods. */ - public ArrayMap untrackedMap() { + public ArrayMap<K, V> untrackedStorage() { return mStorage; } @@ -213,7 +251,7 @@ public class WatchedArrayMap<K, V> extends WatchableImpl * {@inheritDoc} */ @Override - public boolean equals(Object o) { + public boolean equals(@Nullable Object o) { if (o instanceof WatchedArrayMap) { WatchedArrayMap w = (WatchedArrayMap) o; return mStorage.equals(w.mStorage); @@ -401,6 +439,15 @@ public class WatchedArrayMap<K, V> extends WatchableImpl } /** + * 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 WatchedArrayMap<K, V> 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 @@ -414,6 +461,7 @@ public class WatchedArrayMap<K, V> extends WatchableImpl 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 V val = Snapshots.maybeSnapshot(src.valueAt(i)); final K key = src.keyAt(i); diff --git a/services/core/java/com/android/server/utils/WatchedArraySet.java b/services/core/java/com/android/server/utils/WatchedArraySet.java new file mode 100644 index 000000000000..5070dd1675d3 --- /dev/null +++ b/services/core/java/com/android/server/utils/WatchedArraySet.java @@ -0,0 +1,434 @@ +/* + * 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(); + } +} diff --git a/services/core/java/com/android/server/utils/WatchedSparseArray.java b/services/core/java/com/android/server/utils/WatchedSparseArray.java index 6797c6eb7801..9b99b9176d19 100644 --- a/services/core/java/com/android/server/utils/WatchedSparseArray.java +++ b/services/core/java/com/android/server/utils/WatchedSparseArray.java @@ -143,6 +143,13 @@ public class WatchedSparseArray<E> extends WatchableImpl } /** + * Create a {@link WatchedSparseArray} from a {@link SparseArray} + */ + public WatchedSparseArray(@NonNull SparseArray<E> c) { + mStorage = c.clone(); + } + + /** * The copy constructor does not copy the watcher data. */ public WatchedSparseArray(@NonNull WatchedSparseArray<E> r) { @@ -150,6 +157,36 @@ public class WatchedSparseArray<E> extends WatchableImpl } /** + * Make <this> a copy of src. Any data in <this> is discarded. + */ + public void copyFrom(@NonNull SparseArray<E> src) { + clear(); + final int end = src.size(); + for (int i = 0; i < end; i++) { + put(src.keyAt(i), src.valueAt(i)); + } + } + + /** + * Make dst a copy of <this>. Any previous data in dst is discarded. + */ + public void copyTo(@NonNull SparseArray<E> dst) { + dst.clear(); + final int end = size(); + for (int i = 0; i < end; i++) { + dst.put(keyAt(i), valueAt(i)); + } + } + + /** + * Return the underlying storage. This breaks the wrapper but is necessary when + * passing the array to distant methods. + */ + public SparseArray<E> untrackedStorage() { + return mStorage; + } + + /** * Returns true if the key exists in the array. This is equivalent to * {@link #indexOfKey(int)} >= 0. * @@ -390,6 +427,21 @@ public class WatchedSparseArray<E> extends WatchableImpl onChanged(); } + @Override + public int hashCode() { + return mStorage.hashCode(); + } + + @Override + public boolean equals(@Nullable Object o) { + if (o instanceof WatchedSparseArray) { + WatchedSparseArray w = (WatchedSparseArray) o; + return mStorage.equals(w.mStorage); + } else { + return false; + } + } + /** * <p>This implementation composes a string by iterating over its mappings. If * this map contains itself as a value, the string "(this Map)" @@ -407,12 +459,21 @@ public class WatchedSparseArray<E> extends WatchableImpl * @return A new array whose elements are the elements of <this>. */ public WatchedSparseArray<E> snapshot() { - WatchedSparseArray<E> l = new WatchedSparseArray<>(); + WatchedSparseArray<E> l = new WatchedSparseArray<>(size()); 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 WatchedSparseArray<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 diff --git a/services/core/java/com/android/server/utils/WatchedSparseBooleanArray.java b/services/core/java/com/android/server/utils/WatchedSparseBooleanArray.java index b845eea168a5..772a8d07cffb 100644 --- a/services/core/java/com/android/server/utils/WatchedSparseBooleanArray.java +++ b/services/core/java/com/android/server/utils/WatchedSparseBooleanArray.java @@ -17,6 +17,7 @@ package com.android.server.utils; import android.annotation.NonNull; +import android.annotation.Nullable; import android.util.SparseBooleanArray; /** @@ -53,6 +54,13 @@ public class WatchedSparseBooleanArray extends WatchableImpl } /** + * Create a {@link WatchedSparseBooleanArray} from a {@link SparseBooleanArray} + */ + public WatchedSparseBooleanArray(@NonNull SparseBooleanArray c) { + mStorage = c.clone(); + } + + /** * The copy constructor does not copy the watcher data. */ public WatchedSparseBooleanArray(@NonNull WatchedSparseBooleanArray r) { @@ -60,6 +68,36 @@ public class WatchedSparseBooleanArray extends WatchableImpl } /** + * Make <this> a copy of src. Any data in <this> is discarded. + */ + public void copyFrom(@NonNull SparseBooleanArray src) { + clear(); + final int end = src.size(); + for (int i = 0; i < end; i++) { + put(src.keyAt(i), src.valueAt(i)); + } + } + + /** + * Make dst a copy of <this>. Any previous data in dst is discarded. + */ + public void copyTo(@NonNull SparseBooleanArray dst) { + dst.clear(); + final int end = size(); + for (int i = 0; i < end; i++) { + dst.put(keyAt(i), valueAt(i)); + } + } + + /** + * Return the underlying storage. This breaks the wrapper but is necessary when + * passing the array to distant methods. + */ + public SparseBooleanArray untrackedStorage() { + return mStorage; + } + + /** * Gets the boolean mapped from the specified key, or <code>false</code> * if no such mapping has been made. */ @@ -99,10 +137,10 @@ public class WatchedSparseBooleanArray extends WatchableImpl * was one. */ public void put(int key, boolean value) { - if (mStorage.get(key) != value) { - mStorage.put(key, value); - onChanged(); - } + // There is no fast way to know if the key exists with the input value, so this + // method always notifies change listeners. + mStorage.put(key, value); + onChanged(); } /** @@ -219,8 +257,13 @@ public class WatchedSparseBooleanArray extends WatchableImpl } @Override - public boolean equals(Object that) { - return this == that || mStorage.equals(that); + public boolean equals(@Nullable Object o) { + if (o instanceof WatchedSparseBooleanArray) { + WatchedSparseBooleanArray w = (WatchedSparseBooleanArray) o; + return mStorage.equals(w.mStorage); + } else { + return false; + } } /** @@ -249,13 +292,26 @@ public class WatchedSparseBooleanArray extends WatchableImpl * @param r The source array, which is copied into <this> */ public void snapshot(@NonNull WatchedSparseBooleanArray r) { - if (size() != 0) { + 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 void snapshot(@NonNull WatchedSparseBooleanArray dst, + @NonNull WatchedSparseBooleanArray src) { + if (dst.size() != 0) { throw new IllegalArgumentException("snapshot destination is not empty"); } - final int end = r.size(); + final int end = src.size(); for (int i = 0; i < end; i++) { - put(r.keyAt(i), r.valueAt(i)); + dst.put(src.keyAt(i), src.valueAt(i)); } - seal(); + dst.seal(); } } diff --git a/services/core/java/com/android/server/utils/WatchedSparseIntArray.java b/services/core/java/com/android/server/utils/WatchedSparseIntArray.java new file mode 100644 index 000000000000..72705bf24199 --- /dev/null +++ b/services/core/java/com/android/server/utils/WatchedSparseIntArray.java @@ -0,0 +1,323 @@ +/* + * 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.SparseIntArray; + +/** + * A watched variant of SparseIntArray. Changes to the array are notified to + * registered {@link Watcher}s. + */ +public class WatchedSparseIntArray extends WatchableImpl + implements Snappable { + + // The storage + private final SparseIntArray mStorage; + + // A private convenience function + private void onChanged() { + dispatchChange(this); + } + + /** + * Creates a new WatchedSparseIntArray containing no mappings. + */ + public WatchedSparseIntArray() { + mStorage = new SparseIntArray(); + } + + /** + * Creates a new WatchedSparseIntArray 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 WatchedSparseIntArray(int initialCapacity) { + mStorage = new SparseIntArray(initialCapacity); + } + + /** + * Create a {@link WatchedSparseIntArray} from a {@link SparseIntArray} + */ + public WatchedSparseIntArray(@NonNull SparseIntArray c) { + mStorage = c.clone(); + } + + /** + * The copy constructor does not copy the watcher data. + */ + public WatchedSparseIntArray(@NonNull WatchedSparseIntArray r) { + mStorage = r.mStorage.clone(); + } + + /** + * Make <this> a copy of src. Any data in <this> is discarded. + */ + public void copyFrom(@NonNull SparseIntArray src) { + clear(); + final int end = src.size(); + for (int i = 0; i < end; i++) { + put(src.keyAt(i), src.valueAt(i)); + } + } + + /** + * Make dst a copy of <this>. Any previous data in dst is discarded. + */ + public void copyTo(@NonNull SparseIntArray dst) { + dst.clear(); + final int end = size(); + for (int i = 0; i < end; i++) { + dst.put(keyAt(i), valueAt(i)); + } + } + + /** + * Return the underlying storage. This breaks the wrapper but is necessary when + * passing the array to distant methods. + */ + public SparseIntArray untrackedStorage() { + return mStorage; + } + + /** + * Gets the boolean mapped from the specified key, or <code>false</code> + * if no such mapping has been made. + */ + public int 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 int get(int key, int valueIfKeyNotFound) { + return mStorage.get(key, valueIfKeyNotFound); + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(int key) { + // This code ensures that onChanged is called only if the key is actually + // present. + final int index = mStorage.indexOfKey(key); + if (index >= 0) { + mStorage.removeAt(index); + onChanged(); + } + } + + /** + * Removes the mapping at the specified index. + */ + public void removeAt(int index) { + mStorage.removeAt(index); + 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, int value) { + // There is no fast way to know if the key exists with the input value, so this + // method always notifies change listeners. + mStorage.put(key, value); + onChanged(); + } + + /** + * Returns the number of key-value mappings that this SparseIntArray + * 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 + * SparseIntArray 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 + * SparseIntArray 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 int 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, int value) { + if (mStorage.valueAt(index) != value) { + mStorage.setValueAt(index, 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 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(int value) { + return mStorage.indexOfValue(value); + } + + /** + * Removes all key-value mappings from this SparseIntArray. + */ + public void clear() { + final int count = size(); + mStorage.clear(); + if (count > 0) { + 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, int value) { + mStorage.append(key, value); + onChanged(); + } + + /** + * Provides a copy of keys. + **/ + public int[] copyKeys() { + return mStorage.copyKeys(); + } + + @Override + public int hashCode() { + return mStorage.hashCode(); + } + + @Override + public boolean equals(@Nullable Object o) { + if (o instanceof WatchedSparseIntArray) { + WatchedSparseIntArray w = (WatchedSparseIntArray) o; + return mStorage.equals(w.mStorage); + } else { + return false; + } + } + + /** + * {@inheritDoc} + * + * <p>This implementation composes a string by iterating over its mappings. + */ + @Override + public String toString() { + return mStorage.toString(); + } + + /** + * Create a snapshot. The snapshot does not include any {@link Watchable} + * information. + */ + public WatchedSparseIntArray snapshot() { + WatchedSparseIntArray l = new WatchedSparseIntArray(this); + l.seal(); + 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 WatchedSparseIntArray 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 void snapshot(@NonNull WatchedSparseIntArray dst, + @NonNull WatchedSparseIntArray src) { + if (dst.size() != 0) { + throw new IllegalArgumentException("snapshot destination is not empty"); + } + final int end = src.size(); + for (int i = 0; i < end; i++) { + dst.put(src.keyAt(i), src.valueAt(i)); + } + dst.seal(); + } + +} diff --git a/services/tests/servicestests/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/servicestests/src/com/android/server/pm/PackageManagerSettingsTests.java index 282047afaa51..333ec9295b93 100644 --- a/services/tests/servicestests/src/com/android/server/pm/PackageManagerSettingsTests.java +++ b/services/tests/servicestests/src/com/android/server/pm/PackageManagerSettingsTests.java @@ -1215,7 +1215,7 @@ public class PackageManagerSettingsTests { private void verifyKeySetMetaData(Settings settings) throws ReflectiveOperationException, IllegalAccessException { ArrayMap<String, PackageSetting> packages = - settings.mPackages.untrackedMap(); + settings.mPackages.untrackedStorage(); KeySetManagerService ksms = settings.mKeySetManagerService; /* verify keyset and public key ref counts */ diff --git a/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java b/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java index 9bea9d4cedbd..7c65dc03a57e 100644 --- a/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java +++ b/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java @@ -20,12 +20,20 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.SparseArray; +import android.util.SparseBooleanArray; +import android.util.SparseIntArray; + import androidx.test.filters.SmallTest; import org.junit.After; import org.junit.Before; import org.junit.Test; +import java.util.ArrayList; + /** * Test class for {@link Watcher}, {@link Watchable}, {@link WatchableImpl}, * {@link WatchedArrayMap}, {@link WatchedSparseArray}, and @@ -40,7 +48,7 @@ public class WatcherTest { // A counter to generate unique IDs for Leaf elements. private int mLeafId = 0; - // Useful indices used int the tests. + // Useful indices used in the tests. private static final int INDEX_A = 1; private static final int INDEX_B = 2; private static final int INDEX_C = 3; @@ -171,6 +179,7 @@ public class WatcherTest { @Test public void testWatchedArrayMap() { + final String name = "WatchedArrayMap"; WatchableTester tester; // Create a few leaves @@ -183,7 +192,7 @@ public class WatcherTest { WatchedArrayMap<Integer, Leaf> array = new WatchedArrayMap<>(); array.put(INDEX_A, leafA); array.put(INDEX_B, leafB); - tester = new WatchableTester(array, "WatchedArrayMap"); + tester = new WatchableTester(array, name); tester.verify(0, "Initial array - no registration"); leafA.tick(); tester.verify(0, "Updates with no registration"); @@ -231,20 +240,20 @@ public class WatcherTest { final WatchedArrayMap<Integer, Leaf> arraySnap = array.snapshot(); tester.verify(14, "Generate snapshot (no changes)"); // Verify that the snapshot is a proper copy of the source. - assertEquals("WatchedArrayMap snap same size", + assertEquals(name + " snap same size", array.size(), arraySnap.size()); for (int i = 0; i < array.size(); i++) { for (int j = 0; j < arraySnap.size(); j++) { - assertTrue("WatchedArrayMap elements differ", + assertTrue(name + " elements differ", array.valueAt(i) != arraySnap.valueAt(j)); } - assertTrue("WatchedArrayMap element copy", + assertTrue(name + " element copy", array.valueAt(i).equals(arraySnap.valueAt(i))); } leafD.tick(); tester.verify(15, "Tick after snapshot"); // Verify that the snapshot is sealed - verifySealed("WatchedArrayMap", ()->arraySnap.put(INDEX_A, leafA)); + verifySealed(name, ()->arraySnap.put(INDEX_A, leafA)); } // Recreate the snapshot since the test corrupted it. { @@ -253,10 +262,235 @@ public class WatcherTest { final Leaf arraySnapElement = arraySnap.valueAt(0); verifySealed("ArraySnapshotElement", ()->arraySnapElement.tick()); } + // Verify copy-in/out + { + final String msg = name + " copy-in/out failed"; + ArrayMap<Integer, Leaf> base = new ArrayMap<>(); + array.copyTo(base); + WatchedArrayMap<Integer, Leaf> copy = new WatchedArrayMap<>(); + copy.copyFrom(base); + if (!array.equals(copy)) { + fail(msg); + } + } + } + + @Test + public void testWatchedArraySet() { + final String name = "WatchedArraySet"; + WatchableTester tester; + + // Create a few leaves + Leaf leafA = new Leaf(); + Leaf leafB = new Leaf(); + Leaf leafC = new Leaf(); + Leaf leafD = new Leaf(); + + // Test WatchedArraySet + WatchedArraySet<Leaf> array = new WatchedArraySet<>(); + array.add(leafA); + array.add(leafB); + tester = new WatchableTester(array, name); + tester.verify(0, "Initial array - no registration"); + leafA.tick(); + tester.verify(0, "Updates with no registration"); + tester.register(); + tester.verify(0, "Updates with no registration"); + leafA.tick(); + tester.verify(1, "Updates with registration"); + leafB.tick(); + tester.verify(2, "Updates with registration"); + array.remove(leafB); + tester.verify(3, "Removed b"); + leafB.tick(); + tester.verify(3, "Updates with b not watched"); + array.add(leafB); + array.add(leafB); + tester.verify(5, "Added b once"); + leafB.tick(); + tester.verify(6, "Changed b - single notification"); + array.remove(leafB); + tester.verify(7, "Removed b"); + leafB.tick(); + tester.verify(7, "Changed b - not watched"); + array.remove(leafB); + tester.verify(7, "Removed non-existent b"); + array.clear(); + tester.verify(8, "Cleared array"); + leafA.tick(); + tester.verify(8, "Change to a not in array"); + + // Special methods + array.add(leafA); + array.add(leafB); + array.add(leafC); + tester.verify(11, "Added a, b, c"); + leafC.tick(); + tester.verify(12, "Ticked c"); + array.removeAt(array.indexOf(leafC)); + tester.verify(13, "Removed c"); + leafC.tick(); + tester.verify(13, "Ticked c, not registered"); + array.append(leafC); + tester.verify(14, "Append c"); + leafC.tick(); + leafD.tick(); + tester.verify(15, "Ticked d and c"); + assertEquals("Verify three elements", 3, array.size()); + + // Snapshot + { + final WatchedArraySet<Leaf> arraySnap = array.snapshot(); + tester.verify(15, "Generate snapshot (no changes)"); + // Verify that the snapshot is a proper copy of the source. + assertEquals(name + " snap same size", + array.size(), arraySnap.size()); + for (int i = 0; i < array.size(); i++) { + for (int j = 0; j < arraySnap.size(); j++) { + assertTrue(name + " elements differ", + array.valueAt(i) != arraySnap.valueAt(j)); + } + } + leafC.tick(); + tester.verify(16, "Tick after snapshot"); + // Verify that the array snapshot is sealed + verifySealed(name, ()->arraySnap.add(leafB)); + } + // Recreate the snapshot since the test corrupted it. + { + final WatchedArraySet<Leaf> arraySnap = array.snapshot(); + // Verify that elements are also snapshots + final Leaf arraySnapElement = arraySnap.valueAt(0); + verifySealed(name + " snap element", ()->arraySnapElement.tick()); + } + // Verify copy-in/out + { + final String msg = name + " copy-in/out"; + ArraySet<Leaf> base = new ArraySet<>(); + array.copyTo(base); + WatchedArraySet<Leaf> copy = new WatchedArraySet<>(); + copy.copyFrom(base); + if (!array.equals(copy)) { + fail(msg); + } + } + } + + @Test + public void testWatchedArrayList() { + final String name = "WatchedArrayList"; + WatchableTester tester; + + // Create a few leaves + Leaf leafA = new Leaf(); + Leaf leafB = new Leaf(); + Leaf leafC = new Leaf(); + Leaf leafD = new Leaf(); + + // Redefine the indices used in the tests to be zero-based + final int indexA = 0; + final int indexB = 1; + final int indexC = 2; + final int indexD = 3; + + // Test WatchedArrayList + WatchedArrayList<Leaf> array = new WatchedArrayList<>(); + // A spacer that takes up index 0 (and is not Watchable). + array.add(indexA, leafA); + array.add(indexB, leafB); + tester = new WatchableTester(array, name); + tester.verify(0, "Initial array - no registration"); + leafA.tick(); + tester.verify(0, "Updates with no registration"); + tester.register(); + tester.verify(0, "Updates with no registration"); + leafA.tick(); + tester.verify(1, "Updates with registration"); + leafB.tick(); + tester.verify(2, "Updates with registration"); + array.remove(indexB); + tester.verify(3, "Removed b"); + leafB.tick(); + tester.verify(3, "Updates with b not watched"); + array.add(indexB, leafB); + array.add(indexC, leafB); + tester.verify(5, "Added b twice"); + leafB.tick(); + tester.verify(6, "Changed b - single notification"); + array.remove(indexC); + tester.verify(7, "Removed first b"); + leafB.tick(); + tester.verify(8, "Changed b - single notification"); + array.remove(indexB); + tester.verify(9, "Removed second b"); + leafB.tick(); + tester.verify(9, "Updated leafB - no change"); + array.clear(); + tester.verify(10, "Cleared array"); + leafB.tick(); + tester.verify(10, "Change to b not in array"); + + // Special methods + array.add(indexA, leafA); + array.add(indexB, leafB); + array.add(indexC, leafC); + tester.verify(13, "Added c"); + leafC.tick(); + tester.verify(14, "Ticked c"); + array.set(array.indexOf(leafC), leafD); + tester.verify(15, "Replaced c with d"); + leafC.tick(); + leafD.tick(); + tester.verify(16, "Ticked d and c (c not registered)"); + array.add(leafC); + tester.verify(17, "Append c"); + leafC.tick(); + leafD.tick(); + tester.verify(19, "Ticked d and c"); + + // Snapshot + { + final WatchedArrayList<Leaf> arraySnap = array.snapshot(); + tester.verify(19, "Generate snapshot (no changes)"); + // Verify that the snapshot is a proper copy of the source. + assertEquals(name + " snap same size", + array.size(), arraySnap.size()); + for (int i = 0; i < array.size(); i++) { + for (int j = 0; j < arraySnap.size(); j++) { + assertTrue(name + " elements differ", + array.get(i) != arraySnap.get(j)); + } + assertTrue(name + " element copy", + array.get(i).equals(arraySnap.get(i))); + } + leafD.tick(); + tester.verify(20, "Tick after snapshot"); + // Verify that the array snapshot is sealed + verifySealed(name, ()->arraySnap.add(indexA, leafB)); + } + // Recreate the snapshot since the test corrupted it. + { + final WatchedArrayList<Leaf> arraySnap = array.snapshot(); + // Verify that elements are also snapshots + final Leaf arraySnapElement = arraySnap.get(0); + verifySealed("ArraySnapshotElement", ()->arraySnapElement.tick()); + } + // Verify copy-in/out + { + final String msg = name + " copy-in/out"; + ArrayList<Leaf> base = new ArrayList<>(); + array.copyTo(base); + WatchedArrayList<Leaf> copy = new WatchedArrayList<>(); + copy.copyFrom(base); + if (!array.equals(copy)) { + fail(msg); + } + } } @Test public void testWatchedSparseArray() { + final String name = "WatchedSparseArray"; WatchableTester tester; // Create a few leaves @@ -269,7 +503,7 @@ public class WatcherTest { WatchedSparseArray<Leaf> array = new WatchedSparseArray<>(); array.put(INDEX_A, leafA); array.put(INDEX_B, leafB); - tester = new WatchableTester(array, "WatchedSparseArray"); + tester = new WatchableTester(array, name); tester.verify(0, "Initial array - no registration"); leafA.tick(); tester.verify(0, "Updates with no registration"); @@ -338,20 +572,20 @@ public class WatcherTest { final WatchedSparseArray<Leaf> arraySnap = array.snapshot(); tester.verify(22, "Generate snapshot (no changes)"); // Verify that the snapshot is a proper copy of the source. - assertEquals("WatchedSparseArray snap same size", + assertEquals(name + " snap same size", array.size(), arraySnap.size()); for (int i = 0; i < array.size(); i++) { for (int j = 0; j < arraySnap.size(); j++) { - assertTrue("WatchedSparseArray elements differ", + assertTrue(name + " elements differ", array.valueAt(i) != arraySnap.valueAt(j)); } - assertTrue("WatchedArrayMap element copy", + assertTrue(name + " element copy", array.valueAt(i).equals(arraySnap.valueAt(i))); } leafD.tick(); tester.verify(23, "Tick after snapshot"); // Verify that the array snapshot is sealed - verifySealed("WatchedSparseArray", ()->arraySnap.put(INDEX_A, leafB)); + verifySealed(name, ()->arraySnap.put(INDEX_A, leafB)); } // Recreate the snapshot since the test corrupted it. { @@ -360,15 +594,30 @@ public class WatcherTest { final Leaf arraySnapElement = arraySnap.valueAt(0); verifySealed("ArraySnapshotElement", ()->arraySnapElement.tick()); } + // Verify copy-in/out + { + final String msg = name + " copy-in/out"; + SparseArray<Leaf> base = new SparseArray<>(); + array.copyTo(base); + WatchedSparseArray<Leaf> copy = new WatchedSparseArray<>(); + copy.copyFrom(base); + final int end = array.size(); + assertTrue(msg + " size mismatch " + end + " " + copy.size(), end == copy.size()); + for (int i = 0; i < end; i++) { + final int key = array.keyAt(i); + assertTrue(msg, array.get(i) == copy.get(i)); + } + } } @Test public void testWatchedSparseBooleanArray() { + final String name = "WatchedSparseBooleanArray"; WatchableTester tester; // Test WatchedSparseBooleanArray WatchedSparseBooleanArray array = new WatchedSparseBooleanArray(); - tester = new WatchableTester(array, "WatchedSparseBooleanArray"); + tester = new WatchableTester(array, name); tester.verify(0, "Initial array - no registration"); array.put(INDEX_A, true); tester.verify(0, "Updates with no registration"); @@ -376,14 +625,10 @@ public class WatcherTest { tester.verify(0, "Updates with no registration"); array.put(INDEX_B, true); tester.verify(1, "Updates with registration"); - array.put(INDEX_B, true); - tester.verify(1, "Null update"); array.put(INDEX_B, false); array.put(INDEX_C, true); tester.verify(3, "Updates with registration"); // Special methods - array.put(INDEX_C, true); - tester.verify(3, "Added true, no change"); array.setValueAt(array.indexOfKey(INDEX_C), false); tester.verify(4, "Replaced true with false"); array.append(INDEX_D, true); @@ -403,7 +648,77 @@ public class WatcherTest { array.put(INDEX_D, false); tester.verify(6, "Tick after snapshot"); // Verify that the array is sealed - verifySealed("WatchedSparseBooleanArray", ()->arraySnap.put(INDEX_D, false)); + verifySealed(name, ()->arraySnap.put(INDEX_D, false)); + } + // Verify copy-in/out + { + final String msg = name + " copy-in/out"; + SparseBooleanArray base = new SparseBooleanArray(); + array.copyTo(base); + WatchedSparseBooleanArray copy = new WatchedSparseBooleanArray(); + copy.copyFrom(base); + final int end = array.size(); + assertTrue(msg + " size mismatch/2 " + end + " " + copy.size(), end == copy.size()); + for (int i = 0; i < end; i++) { + final int key = array.keyAt(i); + assertTrue(msg + " element", array.get(i) == copy.get(i)); + } + } + } + + @Test + public void testWatchedSparseIntArray() { + final String name = "WatchedSparseIntArray"; + WatchableTester tester; + + // Test WatchedSparseIntArray + WatchedSparseIntArray array = new WatchedSparseIntArray(); + tester = new WatchableTester(array, name); + tester.verify(0, "Initial array - no registration"); + array.put(INDEX_A, 1); + tester.verify(0, "Updates with no registration"); + tester.register(); + tester.verify(0, "Updates with no registration"); + array.put(INDEX_B, 2); + tester.verify(1, "Updates with registration"); + array.put(INDEX_B, 4); + array.put(INDEX_C, 5); + tester.verify(3, "Updates with registration"); + // Special methods + array.setValueAt(array.indexOfKey(INDEX_C), 7); + tester.verify(4, "Replaced 6 with 7"); + array.append(INDEX_D, 8); + tester.verify(5, "Append 8"); + + // Snapshot + { + WatchedSparseIntArray arraySnap = array.snapshot(); + tester.verify(5, "Generate snapshot"); + // Verify that the snapshot is a proper copy of the source. + assertEquals("WatchedSparseIntArray snap same size", + array.size(), arraySnap.size()); + for (int i = 0; i < array.size(); i++) { + assertEquals(name + " element copy", + array.valueAt(i), arraySnap.valueAt(i)); + } + array.put(INDEX_D, 9); + tester.verify(6, "Tick after snapshot"); + // Verify that the array is sealed + verifySealed(name, ()->arraySnap.put(INDEX_D, 10)); + } + // Verify copy-in/out + { + final String msg = name + " copy-in/out"; + SparseIntArray base = new SparseIntArray(); + array.copyTo(base); + WatchedSparseIntArray copy = new WatchedSparseIntArray(); + copy.copyFrom(base); + final int end = array.size(); + assertTrue(msg + " size mismatch " + end + " " + copy.size(), end == copy.size()); + for (int i = 0; i < end; i++) { + final int key = array.keyAt(i); + assertTrue(msg, array.get(i) == copy.get(i)); + } } } } |