Create os/IpcDataCache

Bug: 219609105

Create os/IpcDataCache as a subclass of PropertyInvalidatedCache.  The
APIs in the new class are the SystemApi for mainline modules.

Note that IpcDataCache.invalidateCache(String) is a new system API,
relative to the list in PropertyInvalidateCache.  The new API is
static, which is the most common use case for invalidateCache calls.

The corresponding CTS test is updated in the next review.  However, it
is also cloned into the current unit test.

Test: * atest FrameworksCoreTests:PropertyInvalidatedCacheTests
 * atest FrameworksCoreTests:IpcDataCacheTest
Change-Id: I83ce97a4154b9065e0686ee99a25c90f955366ff
This commit is contained in:
Lee Shombert
2022-03-04 07:37:03 -08:00
parent c6c97b7b84
commit 4f1473c6ac
5 changed files with 752 additions and 47 deletions

View File

@@ -54,22 +54,6 @@ package android.app {
method public void onCanceled(@NonNull android.app.PendingIntent);
}
public class PropertyInvalidatedCache<Query, Result> {
ctor public PropertyInvalidatedCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.app.PropertyInvalidatedCache.QueryHandler<Query,Result>);
method public final void disableForCurrentProcess();
method public final void invalidateCache();
method public static void invalidateCache(@NonNull String, @NonNull String);
method @Nullable public final Result query(@NonNull Query);
field public static final String MODULE_BLUETOOTH = "bluetooth";
field public static final String MODULE_TELEPHONY = "telephony";
}
public abstract static class PropertyInvalidatedCache.QueryHandler<Q, R> {
ctor public PropertyInvalidatedCache.QueryHandler();
method @Nullable public abstract R apply(@NonNull Q);
method public boolean shouldBypassCache(@NonNull Q);
}
public class StatusBarManager {
method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void setExpansionDisabledForSimNetworkLock(boolean);
}
@@ -358,6 +342,22 @@ package android.os {
field public static final int DEVICE_INITIAL_SDK_INT;
}
public class IpcDataCache<Query, Result> {
ctor public IpcDataCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.os.IpcDataCache.QueryHandler<Query,Result>);
method public void disableForCurrentProcess();
method public static void disableForCurrentProcess(@NonNull String);
method public void invalidateCache();
method public static void invalidateCache(@NonNull String, @NonNull String);
method @Nullable public Result query(@NonNull Query);
field public static final String MODULE_BLUETOOTH = "bluetooth";
}
public abstract static class IpcDataCache.QueryHandler<Q, R> {
ctor public IpcDataCache.QueryHandler();
method @Nullable public abstract R apply(@NonNull Q);
method public boolean shouldBypassCache(@NonNull Q);
}
public interface Parcelable {
method public default int getStability();
}

View File

