Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4f1473c6 authored by Lee Shombert's avatar Lee Shombert
Browse files

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
parent c6c97b7b
Loading
Loading
Loading
Loading
+16 −16
Original line number Diff line number Diff line
@@ -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();
  }
+17 −3
Original line number Diff line number Diff line
@@ -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);
+43 −28
Original line number Diff line number Diff line
@@ -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,16 +211,14 @@ 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) {
 *   private final IpcDataCache.QueryHandler&lt;Integer, Birthday&gt; mBirthdayQuery =
 *       new IpcDataCache.QueryHandler&lt;Integer, Birthday&gt;() {
 *           {@literal @}Override
 *       protected Birthday recompute(Integer userId) {
 *           public Birthday apply(Integer) {
 *              return GetService("birthdayd").getUserBirthday(userId);
 *           }
 *           {@literal @}Override
 *       protected boolean bypass(Integer userId) {
 *           public boolean shouldBypassQuery(Integer userId) {
 *               return userId == NEXT_BIRTHDAY;
 *           }
 *       };
@@ -209,8 +226,8 @@ import java.util.concurrent.atomic.AtomicLong;
 * }
 * </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));
+364 −0

File added.

Preview size limit exceeded, changes collapsed.

+312 −0
Original line number Diff line number Diff line
/*
 * 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());
    }
}