diff options
| author | 2022-03-07 20:26:05 +0000 | |
|---|---|---|
| committer | 2022-03-07 20:26:05 +0000 | |
| commit | 53dd894d42497bae0c5edb0b04089ff08cf6154f (patch) | |
| tree | 733263176bf85a540181d7229310f1132666da52 | |
| parent | 33e3c43571fc316022089074b19ac1eb7bf92901 (diff) | |
| parent | 4f1473c6ac050b770bc9c2c14e4528664c5df23b (diff) | |
Merge "Create os/IpcDataCache" into tm-dev
| -rw-r--r-- | core/api/module-lib-current.txt | 32 | ||||
| -rw-r--r-- | core/api/test-current.txt | 20 | ||||
| -rw-r--r-- | core/java/android/app/PropertyInvalidatedCache.java | 71 | ||||
| -rw-r--r-- | core/java/android/os/IpcDataCache.java | 364 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/os/IpcDataCacheTest.java | 312 |
5 files changed, 752 insertions, 47 deletions
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 659783c864cc..27ca6a2794b9 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -50,22 +50,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); } @@ -354,6 +338,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(); } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 7877677dfe71..c5cce35d988e 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -373,16 +373,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); diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index 2f202d95e0e3..df7bf7b94700 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -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<Integer, Birthday%> mBirthdayCache = new - * PropertyInvalidatedCache<Integer, Birthday%>(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<Integer, Birthday> mBirthdayQuery = + * new IpcDataCache.QueryHandler<Integer, Birthday>() { + * {@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)); diff --git a/core/java/android/os/IpcDataCache.java b/core/java/android/os/IpcDataCache.java new file mode 100644 index 000000000000..00734c806b7e --- /dev/null +++ b/core/java/android/os/IpcDataCache.java @@ -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<Integer, Birthday%> mUidToBirthday; + * {@literal @}Override + * public synchronized Birthday getUserBirthday(int userId) { + * return mUidToBirthday.get(userId); + * } + * private synchronized void updateBirthdays(Map<Integer, Birthday%> 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<Integer, Birthday> mBirthdayQuery = + * new IpcDataCache.QueryHandler<Integer, Birthday>() { + * {@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<Integer, Birthday%> mBirthdayCache = new + * IpcDataCache<Integer, Birthday%>( + * 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<Integer, Birthday%> 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<Integer, Birthday> mBirthdayQuery = + * new IpcDataCache.QueryHandler<Integer, Birthday>() { + * {@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); + } +} diff --git a/core/tests/coretests/src/android/os/IpcDataCacheTest.java b/core/tests/coretests/src/android/os/IpcDataCacheTest.java new file mode 100644 index 000000000000..fa7d7214d289 --- /dev/null +++ b/core/tests/coretests/src/android/os/IpcDataCacheTest.java @@ -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()); + } +} |