diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index dd8a86f94432b..eb09ad4478f21 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -316,7 +316,6 @@ import android.util.ExceptionUtils; import android.util.IntArray; import android.util.Log; import android.util.LogPrinter; -import android.util.LongSparseArray; import android.util.LongSparseLongArray; import android.util.MathUtils; import android.util.PackageUtils; @@ -401,6 +400,7 @@ import com.android.server.utils.TimingsTraceAndSlog; import com.android.server.utils.Watchable; import com.android.server.utils.Watched; import com.android.server.utils.WatchedArrayMap; +import com.android.server.utils.WatchedLongSparseArray; import com.android.server.utils.WatchedSparseBooleanArray; import com.android.server.utils.Watcher; import com.android.server.wm.ActivityTaskManagerInternal; @@ -1402,10 +1402,10 @@ public class PackageManagerService extends IPackageManager.Stub // Currently known shared libraries. @Watched - final WatchedArrayMap> mSharedLibraries = - new WatchedArrayMap<>(); + final WatchedArrayMap> + mSharedLibraries = new WatchedArrayMap<>(); @Watched - final WatchedArrayMap> + final WatchedArrayMap> mStaticLibsByDeclaringPackage = new WatchedArrayMap<>(); // Mapping from instrumentation class names to info about them. @@ -1786,8 +1786,8 @@ public class PackageManagerService extends IPackageManager.Stub public final Settings settings; public final SparseIntArray isolatedOwners; public final WatchedArrayMap packages; - public final WatchedArrayMap> sharedLibs; - public final WatchedArrayMap> staticLibs; + public final WatchedArrayMap> sharedLibs; + public final WatchedArrayMap> staticLibs; public final WatchedArrayMap instrumentation; public final WatchedSparseBooleanArray webInstantAppsDisabled; public final ComponentName resolveComponentName; @@ -2005,9 +2005,9 @@ public class PackageManagerService extends IPackageManager.Stub private final WatchedArrayMap mPackages; private final WatchedArrayMap mInstrumentation; - private final WatchedArrayMap> + private final WatchedArrayMap> mStaticLibsByDeclaringPackage; - private final WatchedArrayMap> + private final WatchedArrayMap> mSharedLibraries; private final ComponentName mLocalResolveComponentName; private final ActivityInfo mResolveActivity; @@ -3546,7 +3546,7 @@ public class PackageManagerService extends IPackageManager.Stub packageName = normalizedPackageName != null ? normalizedPackageName : packageName; // Is this a static library? - LongSparseArray versionedLib = + WatchedLongSparseArray versionedLib = mStaticLibsByDeclaringPackage.get(packageName); if (versionedLib == null || versionedLib.size() <= 0) { return packageName; @@ -8039,7 +8039,7 @@ public class PackageManagerService extends IPackageManager.Stub final int[] allUsers = mUserManager.getUserIds(); final int libCount = mSharedLibraries.size(); for (int i = 0; i < libCount; i++) { - final LongSparseArray versionedLib + final WatchedLongSparseArray versionedLib = mSharedLibraries.valueAt(i); if (versionedLib == null) { continue; @@ -8304,7 +8304,8 @@ public class PackageManagerService extends IPackageManager.Stub final int libCount = mSharedLibraries.size(); for (int i = 0; i < libCount; i++) { - LongSparseArray versionedLib = mSharedLibraries.valueAt(i); + WatchedLongSparseArray versionedLib = + mSharedLibraries.valueAt(i); if (versionedLib == null) { continue; } @@ -8373,7 +8374,8 @@ public class PackageManagerService extends IPackageManager.Stub int libraryCount = mSharedLibraries.size(); for (int i = 0; i < libraryCount; i++) { - LongSparseArray versionedLibrary = mSharedLibraries.valueAt(i); + WatchedLongSparseArray versionedLibrary = + mSharedLibraries.valueAt(i); if (versionedLibrary == null) { continue; } @@ -8534,7 +8536,8 @@ public class PackageManagerService extends IPackageManager.Stub Set libs = null; final int libCount = mSharedLibraries.size(); for (int i = 0; i < libCount; i++) { - LongSparseArray versionedLib = mSharedLibraries.valueAt(i); + WatchedLongSparseArray versionedLib = + mSharedLibraries.valueAt(i); if (versionedLib == null) { continue; } @@ -12199,10 +12202,10 @@ public class PackageManagerService extends IPackageManager.Stub @Nullable private static SharedLibraryInfo getSharedLibraryInfo(String name, long version, - Map> existingLibraries, - @Nullable Map> newLibraries) { + Map> existingLibraries, + @Nullable Map> newLibraries) { if (newLibraries != null) { - final LongSparseArray versionedLib = newLibraries.get(name); + final WatchedLongSparseArray versionedLib = newLibraries.get(name); SharedLibraryInfo info = null; if (versionedLib != null) { info = versionedLib.get(version); @@ -12211,7 +12214,7 @@ public class PackageManagerService extends IPackageManager.Stub return info; } } - final LongSparseArray versionedLib = existingLibraries.get(name); + final WatchedLongSparseArray versionedLib = existingLibraries.get(name); if (versionedLib == null) { return null; } @@ -12219,7 +12222,7 @@ public class PackageManagerService extends IPackageManager.Stub } private SharedLibraryInfo getLatestSharedLibraVersionLPr(AndroidPackage pkg) { - LongSparseArray versionedLib = mSharedLibraries.get( + WatchedLongSparseArray versionedLib = mSharedLibraries.get( pkg.getStaticSharedLibName()); if (versionedLib == null) { return null; @@ -12524,8 +12527,8 @@ public class PackageManagerService extends IPackageManager.Stub private static ArrayList collectSharedLibraryInfos(AndroidPackage pkg, Map availablePackages, - @NonNull final Map> existingLibraries, - @Nullable final Map> newLibraries) + @NonNull final Map> existingLibraries, + @Nullable final Map> newLibraries) throws PackageManagerException { if (pkg == null) { return null; @@ -12622,8 +12625,8 @@ public class PackageManagerService extends IPackageManager.Stub @NonNull String packageName, boolean required, int targetSdk, @Nullable ArrayList outUsedLibraries, @NonNull final Map availablePackages, - @NonNull final Map> existingLibraries, - @Nullable final Map> newLibraries) + @NonNull final Map> existingLibraries, + @Nullable final Map> newLibraries) throws PackageManagerException { final int libCount = requestedLibraries.size(); for (int i = 0; i < libCount; i++) { @@ -14044,7 +14047,7 @@ public class PackageManagerService extends IPackageManager.Stub long minVersionCode = Long.MIN_VALUE; long maxVersionCode = Long.MAX_VALUE; - LongSparseArray versionedLib = mSharedLibraries.get( + WatchedLongSparseArray versionedLib = mSharedLibraries.get( pkg.getStaticSharedLibName()); if (versionedLib != null) { final int versionCount = versionedLib.size(); @@ -14272,8 +14275,8 @@ public class PackageManagerService extends IPackageManager.Stub } private static boolean sharedLibExists(final String name, final long version, - Map> librarySource) { - LongSparseArray versionedLib = librarySource.get(name); + Map> librarySource) { + WatchedLongSparseArray versionedLib = librarySource.get(name); if (versionedLib != null && versionedLib.indexOfKey(version) >= 0) { return true; } @@ -14283,9 +14286,9 @@ public class PackageManagerService extends IPackageManager.Stub @GuardedBy("mLock") private void commitSharedLibraryInfoLocked(SharedLibraryInfo libraryInfo) { final String name = libraryInfo.getName(); - LongSparseArray versionedLib = mSharedLibraries.get(name); + WatchedLongSparseArray versionedLib = mSharedLibraries.get(name); if (versionedLib == null) { - versionedLib = new LongSparseArray<>(); + versionedLib = new WatchedLongSparseArray<>(); mSharedLibraries.put(name, versionedLib); } final String declaringPackageName = libraryInfo.getDeclaringPackage().getPackageName(); @@ -14296,7 +14299,7 @@ public class PackageManagerService extends IPackageManager.Stub } private boolean removeSharedLibraryLPw(String name, long version) { - LongSparseArray versionedLib = mSharedLibraries.get(name); + WatchedLongSparseArray versionedLib = mSharedLibraries.get(name); if (versionedLib == null) { return false; } @@ -18291,7 +18294,7 @@ public class PackageManagerService extends IPackageManager.Stub public final Map scannedPackages; public final Map allPackages; - public final Map> sharedLibrarySource; + public final Map> sharedLibrarySource; public final Map installArgs; public final Map installResults; public final Map preparedPackages; @@ -18302,7 +18305,7 @@ public class PackageManagerService extends IPackageManager.Stub Map installArgs, Map installResults, Map preparedPackages, - Map> sharedLibrarySource, + Map> sharedLibrarySource, Map allPackages, Map versionInfos, Map lastStaticSharedLibSettings) { @@ -18317,7 +18320,7 @@ public class PackageManagerService extends IPackageManager.Stub } private ReconcileRequest(Map scannedPackages, - Map> sharedLibrarySource, + Map> sharedLibrarySource, Map allPackages, Map versionInfos, Map lastStaticSharedLibSettings) { @@ -18414,7 +18417,7 @@ public class PackageManagerService extends IPackageManager.Stub combinedPackages.putAll(request.allPackages); - final Map> incomingSharedLibraries = + final Map> incomingSharedLibraries = new ArrayMap<>(); for (String installPackageName : scannedPackages.keySet()) { @@ -18638,7 +18641,7 @@ public class PackageManagerService extends IPackageManager.Stub */ private static List getAllowedSharedLibInfos( ScanResult scanResult, - Map> existingSharedLibraries) { + Map> existingSharedLibraries) { // Let's used the parsed package as scanResult.pkgSetting may be null final ParsedPackage parsedPackage = scanResult.request.parsedPackage; if (scanResult.staticSharedLibraryInfo == null @@ -18708,7 +18711,7 @@ public class PackageManagerService extends IPackageManager.Stub * added. */ private static boolean addSharedLibraryToPackageVersionMap( - Map> target, + Map> target, SharedLibraryInfo library) { final String name = library.getName(); if (target.containsKey(name)) { @@ -18720,7 +18723,7 @@ public class PackageManagerService extends IPackageManager.Stub return false; } } else { - target.put(name, new LongSparseArray<>()); + target.put(name, new WatchedLongSparseArray<>()); } target.get(name).put(library.getLongVersion(), library); return true; @@ -23843,7 +23846,7 @@ public class PackageManagerService extends IPackageManager.Stub final int numSharedLibraries = mSharedLibraries.size(); for (int index = 0; index < numSharedLibraries; index++) { final String libName = mSharedLibraries.keyAt(index); - LongSparseArray versionedLib + WatchedLongSparseArray versionedLib = mSharedLibraries.get(libName); if (versionedLib == null) { continue; @@ -24192,7 +24195,7 @@ public class PackageManagerService extends IPackageManager.Stub final int count = mSharedLibraries.size(); for (int i = 0; i < count; i++) { final String libName = mSharedLibraries.keyAt(i); - LongSparseArray versionedLib = mSharedLibraries.get(libName); + WatchedLongSparseArray versionedLib = mSharedLibraries.get(libName); if (versionedLib == null) { continue; } diff --git a/services/core/java/com/android/server/utils/WatchedLongSparseArray.java b/services/core/java/com/android/server/utils/WatchedLongSparseArray.java new file mode 100644 index 0000000000000..bf23de12ffef3 --- /dev/null +++ b/services/core/java/com/android/server/utils/WatchedLongSparseArray.java @@ -0,0 +1,418 @@ +/* + * 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.LongSparseArray; + +/** + * A watched variant of {@link LongSparseArray}. 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. + * @param The element type, stored in the array. + */ +public class WatchedLongSparseArray extends WatchableImpl + implements Snappable { + + // The storage + private final LongSparseArray 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 o) { + WatchedLongSparseArray.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 WatchedLongSparseArray() { + mStorage = new LongSparseArray(); + } + + /** + * 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 WatchedLongSparseArray(int initialCapacity) { + mStorage = new LongSparseArray(initialCapacity); + } + + /** + * Create a {@link WatchedLongSparseArray} from a {@link LongSparseArray}. + */ + public WatchedLongSparseArray(@NonNull LongSparseArray c) { + mStorage = c.clone(); + } + + /** + * The copy constructor does not copy the watcher data. + */ + public WatchedLongSparseArray(@NonNull WatchedLongSparseArray r) { + mStorage = r.mStorage.clone(); + } + + /** + * Make a copy of src. Any data in is discarded. + */ + public void copyFrom(@NonNull LongSparseArray 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 . Any previous data in dst is discarded. + */ + public void copyTo(@NonNull LongSparseArray 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 LongSparseArray untrackedStorage() { + return mStorage; + } + + /** + * Gets the Object mapped from the specified key, or null + * if no such mapping has been made. + */ + public E get(long 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(long key, E valueIfKeyNotFound) { + return mStorage.get(key, valueIfKeyNotFound); + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(long key) { + E old = mStorage.get(key, null); + mStorage.delete(key); + unregisterChildIf(old); + onChanged(); + + } + + /** + * Alias for {@link #delete(long)}. + */ + public void remove(long key) { + delete(key); + } + + /** + * Removes the mapping at the specified index. + * + *

