diff --git a/services/core/java/com/android/server/pm/AppsFilter.java b/services/core/java/com/android/server/pm/AppsFilter.java index 7b1fa1496bc4d..06ff69176bb72 100644 --- a/services/core/java/com/android/server/pm/AppsFilter.java +++ b/services/core/java/com/android/server/pm/AppsFilter.java @@ -58,10 +58,12 @@ import com.android.server.compat.CompatChange; import com.android.server.om.OverlayReferenceMapper; import com.android.server.pm.parsing.pkg.AndroidPackage; import com.android.server.utils.Snappable; +import com.android.server.utils.SnapshotCache; import com.android.server.utils.Snapshots; import com.android.server.utils.Watchable; import com.android.server.utils.WatchableImpl; import com.android.server.utils.WatchedArrayMap; +import com.android.server.utils.WatchedSparseBooleanMatrix; import com.android.server.utils.Watcher; import java.io.PrintWriter; @@ -158,12 +160,21 @@ public class AppsFilter implements Watchable, Snappable { * initial scam and is null until {@link #onSystemReady()} is called. */ @GuardedBy("mCacheLock") - private volatile SparseArray mShouldFilterCache; + private volatile WatchedSparseBooleanMatrix mShouldFilterCache; /** * A cached snapshot. */ - private volatile AppsFilter mSnapshot = null; + private final SnapshotCache mSnapshot; + + private SnapshotCache makeCache() { + return new SnapshotCache(this, this) { + @Override + public AppsFilter createSnapshot() { + AppsFilter s = new AppsFilter(mSource); + return s; + }}; + } /** * Watchable machinery @@ -211,7 +222,6 @@ public class AppsFilter implements Watchable, Snappable { */ @Override public void dispatchChange(@Nullable Watchable what) { - mSnapshot = null; mWatchable.dispatchChange(what); } @@ -236,6 +246,7 @@ public class AppsFilter implements Watchable, Snappable { overlayProvider); mStateProvider = stateProvider; mBackgroundExecutor = backgroundExecutor; + mSnapshot = makeCache(); } /** @@ -258,8 +269,14 @@ public class AppsFilter implements Watchable, Snappable { mSystemSigningDetails = orig.mSystemSigningDetails; mProtectedBroadcasts = orig.mProtectedBroadcasts; mShouldFilterCache = orig.mShouldFilterCache; + if (mShouldFilterCache != null) { + synchronized (orig.mCacheLock) { + mShouldFilterCache = mShouldFilterCache.snapshot(); + } + } mBackgroundExecutor = null; + mSnapshot = new SnapshotCache.Sealed<>(); } /** @@ -268,13 +285,7 @@ public class AppsFilter implements Watchable, Snappable { * condition causes the cached snapshot to be cleared asynchronously to this method. */ public AppsFilter snapshot() { - AppsFilter s = mSnapshot; - if (s == null) { - s = new AppsFilter(this); - s.mWatchable.seal(); - mSnapshot = s; - } - return s; + return mSnapshot.snapshot(); } /** @@ -636,12 +647,7 @@ public class AppsFilter implements Watchable, Snappable { if (mShouldFilterCache != null) { // update the cache in a one-off manner since we've got all the information we // need. - SparseBooleanArray visibleUids = mShouldFilterCache.get(recipientUid); - if (visibleUids == null) { - visibleUids = new SparseBooleanArray(); - mShouldFilterCache.put(recipientUid, visibleUids); - } - visibleUids.put(visibleUid, false); + mShouldFilterCache.put(recipientUid, visibleUid, false); } } if (changed) { @@ -813,23 +819,21 @@ public class AppsFilter implements Watchable, Snappable { if (mShouldFilterCache == null) { return; } - for (int i = mShouldFilterCache.size() - 1; i >= 0; i--) { + for (int i = 0; i < mShouldFilterCache.size(); i++) { if (UserHandle.getAppId(mShouldFilterCache.keyAt(i)) == appId) { mShouldFilterCache.removeAt(i); - continue; - } - SparseBooleanArray targetSparseArray = mShouldFilterCache.valueAt(i); - for (int j = targetSparseArray.size() - 1; j >= 0; j--) { - if (UserHandle.getAppId(targetSparseArray.keyAt(j)) == appId) { - targetSparseArray.removeAt(j); - } + // The key was deleted so the list of keys has shifted left. That means i + // is now pointing at the next key to be examined. The decrement here and + // the loop increment together mean that i will be unchanged in the need + // iteration and will correctly point to the next key to be examined. + i--; } } } private void updateEntireShouldFilterCache() { mStateProvider.runWithState((settings, users) -> { - SparseArray cache = + WatchedSparseBooleanMatrix cache = updateEntireShouldFilterCacheInner(settings, users); synchronized (mCacheLock) { mShouldFilterCache = cache; @@ -837,10 +841,10 @@ public class AppsFilter implements Watchable, Snappable { }); } - private SparseArray updateEntireShouldFilterCacheInner( + private WatchedSparseBooleanMatrix updateEntireShouldFilterCacheInner( ArrayMap settings, UserInfo[] users) { - SparseArray cache = - new SparseArray<>(users.length * settings.size()); + WatchedSparseBooleanMatrix cache = + new WatchedSparseBooleanMatrix(users.length * settings.size()); for (int i = settings.size() - 1; i >= 0; i--) { updateShouldFilterCacheForPackage(cache, null /*skipPackage*/, settings.valueAt(i), settings, users, i); @@ -864,7 +868,7 @@ public class AppsFilter implements Watchable, Snappable { packagesCache.put(settings.keyAt(i), pkg); } }); - SparseArray cache = + WatchedSparseBooleanMatrix cache = updateEntireShouldFilterCacheInner(settingsCopy, usersRef[0]); boolean[] changed = new boolean[1]; // We have a cache, let's make sure the world hasn't changed out from under us. @@ -916,7 +920,7 @@ public class AppsFilter implements Watchable, Snappable { } } - private void updateShouldFilterCacheForPackage(SparseArray cache, + private void updateShouldFilterCacheForPackage(WatchedSparseBooleanMatrix cache, @Nullable String skipPackageName, PackageSetting subjectSetting, ArrayMap allSettings, UserInfo[] allUsers, int maxIndex) { for (int i = Math.min(maxIndex, allSettings.size() - 1); i >= 0; i--) { @@ -935,17 +939,11 @@ public class AppsFilter implements Watchable, Snappable { for (int ou = 0; ou < userCount; ou++) { int otherUser = allUsers[ou].id; int subjectUid = UserHandle.getUid(subjectUser, subjectSetting.appId); - if (!cache.contains(subjectUid)) { - cache.put(subjectUid, new SparseBooleanArray(appxUidCount)); - } int otherUid = UserHandle.getUid(otherUser, otherSetting.appId); - if (!cache.contains(otherUid)) { - cache.put(otherUid, new SparseBooleanArray(appxUidCount)); - } - cache.get(subjectUid).put(otherUid, + cache.put(subjectUid, otherUid, shouldFilterApplicationInternal( subjectUid, subjectSetting, otherSetting, otherUser)); - cache.get(otherUid).put(subjectUid, + cache.put(otherUid, subjectUid, shouldFilterApplicationInternal( otherUid, otherSetting, subjectSetting, subjectUser)); } @@ -1198,22 +1196,20 @@ public class AppsFilter implements Watchable, Snappable { } synchronized (mCacheLock) { if (mShouldFilterCache != null) { // use cache - SparseBooleanArray shouldFilterTargets = mShouldFilterCache.get(callingUid); - final int targetUid = UserHandle.getUid(userId, targetPkgSetting.appId); - if (shouldFilterTargets == null) { + final int callingIndex = mShouldFilterCache.indexOfKey(callingUid); + if (callingIndex < 0) { Slog.wtf(TAG, "Encountered calling uid with no cached rules: " + callingUid); return true; } - int indexOfTargetUid = shouldFilterTargets.indexOfKey(targetUid); - if (indexOfTargetUid < 0) { + final int targetUid = UserHandle.getUid(userId, targetPkgSetting.appId); + final int targetIndex = mShouldFilterCache.indexOfKey(targetUid); + if (targetIndex < 0) { Slog.w(TAG, "Encountered calling -> target with no cached rules: " + callingUid + " -> " + targetUid); return true; } - if (!shouldFilterTargets.valueAt(indexOfTargetUid)) { - return false; - } + return mShouldFilterCache.valueAt(callingIndex, targetIndex); } else { if (!shouldFilterApplicationInternal( callingUid, callingSetting, targetPkgSetting, userId)) { diff --git a/services/core/java/com/android/server/utils/WatchedSparseBooleanMatrix.java b/services/core/java/com/android/server/utils/WatchedSparseBooleanMatrix.java new file mode 100644 index 0000000000000..42a2f81bf9a70 --- /dev/null +++ b/services/core/java/com/android/server/utils/WatchedSparseBooleanMatrix.java @@ -0,0 +1,562 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.utils; + +import android.annotation.Nullable; +import android.annotation.Size; + +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; + +import java.util.Arrays; + +/** + * A {@link WatchedSparseBooleanMatrix} is an compact NxN array of booleans. The rows and + * columns of the array are indexed by integers, which need not be contiguous. The matrix + * is square and the row and column indices are identical. This matrix is intended to be + * very memory efficient. + * + * The matrix contains a map from indices to columns: this map requires 2*N integers. The + * boolean array is bit-packed and requires N*N/8 bytes. The memory required for an + * order-N matrix is therefore 2*N*4 + N*N bytes. + * + * See {@link SparseBooleanArray} for a discussion of sparse arrays. + */ +public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappable { + + /** + * The matrix is implemented through four arrays. The matrix of booleans is stored in + * a one-dimensional {@code mValues} array. {@code mValues} is always of size + * {@code mOrder * mOrder}. Elements of {@code mValues} are addressed with + * arithmetic: the offset of the element {@code {row, col}} is at + * {@code row * mOrder + col}. The term "storage index" applies to {@code mValues}. + * A storage index designates a row (column) in the underlying storage. This is not + * the same as the row seen by client code. + * + * Client code addresses the matrix through indices. These are integers that need not + * be contiguous. Client indices are mapped to storage indices through two linear + * integer arrays. {@code mKeys} is a sorted list of client indices. + * {@code mIndices} is a parallel array that contains storage indices. The storage + * index of a client index {@code k} is {@code mIndices[i]}, where + * {@code mKeys[i] == k}. + * + * A final array, {@code mInUse} records if storage indices are free or in use. This + * array is of size {@code mOrder}. A client index is deleted by removing it from + * {@code mKeys} and {@code mIndices} and then setting the original storage index + * false in {@code mInUse}. + * + * Some notes: + *
    + *
  • The matrix never shrinks. + *
  • Equality is a very, very expesive operation. + *
+ */ + + /** + * mOrder is always a multiple of this value. A minimal matrix therefore holds 2^12 + * values and requires 1024 bytes. + */ + private static final int STEP = 64; + + /** + * The order of the matrix storage, including any padding. The matrix is always + * square. mOrder is always greater than or equal to mSize. + */ + private int mOrder; + + /** + * The number of client keys. This is always less than or equal to mOrder. It is the + * order of the matrix as seen by the client. + */ + private int mSize; + + /** + * The in-use list. + */ + private boolean[] mInUse; + + /** + * The array of client keys (indices), in sorted order. + */ + private int[] mKeys; + + /** + * The mapping from a client key to an storage index. If client key K is at index N + * in mKeys, then the storage index for K is at mMap[N]. + */ + private int[] mMap; + + /** + * The boolean array. This array is always {@code mOrder x mOrder} in size. + */ + private boolean[] mValues; + + /** + * 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); + } + + /** + * Creates a new WatchedSparseBooleanMatrix containing no mappings. + */ + public WatchedSparseBooleanMatrix() { + this(STEP); + } + + /** + * Creates a new SparseBooleanMatrix containing no mappings that will not require any + * additional memory allocation to store the specified number of mappings. The + * capacity is always rounded up to a non-zero multiple of STEP. + */ + public WatchedSparseBooleanMatrix(int initialCapacity) { + mOrder = initialCapacity; + if (mOrder < STEP) { + mOrder = STEP; + } + if (mOrder % STEP != 0) { + mOrder = ((initialCapacity / STEP) + 1) * STEP; + } + if (mOrder < STEP || (mOrder % STEP != 0)) { + throw new RuntimeException("mOrder is " + mOrder + " initCap is " + initialCapacity); + } + + mInUse = new boolean[mOrder]; + mKeys = ArrayUtils.newUnpaddedIntArray(mOrder); + mMap = ArrayUtils.newUnpaddedIntArray(mOrder); + mValues = new boolean[mOrder * mOrder]; + mSize = 0; + } + + /** + * A copy constructor that can be used for snapshotting. + */ + private WatchedSparseBooleanMatrix(WatchedSparseBooleanMatrix r) { + mOrder = r.mOrder; + mSize = r.mSize; + mKeys = r.mKeys.clone(); + mMap = r.mMap.clone(); + mInUse = r.mInUse.clone(); + mValues = r.mValues.clone(); + } + + /** + * Return a copy of this object. + */ + public WatchedSparseBooleanMatrix snapshot() { + return new WatchedSparseBooleanMatrix(this); + } + + /** + * Gets the boolean mapped from the specified key, or false + * if no such mapping has been made. + */ + public boolean get(int row, int col) { + return get(row, col, false); + } + + /** + * Gets the boolean mapped from the specified key, or the specified value + * if no such mapping has been made. + */ + public boolean get(int row, int col, boolean valueIfKeyNotFound) { + int r = indexOfKey(row, false); + int c = indexOfKey(col, false); + if (r >= 0 && c >= 0) { + return valueAt(r, c); + } else { + return valueIfKeyNotFound; + } + } + + /** + * Adds a mapping from the specified keys to the specified value, replacing the + * previous mapping from the specified keys if there was one. + */ + public void put(int row, int col, boolean value) { + int r = indexOfKey(row); + int c = indexOfKey(col); + if (r < 0 || c < 0) { + // One or both of the keys has not be installed yet. Install them now. + // Installing either key may shift the other key. The safest course is to + // install the keys that are not present and then recompute both indices. + if (r < 0) { + r = indexOfKey(row, true); + } + if (c < 0) { + c = indexOfKey(col, true); + } + r = indexOfKey(row); + c = indexOfKey(col); + } + if (r >= 0 && c >= 0) { + setValueAt(r, c, value); + onChanged(); + } else { + throw new RuntimeException("matrix overflow"); + } + } + + /** + * Removes the mapping from the specified key, if there was any. Note that deletion + * applies to a single index, not to an element. The matrix never shrinks but the + * space will be reused the next time an index is added. + */ + public void deleteKey(int key) { + int i = indexOfKey(key, false); + if (i >= 0) { + removeAt(i); + } + } + + /** + * Removes the mapping at the specified index. The matrix does not shrink. This + * throws ArrayIndexOutOfBounds if the index out outside the range {@code 0..size()-1}. + */ + public void removeAt(int index) { + validateIndex(index); + mInUse[mMap[index]] = false; + System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1)); + System.arraycopy(mMap, index + 1, mMap, index, mSize - (index + 1)); + mSize--; + onChanged(); + } + + /** + * Returns the number of key-value mappings that this WatchedSparseBooleanMatrix + * currently stores. + */ + public int size() { + return mSize; + } + + /** + * Removes all key-value mappings from this WatchedSparseBooleanMatrix. + */ + public void clear() { + mSize = 0; + Arrays.fill(mInUse, false); + onChanged(); + } + + /** + * Given an index in the range 0...size()-1, returns the key from the + * indexth key-value mapping that this WatchedSparseBooleanMatrix 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.

+ * + *

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

+ */ + public int keyAt(int index) { + validateIndex(index); + return mKeys[index]; + } + + /** + * Given a row and column, each in the range 0...size()-1, returns the + * value from the indexth key-value mapping that this WatchedSparseBooleanMatrix + * stores. + */ + public boolean valueAt(int rowIndex, int colIndex) { + validateIndex(rowIndex, colIndex); + int r = mMap[rowIndex]; + int c = mMap[colIndex]; + int element = r * mOrder + c; + return mValues[element]; + } + + /** + * Directly set the value at a particular index. + */ + public void setValueAt(int rowIndex, int colIndex, boolean value) { + validateIndex(rowIndex, colIndex); + int r = mMap[rowIndex]; + int c = mMap[colIndex]; + int element = r * mOrder + c; + mValues[element] = 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 binarySearch(mKeys, mSize, key); + } + + /** + * Return true if the matrix knows the user index. + */ + public boolean contains(int key) { + return indexOfKey(key) >= 0; + } + + /** + * Fetch the index of a key. If the key does not exist and grow is true, then add the + * key. If the does not exist and grow is false, return -1. + */ + private int indexOfKey(int key, boolean grow) { + int i = binarySearch(mKeys, mSize, key); + if (i < 0 && grow) { + i = ~i; + if (mSize >= mOrder) { + // Preemptively grow the matrix, which also grows the free list. + growMatrix(); + } + int newIndex = nextFree(); + mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key); + mMap = GrowingArrayUtils.insert(mMap, mSize, i, newIndex); + mSize++; + // Initialize the row and column corresponding to the new index. + for (int n = 0; n < mSize; n++) { + mValues[n * mOrder + newIndex] = false; + mValues[newIndex * mOrder + n] = false; + } + onChanged(); + } + return i; + } + + /** + * Validate the index. This can throw. + */ + private void validateIndex(int index) { + if (index >= mSize) { + // The array might be slightly bigger than mSize, in which case, indexing won't fail. + throw new ArrayIndexOutOfBoundsException(index); + } + } + + /** + * Validate two indices. + */ + private void validateIndex(int row, int col) { + validateIndex(row); + validateIndex(col); + } + + /** + * Find an unused storage index, mark it in-use, and return it. + */ + private int nextFree() { + for (int i = 0; i < mInUse.length; i++) { + if (!mInUse[i]) { + mInUse[i] = true; + return i; + } + } + throw new RuntimeException(); + } + + /** + * Expand the 2D array. This also extends the free list. + */ + private void growMatrix() { + int newOrder = mOrder + STEP; + + boolean[] newInuse = Arrays.copyOf(mInUse, newOrder); + + boolean[] newValues = new boolean[newOrder * newOrder]; + for (int i = 0; i < mOrder; i++) { + int row = mOrder * i; + int newRow = newOrder * i; + for (int j = 0; j < mOrder; j++) { + int index = row + j; + int newIndex = newRow + j; + newValues[newIndex] = mValues[index]; + } + } + + mInUse = newInuse; + mValues = newValues; + mOrder = newOrder; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + int hashCode = mSize; + for (int i = 0; i < mSize; i++) { + hashCode = 31 * hashCode + mKeys[i]; + hashCode = 31 * hashCode + mMap[i]; + } + for (int i = 0; i < mSize; i++) { + int row = mMap[i] * mOrder; + for (int j = 0; j < mSize; j++) { + int element = mMap[j] + row; + hashCode = 31 * hashCode + (mValues[element] ? 1 : 0); + } + } + return hashCode; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(@Nullable Object that) { + if (this == that) { + return true; + } + + if (!(that instanceof WatchedSparseBooleanMatrix)) { + return false; + } + + WatchedSparseBooleanMatrix other = (WatchedSparseBooleanMatrix) that; + if (mSize != other.mSize) { + return false; + } + + for (int i = 0; i < mSize; i++) { + if (mKeys[i] != other.mKeys[i]) { + return false; + } + if (mMap[i] != other.mMap[i]) { + return false; + } + } + for (int i = 0; i < mSize; i++) { + int row = mMap[i] * mOrder; + for (int j = 0; j < mSize; j++) { + int element = mMap[j] + row; + if (mValues[element] != other.mValues[element]) { + return false; + } + } + } + return true; + } + + /** + * Return the matrix meta information. This is always three strings long. + */ + private @Size(3) String[] matrixToStringMeta() { + String[] result = new String[3]; + + StringBuilder k = new StringBuilder(); + for (int i = 0; i < mSize; i++) { + k.append(mKeys[i]); + if (i < mSize - 1) { + k.append(" "); + } + } + result[0] = k.substring(0); + + StringBuilder m = new StringBuilder(); + for (int i = 0; i < mSize; i++) { + m.append(mMap[i]); + if (i < mSize - 1) { + m.append(" "); + } + } + result[1] = m.substring(0); + + StringBuilder u = new StringBuilder(); + for (int i = 0; i < mOrder; i++) { + u.append(mInUse[i] ? "1" : "0"); + } + result[2] = u.substring(0); + return result; + } + + /** + * Return the matrix as an array of strings. There is one string per row. Each + * string has a '1' or a '0' in the proper column. + */ + private String[] matrixToStringRaw() { + String[] result = new String[mOrder]; + for (int i = 0; i < mOrder; i++) { + int row = i * mOrder; + StringBuilder line = new StringBuilder(mOrder); + for (int j = 0; j < mOrder; j++) { + int element = row + j; + line.append(mValues[element] ? "1" : "0"); + } + result[i] = line.substring(0); + } + return result; + } + + private String[] matrixToStringCooked() { + String[] result = new String[mSize]; + for (int i = 0; i < mSize; i++) { + int row = mMap[i] * mOrder; + StringBuilder line = new StringBuilder(mSize); + for (int j = 0; j < mSize; j++) { + int element = row + mMap[j]; + line.append(mValues[element] ? "1" : "0"); + } + result[i] = line.substring(0); + } + return result; + } + + public String[] matrixToString(boolean raw) { + String[] meta = matrixToStringMeta(); + String[] data; + if (raw) { + data = matrixToStringRaw(); + } else { + data = matrixToStringCooked(); + } + String[] result = new String[meta.length + data.length]; + System.arraycopy(meta, 0, result, 0, meta.length); + System.arraycopy(data, 0, result, meta.length, data.length); + return result; + } + + /** + * {@inheritDoc} + * + *

This implementation creates a string that describes the size of the array. A + * string with all the values could easily exceed 1Mb. + */ + @Override + public String toString() { + return "{" + mSize + "x" + mSize + "}"; + } + + // Copied from android.util.ContainerHelpers, which is not visible outside the + // android.util package. + private static int binarySearch(int[] array, int size, int value) { + int lo = 0; + int hi = size - 1; + + while (lo <= hi) { + final int mid = (lo + hi) >>> 1; + final int midVal = array[mid]; + + if (midVal < value) { + lo = mid + 1; + } else if (midVal > value) { + hi = mid - 1; + } else { + return mid; // value found + } + } + return ~lo; // value not present + } +} 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 9679e58c4e7d2..5db9492c35c5f 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.Log; import android.util.LongSparseArray; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -34,9 +35,12 @@ import org.junit.Before; import org.junit.Test; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Random; /** - * Test class for {@link Watcher}, {@link Watchable}, {@link WatchableImpl}, + * Test class for various utility classes that support the Watchable or Snappable + * features. This covers {@link Watcher}, {@link Watchable}, {@link WatchableImpl}, * {@link WatchedArrayMap}, {@link WatchedSparseArray}, and * {@link WatchedSparseBooleanArray}. * @@ -858,6 +862,93 @@ public class WatcherTest { } } + private static class IndexGenerator { + private final int mSeed; + private final Random mRandom; + public IndexGenerator(int seed) { + mSeed = seed; + mRandom = new Random(mSeed); + } + public int index() { + return mRandom.nextInt(50000); + } + public void reset() { + mRandom.setSeed(mSeed); + } + } + + // Return a value based on the row and column. The algorithm tries to avoid simple + // patterns like checkerboard. + private final boolean cellValue(int row, int col) { + return (((row * 4 + col) % 3)& 1) == 1; + } + + // This is an inefficient way to know if a value appears in an array. + private final boolean contains(int[] s, int length, int k) { + for (int i = 0; i < length; i++) { + if (s[i] == k) { + return true; + } + } + return false; + } + + private void matrixTest(WatchedSparseBooleanMatrix matrix, int size, IndexGenerator indexer) { + indexer.reset(); + int[] indexes = new int[size]; + for (int i = 0; i < size; i++) { + int key = indexer.index(); + // Ensure the list of indices are unique. + while (contains(indexes, i, key)) { + key = indexer.index(); + } + indexes[i] = key; + } + // Set values in the matrix. + for (int i = 0; i < size; i++) { + int row = indexes[i]; + for (int j = 0; j < size; j++) { + int col = indexes[j]; + boolean want = cellValue(i, j); + matrix.put(row, col, want); + } + } + + assertEquals(matrix.size(), size); + + // Read back and verify + for (int i = 0; i < matrix.size(); i++) { + int row = indexes[i]; + for (int j = 0; j < matrix.size(); j++) { + int col = indexes[j]; + boolean want = cellValue(i, j); + boolean actual = matrix.get(row, col); + String msg = String.format("matrix(%d:%d, %d:%d) == %s, expected %s", + i, row, j, col, actual, want); + assertEquals(msg, actual, want); + } + } + + // Test the keyAt/indexOfKey methods + for (int i = 0; i < matrix.size(); i++) { + int key = indexes[i]; + assertEquals(matrix.keyAt(matrix.indexOfKey(key)), key); + } + } + + @Test + public void testWatchedSparseBooleanMatrix() { + final String name = "WatchedSparseBooleanMatrix"; + + // The first part of this method tests the core matrix functionality. The second + // part tests the watchable behavior. The third part tests the snappable + // behavior. + IndexGenerator indexer = new IndexGenerator(3); + matrixTest(new WatchedSparseBooleanMatrix(), 10, indexer); + matrixTest(new WatchedSparseBooleanMatrix(1000), 500, indexer); + matrixTest(new WatchedSparseBooleanMatrix(1000), 2000, indexer); + } + @Test public void testNestedArrays() { final String name = "NestedArrays";