@@ -374,16 +374,17 @@ package android.app {
public class PropertyInvalidatedCache<Query, Result> {
ctor public PropertyInvalidatedCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.app.PropertyInvalidatedCache.QueryHandler<Query,Result>);
method @NonNull public static String createPropertyName(@NonNull String, @NonNull String);
method public final void disableForCurrentProcess();
method public void disableForCurrentProcess();
method public static void disableForCurrentProcess(@NonNull String);
method public static void disableForTestMode();
method public final void disableInstance();
method public final void disableSystemWide();
method public final void forgetDisableLocal();
method public boolean getDisabledState();
method public final void invalidateCache();
method public void invalidateCache();
method public static void invalidateCache(@NonNull String, @NonNull String);
method public final boolean isDisabled();
method @Nullable public final Result query(@NonNull Query);
method @Nullable public Result query(@NonNull Query);
method public static void setTestMode(boolean);
method public void testPropertyName();
field public static final String MODULE_BLUETOOTH = "bluetooth";
@@ -1726,6 +1727,19 @@ package android.os {
method @NonNull public static byte[] digest(@NonNull java.io.InputStream, @NonNull String) throws java.io.IOException, java.security.NoSuchAlgorithmException;
}
public class IpcDataCache<Query, Result> extends android.app.PropertyInvalidatedCache<Query,Result> {
ctor public IpcDataCache(int, @NonNull String, @NonNull String, @NonNull String, @NonNull android.os.IpcDataCache.QueryHandler<Query,Result>);
method public static void disableForCurrentProcess(@NonNull String);
method public static void invalidateCache(@NonNull String, @NonNull String);
field public static final String MODULE_BLUETOOTH = "bluetooth";
field public static final String MODULE_SYSTEM = "system_server";
field public static final String MODULE_TEST = "test";
}
public abstract static class IpcDataCache.QueryHandler<Q, R> extends android.app.PropertyInvalidatedCache.QueryHandler<Q,R> {
ctor public IpcDataCache.QueryHandler();
}
public final class MessageQueue {
method public int postSyncBarrier();
method public void removeSyncBarrier(int);

View File

@@ -18,7 +18,6 @@ package android.app;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.os.Handler;
import android.os.Looper;
@@ -137,6 +136,26 @@ import java.util.concurrent.atomic.AtomicLong;
* With this cache, clients perform a binder call to birthdayd if asking for a user's birthday
* for the first time; on subsequent queries, we return the already-known Birthday object.
*
* The second parameter to the IpcDataCache constructor is a string that identifies the "module"
* that owns the cache. There are some well-known modules (such as {@code MODULE_SYSTEM} but any
* string is permitted. The third parameters is the name of the API being cached; this, too, can
* any value. The fourth is the name of the cache. The cache is usually named after th API.
* Some things you must know about the three strings:
* <list>
* <ul> The system property that controls the cache is named {@code cache_key.<module>.<api>}.
* Usually, the SELinux rules permit a process to write a system property (and therefore
* invalidate a cache) based on the wildcard {@code cache_key.<module>.*}. This means that
* although the cache can be constructed with any module string, whatever string is chosen must be
* consistent with the SELinux configuration.
* <ul> The API name can be any string of alphanumeric characters. All caches with the same API
* are invalidated at the same time. If a server supports several caches and all are invalidated
* in common, then it is most efficient to assign the same API string to every cache.
* <ul> The cache name can be any string. In debug output, the name is used to distiguish between
* caches with the same API name. The cache name is also used when disabling caches in the
* current process. So, invalidation is based on the module+api but disabling (which is generally
* a once-per-process operation) is based on the cache name.
* </list>
*
* User birthdays do occasionally change, so we have to modify the server to invalidate this
* cache when necessary. That invalidation code looks like this:
*
@@ -192,25 +211,23 @@ import java.util.concurrent.atomic.AtomicLong;
* <pre>
* public class ActivityThread {
* ...
* private static final int BDAY_CACHE_MAX = 8; // Maximum birthdays to cache
* private static final String BDAY_CACHE_KEY = "cache_key.birthdayd";
* private final PropertyInvalidatedCache&lt;Integer, Birthday%&gt; mBirthdayCache = new
* PropertyInvalidatedCache&lt;Integer, Birthday%&gt;(BDAY_CACHE_MAX, BDAY_CACHE_KEY) {
* {@literal @}Override
* protected Birthday recompute(Integer userId) {
* return GetService("birthdayd").getUserBirthday(userId);
* }
* {@literal @}Override
* protected boolean bypass(Integer userId) {
* return userId == NEXT_BIRTHDAY;
* }
* };
* private final IpcDataCache.QueryHandler&lt;Integer, Birthday&gt; mBirthdayQuery =
* new IpcDataCache.QueryHandler&lt;Integer, Birthday&gt;() {
* {@literal @}Override
* public Birthday apply(Integer) {
* return GetService("birthdayd").getUserBirthday(userId);
* }
* {@literal @}Override
* public boolean shouldBypassQuery(Integer userId) {
* return userId == NEXT_BIRTHDAY;
* }
* };
* ...
* }
* </pre>
*
* If the {@code bypass()} method returns true then the cache is not used for that
* particular query. The {@code bypass()} method is not abstract and the default
* If the {@code shouldBypassQuery()} method returns true then the cache is not used for that
* particular query. The {@code shouldBypassQuery()} method is not abstract and the default
* implementation returns false.
*
* For security, there is a allowlist of processes that are allowed to invalidate a cache.
@@ -231,14 +248,12 @@ import java.util.concurrent.atomic.AtomicLong;
* @param <Result> The class holding cache entries; use a boxed primitive if possible
* @hide
*/
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
public class PropertyInvalidatedCache<Query, Result> {
/**
* This is a configuration class that customizes a cache instance.
* @hide
*/
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
public static abstract class QueryHandler<Q,R> {
/**
@@ -285,7 +300,6 @@ public class PropertyInvalidatedCache<Query, Result> {
* The module used for bluetooth caches.
* @hide
*/
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
public static final String MODULE_BLUETOOTH = "bluetooth";
@@ -533,7 +547,6 @@ public class PropertyInvalidatedCache<Query, Result> {
* @param computer The code to compute values that are not in the cache.
* @hide
*/
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
public PropertyInvalidatedCache(int maxEntries, @NonNull String module, @NonNull String api,
@NonNull String cacheName, @NonNull QueryHandler<Query, Result> computer) {
@@ -792,7 +805,7 @@ public class PropertyInvalidatedCache<Query, Result> {
* TODO(216112648) Remove this in favor of disableForCurrentProcess().
* @hide
*/
public final void disableLocal() {
public void disableLocal() {
disableForCurrentProcess();
}
@@ -802,12 +815,17 @@ public class PropertyInvalidatedCache<Query, Result> {
* property.
* @hide
*/
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
public final void disableForCurrentProcess() {
public void disableForCurrentProcess() {
disableLocal(mCacheName);
}
/** @hide */
@TestApi
public static void disableForCurrentProcess(@NonNull String cacheName) {
disableLocal(cacheName);
}
/**
* Return whether a cache instance is disabled.
* @hide
@@ -821,9 +839,8 @@ public class PropertyInvalidatedCache<Query, Result> {
* Get a value from the cache or recompute it.
* @hide
*/
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
public final @Nullable Result query(@NonNull Query query) {
public @Nullable Result query(@NonNull Query query) {
// Let access to mDisabled race: it's atomic anyway.
long currentNonce = (!isDisabled()) ? getCurrentNonce() : NONCE_DISABLED;
if (bypass(query)) {
@@ -964,9 +981,8 @@ public class PropertyInvalidatedCache<Query, Result> {
* PropertyInvalidatedCache is keyed on a particular property value.
* @hide
*/
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
public final void invalidateCache() {
public void invalidateCache() {
invalidateCache(mPropertyName);
}
@@ -974,7 +990,6 @@ public class PropertyInvalidatedCache<Query, Result> {
* Invalidate caches in all processes that are keyed for the module and api.
* @hide
*/
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
public static void invalidateCache(@NonNull String module, @NonNull String api) {
invalidateCache(createPropertyName(module, api));

View File

@@ -0,0 +1,364 @@
/*
* Copyright (C) 2022 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 android.os;
import android.app.PropertyInvalidatedCache;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.FastPrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* LRU cache that's invalidated when an opaque value in a property changes. Self-synchronizing,
* but doesn't hold a lock across data fetches on query misses.
*
* The intended use case is caching frequently-read, seldom-changed information normally retrieved
* across interprocess communication. Imagine that you've written a user birthday information
* daemon called "birthdayd" that exposes an {@code IUserBirthdayService} interface over
* binder. That binder interface looks something like this:
*
* <pre>
* parcelable Birthday {
* int month;
* int day;
* }
* interface IUserBirthdayService {
* Birthday getUserBirthday(int userId);
* }
* </pre>
*
* Suppose the service implementation itself looks like this...
*
* <pre>
* public class UserBirthdayServiceImpl implements IUserBirthdayService {
* private final HashMap&lt;Integer, Birthday%&gt; mUidToBirthday;
* {@literal @}Override
* public synchronized Birthday getUserBirthday(int userId) {
* return mUidToBirthday.get(userId);
* }
* private synchronized void updateBirthdays(Map&lt;Integer, Birthday%&gt; uidToBirthday) {
* mUidToBirthday.clear();
* mUidToBirthday.putAll(uidToBirthday);
* }
* }
* </pre>
*
* ... and we have a client in frameworks (loaded into every app process) that looks like this:
*
* <pre>
* public class ActivityThread {
* ...
* public Birthday getUserBirthday(int userId) {
* return GetService("birthdayd").getUserBirthday(userId);
* }
* ...
* }
* </pre>
*
* With this code, every time an app calls {@code getUserBirthday(uid)}, we make a binder call to
* the birthdayd process and consult its database of birthdays. If we query user birthdays
* frequently, we do a lot of work that we don't have to do, since user birthdays change
* infrequently.
*
* IpcDataCache is part of a pattern for optimizing this kind of information-querying code. Using
* {@code IpcDataCache}, you'd write the client this way:
*
* <pre>
* public class ActivityThread {
* ...
* private final IpcDataCache.QueryHandler&lt;Integer, Birthday&gt; mBirthdayQuery =
* new IpcDataCache.QueryHandler&lt;Integer, Birthday&gt;() {
* {@literal @}Override
* public Birthday apply(Integer) {
* return GetService("birthdayd").getUserBirthday(userId);
* }
* };
* private static final int BDAY_CACHE_MAX = 8; // Maximum birthdays to cache
* private static final String BDAY_API = "getUserBirthday";
* private final IpcDataCache&lt;Integer, Birthday%&gt; mBirthdayCache = new
* IpcDataCache&lt;Integer, Birthday%&gt;(
* BDAY_CACHE_MAX, MODULE_SYSTEM, BDAY_API, BDAY_API, mBirthdayQuery);
*
* public void disableUserBirthdayCache() {
* mBirthdayCache.disableForCurrentProcess();
* }
* public void invalidateUserBirthdayCache() {
* mBirthdayCache.invalidateCache();
* }
* public Birthday getUserBirthday(int userId) {
* return mBirthdayCache.query(userId);
* }
* ...
* }
* </pre>
*
* With this cache, clients perform a binder call to birthdayd if asking for a user's birthday
* for the first time; on subsequent queries, we return the already-known Birthday object.
*
* The second parameter to the IpcDataCache constructor is a string that identifies the "module"
* that owns the cache. There are some well-known modules (such as {@code MODULE_SYSTEM} but any
* string is permitted. The third parameters is the name of the API being cached; this, too, can
* any value. The fourth is the name of the cache. The cache is usually named after th API.
* Some things you must know about the three strings:
* <list>
* <ul> The system property that controls the cache is named {@code cache_key.<module>.<api>}.
* Usually, the SELinux rules permit a process to write a system property (and therefore
* invalidate a cache) based on the wildcard {@code cache_key.<module>.*}. This means that
* although the cache can be constructed with any module string, whatever string is chosen must be
* consistent with the SELinux configuration.
* <ul> The API name can be any string of alphanumeric characters. All caches with the same API
* are invalidated at the same time. If a server supports several caches and all are invalidated
* in common, then it is most efficient to assign the same API string to every cache.
* <ul> The cache name can be any string. In debug output, the name is used to distiguish between
* caches with the same API name. The cache name is also used when disabling caches in the
* current process. So, invalidation is based on the module+api but disabling (which is generally
* a once-per-process operation) is based on the cache name.
* </list>
*
* User birthdays do occasionally change, so we have to modify the server to invalidate this
* cache when necessary. That invalidation code looks like this:
*
* <pre>
* public class UserBirthdayServiceImpl {
* ...
* public UserBirthdayServiceImpl() {
* ...
* ActivityThread.currentActivityThread().disableUserBirthdayCache();
* ActivityThread.currentActivityThread().invalidateUserBirthdayCache();
* }
*
* private synchronized void updateBirthdays(Map&lt;Integer, Birthday%&gt; uidToBirthday) {
* mUidToBirthday.clear();
* mUidToBirthday.putAll(uidToBirthday);
* ActivityThread.currentActivityThread().invalidateUserBirthdayCache();
* }
* ...
* }
* </pre>
*
* The call to {@code IpcDataCache.invalidateCache()} guarantees that all clients will re-fetch
* birthdays from binder during consequent calls to
* {@code ActivityThread.getUserBirthday()}. Because the invalidate call happens with the lock
* held, we maintain consistency between different client views of the birthday state. The use of
* IpcDataCache in this idiomatic way introduces no new race conditions.
*
* IpcDataCache has a few other features for doing things like incremental enhancement of cached
* values and invalidation of multiple caches (that all share the same property key) at once.
*
* {@code BDAY_CACHE_KEY} is the name of a property that we set to an opaque unique value each
* time we update the cache. SELinux configuration must allow everyone to read this property
* and it must allow any process that needs to invalidate the cache (here, birthdayd) to write
* the property. (These properties conventionally begin with the "cache_key." prefix.)
*
* The {@code UserBirthdayServiceImpl} constructor calls {@code disableUserBirthdayCache()} so
* that calls to {@code getUserBirthday} from inside birthdayd don't go through the cache. In this
* local case, there's no IPC, so use of the cache is (depending on exact circumstance)
* unnecessary.
*
* There may be queries for which it is more efficient to bypass the cache than to cache the
* result. This would be true, for example, if some queries would require frequent cache
* invalidation while other queries require infrequent invalidation. To expand on the birthday
* example, suppose that there is a userId that signifies "the next birthday". When passed this
* userId, the server returns the next birthday among all users - this value changes as time
* advances. The userId value can be cached, but the cache must be invalidated whenever a
* birthday occurs, and this invalidates all birthdays. If there is a large number of users,
* invalidation will happen so often that the cache provides no value.
*
* The class provides a bypass mechanism to handle this situation.
* <pre>
* public class ActivityThread {
* ...
* private final IpcDataCache.QueryHandler&lt;Integer, Birthday&gt; mBirthdayQuery =
* new IpcDataCache.QueryHandler&lt;Integer, Birthday&gt;() {
* {@literal @}Override
* public Birthday apply(Integer) {
* return GetService("birthdayd").getUserBirthday(userId);
* }
* {@literal @}Override
* public boolean shouldBypassQuery(Integer userId) {
* return userId == NEXT_BIRTHDAY;
* }
* };
* ...
* }
* </pre>
*
* If the {@code shouldBypassQuery()} method returns true then the cache is not used for that
* particular query. The {@code shouldBypassQuery()} method is not abstract and the default
* implementation returns false.
*
* For security, there is a allowlist of processes that are allowed to invalidate a cache. The
* allowlist includes normal runtime processes but does not include test processes. Test
* processes must call {@code IpcDataCache.disableForTestMode()} to disable all cache activity in
* that process.
*
* Caching can be disabled completely by initializing {@code sEnabled} to false and rebuilding.
*
* To test a binder cache, create one or more tests that exercise the binder method. This should
* be done twice: once with production code and once with a special image that sets {@code DEBUG}
* and {@code VERIFY} true. In the latter case, verify that no cache inconsistencies are
* reported. If a cache inconsistency is reported, however, it might be a false positive. This
* happens if the server side data can be read and written non-atomically with respect to cache
* invalidation.
*
* @param <Query> The class used to index cache entries: must be hashable and comparable
* @param <Result> The class holding cache entries; use a boxed primitive if possible
* @hide
*/
@TestApi
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, Result> {
/**
* {@inheritDoc}
* @hide
*/
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
public static abstract class QueryHandler<Q,R>
extends PropertyInvalidatedCache.QueryHandler<Q,R> {
/**
* Compute a result given a query. The semantics are those of Functor.
*/
public abstract @Nullable R apply(@NonNull Q query);
/**
* Return true if a query should not use the cache. The default implementation
* always uses the cache.
*/
public boolean shouldBypassCache(@NonNull Q query) {
return false;
}
};
/**
* The module used for unit tests and cts tests. It is expected that no process in
* the system has permissions to write properties with this module.
* @hide
*/
@TestApi
public static final String MODULE_TEST = PropertyInvalidatedCache.MODULE_TEST;
/**
* The module used for system server/framework caches. This is not visible outside
* the system processes.
* @hide
*/
@TestApi
public static final String MODULE_SYSTEM = PropertyInvalidatedCache.MODULE_SYSTEM;
/**
* The module used for bluetooth caches.
* @hide
*/
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
public static final String MODULE_BLUETOOTH = PropertyInvalidatedCache.MODULE_BLUETOOTH;
/**
* Make a new property invalidated cache. The key is computed from the module and api
* parameters.
*
* @param maxEntries Maximum number of entries to cache; LRU discard
* @param module The module under which the cache key should be placed.
* @param api The api this cache front-ends. The api must be a Java identifier but
* need not be an actual api.
* @param cacheName Name of this cache in debug and dumpsys
* @param computer The code to compute values that are not in the cache.
* @hide
*/
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
public IpcDataCache(int maxEntries, @NonNull String module, @NonNull String api,
@NonNull String cacheName, @NonNull QueryHandler<Query, Result> computer) {
super(maxEntries, module, api, cacheName, computer);
}
/**
* {@inheritDoc}
* @hide
*/
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
@Override
public void disableForCurrentProcess() {
super.disableForCurrentProcess();
}
/**
* {@inheritDoc}
* @hide
*/
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
public static void disableForCurrentProcess(@NonNull String cacheName) {
PropertyInvalidatedCache.disableForCurrentProcess(cacheName);
}
/**
* {@inheritDoc}
* @hide
*/
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
@Override
public @Nullable Result query(@NonNull Query query) {
return super.query(query);
}
/**
* {@inheritDoc}
* @hide
*/
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
@Override
public void invalidateCache() {
super.invalidateCache();
}
/**
* Invalidate caches in all processes that are keyed for the module and api.
* @hide
*/
@SystemApi(client=SystemApi.Client.MODULE_LIBRARIES)
@TestApi
public static void invalidateCache(@NonNull String module, @NonNull String api) {
PropertyInvalidatedCache.invalidateCache(module, api);
}
}

View File

@@ -0,0 +1,312 @@
/*
* Copyright (C) 2022 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 android.os;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import androidx.test.filters.SmallTest;
import org.junit.After;
import org.junit.Test;
/**
* Test for verifying the behavior of {@link IpcDataCache}. This test does
* not use any actual binder calls - it is entirely self-contained. This test also relies
* on the test mode of {@link IpcDataCache} because Android SELinux rules do
* not grant test processes the permission to set system properties.
* <p>
* Build/Install/Run:
* atest FrameworksCoreTests:IpcDataCacheTest
*/
@SmallTest
public class IpcDataCacheTest {
// Configuration for creating caches
private static final String MODULE = IpcDataCache.MODULE_TEST;
private static final String API = "testApi";
// This class is a proxy for binder calls. It contains a counter that increments
// every time the class is queried.
private static class ServerProxy {
// The number of times this class was queried.
private int mCount = 0;
// A single query. The key behavior is that the query count is incremented.
boolean query(int x) {
mCount++;
return value(x);
}
// Return the expected value of an input, without incrementing the query count.
boolean value(int x) {
return x % 3 == 0;
}
// Verify the count.
void verify(int x) {
assertEquals(x, mCount);
}
}
// The functions for querying the server.
private static class ServerQuery
extends IpcDataCache.QueryHandler<Integer, Boolean> {
private final ServerProxy mServer;
ServerQuery(ServerProxy server) {
mServer = server;
}
@Override
public Boolean apply(Integer x) {
return mServer.query(x);
}
@Override
public boolean shouldBypassCache(Integer x) {
return x % 13 == 0;
}
}
// Clear the test mode after every test, in case this process is used for other
// tests. This also resets the test property map.
@After
public void tearDown() throws Exception {
IpcDataCache.setTestMode(false);
}
// This test is disabled pending an sepolicy change that allows any app to set the
// test property.
@Test
public void testBasicCache() {
// A stand-in for the binder. The test verifies that calls are passed through to
// this class properly.
ServerProxy tester = new ServerProxy();
// Create a cache that uses simple arithmetic to computer its values.
IpcDataCache<Integer, Boolean> testCache =
new IpcDataCache<>(4, MODULE, API, "testCache1",
new ServerQuery(tester));
IpcDataCache.setTestMode(true);
testCache.testPropertyName();
tester.verify(0);
assertEquals(tester.value(3), testCache.query(3));
tester.verify(1);
assertEquals(tester.value(3), testCache.query(3));
tester.verify(2);
testCache.invalidateCache();
assertEquals(tester.value(3), testCache.query(3));
tester.verify(3);
assertEquals(tester.value(5), testCache.query(5));
tester.verify(4);
assertEquals(tester.value(5), testCache.query(5));
tester.verify(4);
assertEquals(tester.value(3), testCache.query(3));
tester.verify(4);
// Invalidate the cache, and verify that the next read on 3 goes to the server.
testCache.invalidateCache();
assertEquals(tester.value(3), testCache.query(3));
tester.verify(5);
// Test bypass. The query for 13 always bypasses the cache.
assertEquals(tester.value(12), testCache.query(12));
assertEquals(tester.value(13), testCache.query(13));
assertEquals(tester.value(14), testCache.query(14));
tester.verify(8);
assertEquals(tester.value(12), testCache.query(12));
assertEquals(tester.value(13), testCache.query(13));
assertEquals(tester.value(14), testCache.query(14));
tester.verify(9);
}
@Test
public void testDisableCache() {
// A stand-in for the binder. The test verifies that calls are passed through to
// this class properly.
ServerProxy tester = new ServerProxy();
// Three caches, all using the same system property but one uses a different name.
IpcDataCache<Integer, Boolean> cache1 =
new IpcDataCache<>(4, MODULE, API, "cacheA",
new ServerQuery(tester));
IpcDataCache<Integer, Boolean> cache2 =
new IpcDataCache<>(4, MODULE, API, "cacheA",
new ServerQuery(tester));
IpcDataCache<Integer, Boolean> cache3 =
new IpcDataCache<>(4, MODULE, API, "cacheB",
new ServerQuery(tester));
// Caches are enabled upon creation.
assertEquals(false, cache1.getDisabledState());
assertEquals(false, cache2.getDisabledState());
assertEquals(false, cache3.getDisabledState());
// Disable the cache1 instance. Only cache1 is disabled
cache1.disableInstance();
assertEquals(true, cache1.getDisabledState());
assertEquals(false, cache2.getDisabledState());
assertEquals(false, cache3.getDisabledState());
// Disable cache1. This will disable cache1 and cache2 because they share the
// same name. cache3 has a different name and will not be disabled.
cache1.disableForCurrentProcess();
assertEquals(true, cache1.getDisabledState());
assertEquals(true, cache2.getDisabledState());
assertEquals(false, cache3.getDisabledState());
// Create a new cache1. Verify that the new instance is disabled.
cache1 = new IpcDataCache<>(4, MODULE, API, "cacheA",
new ServerQuery(tester));
assertEquals(true, cache1.getDisabledState());
// Remove the record of caches being locally disabled. This is a clean-up step.
cache1.forgetDisableLocal();
assertEquals(true, cache1.getDisabledState());
assertEquals(true, cache2.getDisabledState());
assertEquals(false, cache3.getDisabledState());
// Create a new cache1. Verify that the new instance is not disabled.
cache1 = new IpcDataCache<>(4, MODULE, API, "cacheA",
new ServerQuery(tester));
assertEquals(false, cache1.getDisabledState());
}
private static class TestQuery
extends IpcDataCache.QueryHandler<Integer, String> {
private int mRecomputeCount = 0;
@Override
public String apply(Integer qv) {
mRecomputeCount += 1;
return "foo" + qv.toString();
}
int getRecomputeCount() {
return mRecomputeCount;
}
}
private static class TestCache extends IpcDataCache<Integer, String> {
private final TestQuery mQuery;
TestCache() {
this(MODULE, API);
}
TestCache(String module, String api) {
this(module, api, new TestQuery());
}
TestCache(String module, String api, TestQuery query) {
super(4, module, api, "testCache7", query);
mQuery = query;
setTestMode(true);
testPropertyName();
}
int getRecomputeCount() {
return mQuery.getRecomputeCount();
}
}
@Test
public void testCacheRecompute() {
TestCache cache = new TestCache();
cache.invalidateCache();
assertEquals(cache.isDisabled(), false);
assertEquals("foo5", cache.query(5));
assertEquals(1, cache.getRecomputeCount());
assertEquals("foo5", cache.query(5));
assertEquals(1, cache.getRecomputeCount());
assertEquals("foo6", cache.query(6));
assertEquals(2, cache.getRecomputeCount());
cache.invalidateCache();
assertEquals("foo5", cache.query(5));
assertEquals("foo5", cache.query(5));
assertEquals(3, cache.getRecomputeCount());
// Invalidate the cache with a direct call to the property.
IpcDataCache.invalidateCache(MODULE, API);
assertEquals("foo5", cache.query(5));
assertEquals("foo5", cache.query(5));
assertEquals(4, cache.getRecomputeCount());
}
@Test
public void testCacheInitialState() {
TestCache cache = new TestCache();
assertEquals("foo5", cache.query(5));
assertEquals("foo5", cache.query(5));
assertEquals(2, cache.getRecomputeCount());
cache.invalidateCache();
assertEquals("foo5", cache.query(5));
assertEquals("foo5", cache.query(5));
assertEquals(3, cache.getRecomputeCount());
}
@Test
public void testCachePropertyUnset() {
final String UNSET_API = "otherApi";
TestCache cache = new TestCache(MODULE, UNSET_API);
assertEquals("foo5", cache.query(5));
assertEquals("foo5", cache.query(5));
assertEquals(2, cache.getRecomputeCount());
}
@Test
public void testCacheDisableState() {
TestCache cache = new TestCache();
assertEquals("foo5", cache.query(5));
assertEquals("foo5", cache.query(5));
assertEquals(2, cache.getRecomputeCount());
cache.invalidateCache();
assertEquals("foo5", cache.query(5));
assertEquals("foo5", cache.query(5));
assertEquals(3, cache.getRecomputeCount());
cache.disableSystemWide();
assertEquals("foo5", cache.query(5));
assertEquals("foo5", cache.query(5));
assertEquals(5, cache.getRecomputeCount());
cache.invalidateCache(); // Should not reenable
assertEquals("foo5", cache.query(5));
assertEquals("foo5", cache.query(5));
assertEquals(7, cache.getRecomputeCount());
}
@Test
public void testLocalProcessDisable() {
TestCache cache = new TestCache();
assertEquals(cache.isDisabled(), false);
cache.invalidateCache();
assertEquals("foo5", cache.query(5));
assertEquals(1, cache.getRecomputeCount());
assertEquals("foo5", cache.query(5));
assertEquals(1, cache.getRecomputeCount());
assertEquals(cache.isDisabled(), false);
cache.disableForCurrentProcess();
assertEquals(cache.isDisabled(), true);
assertEquals("foo5", cache.query(5));
assertEquals("foo5", cache.query(5));
assertEquals(3, cache.getRecomputeCount());
}
}