For indices outside of the range 0...size()-1, an + * {@link ArrayIndexOutOfBoundsException} is thrown.

+ */ + public void removeAt(int index) { + E old = mStorage.valueAt(index); + mStorage.removeAt(index); + unregisterChildIf(old); + 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(long key, E value) { + E old = mStorage.get(key); + mStorage.put(key, value); + unregisterChildIf(old); + registerChild(value); + onChanged(); + } + + /** + * Returns the number of key-value mappings that this LongSparseArray + * currently stores. + */ + public int size() { + return mStorage.size(); + } + + /** + * Given an index in the range 0...size()-1, returns + * the key from the indexth key-value mapping that this + * LongSparseArray stores. + * + *

The keys corresponding to indices in ascending order are guaranteed to + * be in ascending order, e.g., keyAt(0) will return the + * smallest key and keyAt(size()-1) will return the largest + * key.

+ * + *

For indices outside of the range 0...size()-1, an + * {@link ArrayIndexOutOfBoundsException} is thrown.

+ */ + public long keyAt(int index) { + return mStorage.keyAt(index); + } + + /** + * Given an index in the range 0...size()-1, returns + * the value from the indexth key-value mapping that this + * LongSparseArray stores. + * + *

The values corresponding to indices in ascending order are guaranteed + * to be associated with keys in ascending order, e.g., + * valueAt(0) will return the value associated with the + * smallest key and valueAt(size()-1) will return the value + * associated with the largest key.

+ * + *

For indices outside of the range 0...size()-1, an + * {@link ArrayIndexOutOfBoundsException} is thrown.

+ */ + public E valueAt(int index) { + return mStorage.valueAt(index); + } + + /** + * Given an index in the range 0...size()-1, sets a new + * value for the indexth key-value mapping that this + * LongSparseArray stores. + * + *

For indices outside of the range 0...size()-1, an + * {@link ArrayIndexOutOfBoundsException} is thrown.

+ */ + public void setValueAt(int index, E value) { + E old = mStorage.valueAt(index); + mStorage.setValueAt(index, value); + 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(long 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(E value) { + return mStorage.indexOfValue(value); + } + + /** + * 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. + *

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 LongSparseArray. + */ + public void clear() { + 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(long key, E value) { + mStorage.append(key, value); + registerChild(value); + onChanged(); + } + + /** + * {@inheritDoc} + * + *

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(); + } + + /** + * 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 . + */ + public WatchedLongSparseArray snapshot() { + WatchedLongSparseArray l = new WatchedLongSparseArray<>(size()); + snapshot(l, this); + return l; + } + + /** + * Make a snapshot of the argument. Note that is immutable when the + * method returns. must be empty when the function is called. + * @param r The source array, which is copied into + */ + public void snapshot(@NonNull WatchedLongSparseArray 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 WatchedLongSparseArray dst, + @NonNull WatchedLongSparseArray 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++) { + final E val = Snapshots.maybeSnapshot(src.valueAt(i)); + final long key = src.keyAt(i); + dst.put(key, val); + } + dst.seal(); + } + +} 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 b40d59c14b8de..57e95d7145930 100644 --- a/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java +++ b/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.fail; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.LongSparseArray; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.util.SparseIntArray; @@ -618,6 +619,129 @@ public class WatcherTest { } } + @Test + public void testWatchedLongSparseArray() { + final String name = "WatchedLongSparseArray"; + WatchableTester tester; + + // Create a few leaves + Leaf leafA = new Leaf(); + Leaf leafB = new Leaf(); + Leaf leafC = new Leaf(); + Leaf leafD = new Leaf(); + + // Test WatchedLongSparseArray + WatchedLongSparseArray array = new WatchedLongSparseArray<>(); + array.put(INDEX_A, leafA); + array.put(INDEX_B, 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(INDEX_B); + tester.verify(3, "Removed b"); + leafB.tick(); + tester.verify(3, "Updates with b not watched"); + array.put(INDEX_B, leafB); + array.put(INDEX_C, leafB); + tester.verify(5, "Added b twice"); + leafB.tick(); + tester.verify(6, "Changed b - single notification"); + array.remove(INDEX_C); + tester.verify(7, "Removed first b"); + leafB.tick(); + tester.verify(8, "Changed b - single notification"); + array.remove(INDEX_B); + 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.put(INDEX_A, leafA); + array.put(INDEX_B, leafB); + array.put(INDEX_C, leafC); + tester.verify(13, "Added c"); + leafC.tick(); + tester.verify(14, "Ticked c"); + array.setValueAt(array.indexOfKey(INDEX_C), leafD); + tester.verify(15, "Replaced c with d"); + leafC.tick(); + tester.verify(15, "Ticked c (c not registered)"); + leafD.tick(); + tester.verify(16, "Ticked d and c (c not registered)"); + array.append(INDEX_D, leafC); + tester.verify(17, "Append c"); + leafC.tick(); + leafD.tick(); + tester.verify(19, "Ticked d and c"); + assertEquals("Verify four elements", 4, array.size()); + // Figure out which elements are at which indices. + Leaf[] x = new Leaf[4]; + for (int i = 0; i < 4; i++) { + x[i] = array.valueAt(i); + } + array.removeAt(1); + tester.verify(20, "Removed one element"); + x[1].tick(); + tester.verify(20, "Ticked one removed element"); + x[2].tick(); + tester.verify(21, "Ticked one remaining element"); + + // Snapshot + { + final WatchedLongSparseArray arraySnap = array.snapshot(); + tester.verify(21, "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)); + } + assertTrue(name + " element copy", + array.valueAt(i).equals(arraySnap.valueAt(i))); + } + leafD.tick(); + tester.verify(22, "Tick after snapshot"); + // Verify that the array snapshot is sealed + verifySealed(name, ()->arraySnap.put(INDEX_A, leafB)); + assertTrue(!array.isSealed()); + assertTrue(arraySnap.isSealed()); + } + // Recreate the snapshot since the test corrupted it. + { + final WatchedLongSparseArray arraySnap = array.snapshot(); + // Verify that elements are also snapshots + final Leaf arraySnapElement = arraySnap.valueAt(0); + verifySealed("ArraySnapshotElement", ()->arraySnapElement.tick()); + } + // Verify copy-in/out + { + final String msg = name + " copy-in/out"; + LongSparseArray base = new LongSparseArray<>(); + array.copyTo(base); + WatchedLongSparseArray copy = new WatchedLongSparseArray<>(); + 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 long key = array.keyAt(i); + assertTrue(msg, array.get(i) == copy.get(i)); + } + } + } + @Test public void testWatchedSparseBooleanArray() { final String name = "WatchedSparseBooleanArray"; @@ -733,4 +857,43 @@ public class WatcherTest { } } } + + @Test + public void testNestedArrays() { + final String name = "NestedArrays"; + WatchableTester tester; + + // Create a few leaves + Leaf leafA = new Leaf(); + Leaf leafB = new Leaf(); + Leaf leafC = new Leaf(); + Leaf leafD = new Leaf(); + + // Test nested arrays. + WatchedLongSparseArray lsaA = new WatchedLongSparseArray<>(); + lsaA.put(2, leafA); + WatchedLongSparseArray lsaB = new WatchedLongSparseArray<>(); + lsaB.put(4, leafB); + WatchedLongSparseArray lsaC = new WatchedLongSparseArray<>(); + lsaC.put(6, leafC); + + WatchedArrayMap> array = + new WatchedArrayMap<>(); + array.put("A", lsaA); + array.put("B", lsaB); + + // Test WatchedSparseIntArray + tester = new WatchableTester(array, name); + tester.verify(0, "Initial array - no registration"); + tester.register(); + tester.verify(0, "Initial array - post registration"); + leafA.tick(); + tester.verify(1, "tick grand-leaf"); + lsaA.put(2, leafD); + tester.verify(2, "replace leafA"); + leafA.tick(); + tester.verify(2, "tick unregistered leafA"); + leafD.tick(); + tester.verify(3, "tick leafD"); + } }