From 7ec1df49d153ce73202ffb687e59cc04590c703e Mon Sep 17 00:00:00 2001 From: Lee Shombert Date: Fri, 21 May 2021 09:45:31 -0700 Subject: [PATCH] A space-efficient 2D matrix Bug: Bug: 188447813 WatchedSparseBooleanMatrix is a space-efficient NxN boolean array indexed by integers (the keys). The integer indices need not be contiguous. The indices apply identically to rows and columns, so there is only one mapping from index to internal row or column. Test: atest * CtsContentTestCases:IntentFilterTest * CtsDynamicMimeHostTestCases * CtsRoleTestCases * FrameworksServicesTests:UserSystemPackageInstallerTest * FrameworksServicesTests:PackageManagerSettingsTests * FrameworksServicesTests:PackageManagerServiceTest * FrameworksServicesTests:AppsFilterTest * FrameworksServicesTests:PackageInstallerSessionTest * FrameworksServicesTests:ScanTests * UserLifecycleTests#startUser * UserLifecycleTests#stopUser * UserLifecycleTests#switchUser * FrameworksServicesTests:WatcherTest * android.appsecurity.cts.EphemeralTest * android.appsecurity.cts.InstantAppUserTest Change-Id: Ideafb5bad6b68913eceb8b1bbd2fb78dc5e82644 --- .../com/android/server/pm/AppsFilter.java | 88 ++- .../utils/WatchedSparseBooleanMatrix.java | 562 ++++++++++++++++++ .../com/android/server/utils/WatcherTest.java | 93 ++- 3 files changed, 696 insertions(+), 47 deletions(-) create mode 100644 services/core/java/com/android/server/utils/WatchedSparseBooleanMatrix.java 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";