Merge "A space-efficient 2D matrix" into sc-dev am: 1b1bbfc9f7
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/14671775 Change-Id: I8bfad2393178e680e550b7b4570111aa5cef3fab
This commit is contained in:
@@ -58,10 +58,12 @@ import com.android.server.compat.CompatChange;
|
|||||||
import com.android.server.om.OverlayReferenceMapper;
|
import com.android.server.om.OverlayReferenceMapper;
|
||||||
import com.android.server.pm.parsing.pkg.AndroidPackage;
|
import com.android.server.pm.parsing.pkg.AndroidPackage;
|
||||||
import com.android.server.utils.Snappable;
|
import com.android.server.utils.Snappable;
|
||||||
|
import com.android.server.utils.SnapshotCache;
|
||||||
import com.android.server.utils.Snapshots;
|
import com.android.server.utils.Snapshots;
|
||||||
import com.android.server.utils.Watchable;
|
import com.android.server.utils.Watchable;
|
||||||
import com.android.server.utils.WatchableImpl;
|
import com.android.server.utils.WatchableImpl;
|
||||||
import com.android.server.utils.WatchedArrayMap;
|
import com.android.server.utils.WatchedArrayMap;
|
||||||
|
import com.android.server.utils.WatchedSparseBooleanMatrix;
|
||||||
import com.android.server.utils.Watcher;
|
import com.android.server.utils.Watcher;
|
||||||
|
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
@@ -158,12 +160,21 @@ public class AppsFilter implements Watchable, Snappable {
|
|||||||
* initial scam and is null until {@link #onSystemReady()} is called.
|
* initial scam and is null until {@link #onSystemReady()} is called.
|
||||||
*/
|
*/
|
||||||
@GuardedBy("mCacheLock")
|
@GuardedBy("mCacheLock")
|
||||||
private volatile SparseArray<SparseBooleanArray> mShouldFilterCache;
|
private volatile WatchedSparseBooleanMatrix mShouldFilterCache;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A cached snapshot.
|
* A cached snapshot.
|
||||||
*/
|
*/
|
||||||
private volatile AppsFilter mSnapshot = null;
|
private final SnapshotCache<AppsFilter> mSnapshot;
|
||||||
|
|
||||||
|
private SnapshotCache<AppsFilter> makeCache() {
|
||||||
|
return new SnapshotCache<AppsFilter>(this, this) {
|
||||||
|
@Override
|
||||||
|
public AppsFilter createSnapshot() {
|
||||||
|
AppsFilter s = new AppsFilter(mSource);
|
||||||
|
return s;
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Watchable machinery
|
* Watchable machinery
|
||||||
@@ -211,7 +222,6 @@ public class AppsFilter implements Watchable, Snappable {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void dispatchChange(@Nullable Watchable what) {
|
public void dispatchChange(@Nullable Watchable what) {
|
||||||
mSnapshot = null;
|
|
||||||
mWatchable.dispatchChange(what);
|
mWatchable.dispatchChange(what);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +246,7 @@ public class AppsFilter implements Watchable, Snappable {
|
|||||||
overlayProvider);
|
overlayProvider);
|
||||||
mStateProvider = stateProvider;
|
mStateProvider = stateProvider;
|
||||||
mBackgroundExecutor = backgroundExecutor;
|
mBackgroundExecutor = backgroundExecutor;
|
||||||
|
mSnapshot = makeCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -258,8 +269,14 @@ public class AppsFilter implements Watchable, Snappable {
|
|||||||
mSystemSigningDetails = orig.mSystemSigningDetails;
|
mSystemSigningDetails = orig.mSystemSigningDetails;
|
||||||
mProtectedBroadcasts = orig.mProtectedBroadcasts;
|
mProtectedBroadcasts = orig.mProtectedBroadcasts;
|
||||||
mShouldFilterCache = orig.mShouldFilterCache;
|
mShouldFilterCache = orig.mShouldFilterCache;
|
||||||
|
if (mShouldFilterCache != null) {
|
||||||
|
synchronized (orig.mCacheLock) {
|
||||||
|
mShouldFilterCache = mShouldFilterCache.snapshot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mBackgroundExecutor = null;
|
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.
|
* condition causes the cached snapshot to be cleared asynchronously to this method.
|
||||||
*/
|
*/
|
||||||
public AppsFilter snapshot() {
|
public AppsFilter snapshot() {
|
||||||
AppsFilter s = mSnapshot;
|
return mSnapshot.snapshot();
|
||||||
if (s == null) {
|
|
||||||
s = new AppsFilter(this);
|
|
||||||
s.mWatchable.seal();
|
|
||||||
mSnapshot = s;
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -636,12 +647,7 @@ public class AppsFilter implements Watchable, Snappable {
|
|||||||
if (mShouldFilterCache != null) {
|
if (mShouldFilterCache != null) {
|
||||||
// update the cache in a one-off manner since we've got all the information we
|
// update the cache in a one-off manner since we've got all the information we
|
||||||
// need.
|
// need.
|
||||||
SparseBooleanArray visibleUids = mShouldFilterCache.get(recipientUid);
|
mShouldFilterCache.put(recipientUid, visibleUid, false);
|
||||||
if (visibleUids == null) {
|
|
||||||
visibleUids = new SparseBooleanArray();
|
|
||||||
mShouldFilterCache.put(recipientUid, visibleUids);
|
|
||||||
}
|
|
||||||
visibleUids.put(visibleUid, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
@@ -813,23 +819,21 @@ public class AppsFilter implements Watchable, Snappable {
|
|||||||
if (mShouldFilterCache == null) {
|
if (mShouldFilterCache == null) {
|
||||||
return;
|
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) {
|
if (UserHandle.getAppId(mShouldFilterCache.keyAt(i)) == appId) {
|
||||||
mShouldFilterCache.removeAt(i);
|
mShouldFilterCache.removeAt(i);
|
||||||
continue;
|
// 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
|
||||||
SparseBooleanArray targetSparseArray = mShouldFilterCache.valueAt(i);
|
// the loop increment together mean that i will be unchanged in the need
|
||||||
for (int j = targetSparseArray.size() - 1; j >= 0; j--) {
|
// iteration and will correctly point to the next key to be examined.
|
||||||
if (UserHandle.getAppId(targetSparseArray.keyAt(j)) == appId) {
|
i--;
|
||||||
targetSparseArray.removeAt(j);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateEntireShouldFilterCache() {
|
private void updateEntireShouldFilterCache() {
|
||||||
mStateProvider.runWithState((settings, users) -> {
|
mStateProvider.runWithState((settings, users) -> {
|
||||||
SparseArray<SparseBooleanArray> cache =
|
WatchedSparseBooleanMatrix cache =
|
||||||
updateEntireShouldFilterCacheInner(settings, users);
|
updateEntireShouldFilterCacheInner(settings, users);
|
||||||
synchronized (mCacheLock) {
|
synchronized (mCacheLock) {
|
||||||
mShouldFilterCache = cache;
|
mShouldFilterCache = cache;
|
||||||
@@ -837,10 +841,10 @@ public class AppsFilter implements Watchable, Snappable {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private SparseArray<SparseBooleanArray> updateEntireShouldFilterCacheInner(
|
private WatchedSparseBooleanMatrix updateEntireShouldFilterCacheInner(
|
||||||
ArrayMap<String, PackageSetting> settings, UserInfo[] users) {
|
ArrayMap<String, PackageSetting> settings, UserInfo[] users) {
|
||||||
SparseArray<SparseBooleanArray> cache =
|
WatchedSparseBooleanMatrix cache =
|
||||||
new SparseArray<>(users.length * settings.size());
|
new WatchedSparseBooleanMatrix(users.length * settings.size());
|
||||||
for (int i = settings.size() - 1; i >= 0; i--) {
|
for (int i = settings.size() - 1; i >= 0; i--) {
|
||||||
updateShouldFilterCacheForPackage(cache,
|
updateShouldFilterCacheForPackage(cache,
|
||||||
null /*skipPackage*/, settings.valueAt(i), settings, users, i);
|
null /*skipPackage*/, settings.valueAt(i), settings, users, i);
|
||||||
@@ -864,7 +868,7 @@ public class AppsFilter implements Watchable, Snappable {
|
|||||||
packagesCache.put(settings.keyAt(i), pkg);
|
packagesCache.put(settings.keyAt(i), pkg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
SparseArray<SparseBooleanArray> cache =
|
WatchedSparseBooleanMatrix cache =
|
||||||
updateEntireShouldFilterCacheInner(settingsCopy, usersRef[0]);
|
updateEntireShouldFilterCacheInner(settingsCopy, usersRef[0]);
|
||||||
boolean[] changed = new boolean[1];
|
boolean[] changed = new boolean[1];
|
||||||
// We have a cache, let's make sure the world hasn't changed out from under us.
|
// 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<SparseBooleanArray> cache,
|
private void updateShouldFilterCacheForPackage(WatchedSparseBooleanMatrix cache,
|
||||||
@Nullable String skipPackageName, PackageSetting subjectSetting, ArrayMap<String,
|
@Nullable String skipPackageName, PackageSetting subjectSetting, ArrayMap<String,
|
||||||
PackageSetting> allSettings, UserInfo[] allUsers, int maxIndex) {
|
PackageSetting> allSettings, UserInfo[] allUsers, int maxIndex) {
|
||||||
for (int i = Math.min(maxIndex, allSettings.size() - 1); i >= 0; i--) {
|
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++) {
|
for (int ou = 0; ou < userCount; ou++) {
|
||||||
int otherUser = allUsers[ou].id;
|
int otherUser = allUsers[ou].id;
|
||||||
int subjectUid = UserHandle.getUid(subjectUser, subjectSetting.appId);
|
int subjectUid = UserHandle.getUid(subjectUser, subjectSetting.appId);
|
||||||
if (!cache.contains(subjectUid)) {
|
|
||||||
cache.put(subjectUid, new SparseBooleanArray(appxUidCount));
|
|
||||||
}
|
|
||||||
int otherUid = UserHandle.getUid(otherUser, otherSetting.appId);
|
int otherUid = UserHandle.getUid(otherUser, otherSetting.appId);
|
||||||
if (!cache.contains(otherUid)) {
|
cache.put(subjectUid, otherUid,
|
||||||
cache.put(otherUid, new SparseBooleanArray(appxUidCount));
|
|
||||||
}
|
|
||||||
cache.get(subjectUid).put(otherUid,
|
|
||||||
shouldFilterApplicationInternal(
|
shouldFilterApplicationInternal(
|
||||||
subjectUid, subjectSetting, otherSetting, otherUser));
|
subjectUid, subjectSetting, otherSetting, otherUser));
|
||||||
cache.get(otherUid).put(subjectUid,
|
cache.put(otherUid, subjectUid,
|
||||||
shouldFilterApplicationInternal(
|
shouldFilterApplicationInternal(
|
||||||
otherUid, otherSetting, subjectSetting, subjectUser));
|
otherUid, otherSetting, subjectSetting, subjectUser));
|
||||||
}
|
}
|
||||||
@@ -1198,22 +1196,20 @@ public class AppsFilter implements Watchable, Snappable {
|
|||||||
}
|
}
|
||||||
synchronized (mCacheLock) {
|
synchronized (mCacheLock) {
|
||||||
if (mShouldFilterCache != null) { // use cache
|
if (mShouldFilterCache != null) { // use cache
|
||||||
SparseBooleanArray shouldFilterTargets = mShouldFilterCache.get(callingUid);
|
final int callingIndex = mShouldFilterCache.indexOfKey(callingUid);
|
||||||
final int targetUid = UserHandle.getUid(userId, targetPkgSetting.appId);
|
if (callingIndex < 0) {
|
||||||
if (shouldFilterTargets == null) {
|
|
||||||
Slog.wtf(TAG, "Encountered calling uid with no cached rules: "
|
Slog.wtf(TAG, "Encountered calling uid with no cached rules: "
|
||||||
+ callingUid);
|
+ callingUid);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
int indexOfTargetUid = shouldFilterTargets.indexOfKey(targetUid);
|
final int targetUid = UserHandle.getUid(userId, targetPkgSetting.appId);
|
||||||
if (indexOfTargetUid < 0) {
|
final int targetIndex = mShouldFilterCache.indexOfKey(targetUid);
|
||||||
|
if (targetIndex < 0) {
|
||||||
Slog.w(TAG, "Encountered calling -> target with no cached rules: "
|
Slog.w(TAG, "Encountered calling -> target with no cached rules: "
|
||||||
+ callingUid + " -> " + targetUid);
|
+ callingUid + " -> " + targetUid);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!shouldFilterTargets.valueAt(indexOfTargetUid)) {
|
return mShouldFilterCache.valueAt(callingIndex, targetIndex);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (!shouldFilterApplicationInternal(
|
if (!shouldFilterApplicationInternal(
|
||||||
callingUid, callingSetting, targetPkgSetting, userId)) {
|
callingUid, callingSetting, targetPkgSetting, userId)) {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
* <ul>
|
||||||
|
* <li> The matrix never shrinks.
|
||||||
|
* <li> Equality is a very, very expesive operation.
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 <code>false</code>
|
||||||
|
* 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 <code>0...size()-1</code>, returns the key from the
|
||||||
|
* <code>index</code>th key-value mapping that this WatchedSparseBooleanMatrix 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>{@link ArrayIndexOutOfBoundsException} is thrown for indices outside of the
|
||||||
|
* range <code>0...size()-1</code></p>
|
||||||
|
*/
|
||||||
|
public int keyAt(int index) {
|
||||||
|
validateIndex(index);
|
||||||
|
return mKeys[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a row and column, each in the range <code>0...size()-1</code>, returns the
|
||||||
|
* value from the <code>index</code>th 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}
|
||||||
|
*
|
||||||
|
* <p>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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import static org.junit.Assert.fail;
|
|||||||
|
|
||||||
import android.util.ArrayMap;
|
import android.util.ArrayMap;
|
||||||
import android.util.ArraySet;
|
import android.util.ArraySet;
|
||||||
|
import android.util.Log;
|
||||||
import android.util.LongSparseArray;
|
import android.util.LongSparseArray;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.util.SparseBooleanArray;
|
import android.util.SparseBooleanArray;
|
||||||
@@ -34,9 +35,12 @@ import org.junit.Before;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
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 WatchedArrayMap}, {@link WatchedSparseArray}, and
|
||||||
* {@link WatchedSparseBooleanArray}.
|
* {@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
|
@Test
|
||||||
public void testNestedArrays() {
|
public void testNestedArrays() {
|
||||||
final String name = "NestedArrays";
|
final String name = "NestedArrays";
|
||||||
|
|||||||
Reference in New Issue
Block a user