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

Commit c547e9fb authored by Chris Göllner's avatar Chris Göllner
Browse files

Fix: Invalidate Icon's Resources cache on configuration change

When an Icon is loaded, it caches the Resources object used to
retrieve the Drawable. This cache was not invalidated if the
Context's Configuration changed on subsequent loads. This could lead
to incorrect Resources being used, such as a Drawable with the
wrong density.

This change introduces a cache invalidation mechanism. Before
loading a Drawable, `loadDrawable` and `loadDrawableAsUser` now
check if the Context's Configuration differs from the cached
Resources. If so, the cache is cleared, forcing the resources to
be reloaded with the new configuration.

This behavior is gated by the
USE_RESOURCES_FROM_CONTEXT_TO_CREATE_DRAWABLE_ICONS feature flag
to ensure backward compatibility.

Flag: com.android.graphics.flags.use_resources_from_context_to_create_drawable_icons
Test: atest FrameworkCoreTests:IconTest
Bug: 420905041
Change-Id: Ib1303387e7e984b8053aee07e331e4aeecccbbdb
parent fd201a12
Loading
Loading
Loading
Loading
+179 −0
Original line number Diff line number Diff line
@@ -27,6 +27,8 @@ import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ParceledListSlice;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.RecordingCanvas;
@@ -37,16 +39,22 @@ import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.util.Log;

import androidx.test.core.app.ApplicationProvider;

import com.android.frameworks.coretests.R;
import com.android.graphics.flags.Flags;

import com.google.testing.junit.testparameterinjector.TestParameter;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;

import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

@@ -60,6 +68,11 @@ import java.util.Arrays;
@RunWith(TestParameterInjector.class)
public class IconTest {
    public static final String TAG = IconTest.class.getSimpleName();

    @ClassRule public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(
            com.android.graphics.flags.Flags.class);
    @Rule public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule();

    private Context mContext;

    public static void L(String s, Object... parts) {
@@ -610,6 +623,172 @@ public class IconTest {
        assertThat(loadedDrawable).isNull();
    }

    @EnableFlags(Flags.FLAG_USE_RESOURCES_FROM_CONTEXT_TO_CREATE_DRAWABLE_ICONS)
    @Test
    public void testLoadDrawable_withConfigurationChange_flagEnabled_invalidatesCache() {
        final String resPackage = mContext.getPackageName();
        final Icon icon = Icon.createWithResource(resPackage, R.drawable.landscape);

        // Initial load with default configuration
        icon.loadDrawable(mContext);
        final int initialDpi = icon.getResources().getConfiguration().densityDpi;

        // Create a new context with a different configuration
        final Configuration newConfig =
                new Configuration(mContext.getResources().getConfiguration());
        final int newDpi = initialDpi + 100;
        newConfig.densityDpi = newDpi;
        final Context newContext = mContext.createConfigurationContext(newConfig);

        // Load again with the new context
        icon.loadDrawable(newContext);

        // Verify that the cached resources have been updated to the new configuration
        assertThat(icon.getResources().getConfiguration().densityDpi).isEqualTo(newDpi);
    }

    @DisableFlags(Flags.FLAG_USE_RESOURCES_FROM_CONTEXT_TO_CREATE_DRAWABLE_ICONS)
    @Test
    public void testLoadDrawable_withConfigurationChange_flagDisabled_doesNotInvalidateCache() {
        final String resPackage = mContext.getPackageName();
        final Icon icon = Icon.createWithResource(resPackage, R.drawable.landscape);

        // Initial load with default configuration
        icon.loadDrawable(mContext);
        final int initialDpi = icon.getResources().getConfiguration().densityDpi;

        // Create a new context with a different configuration
        final Configuration newConfig =
                new Configuration(mContext.getResources().getConfiguration());
        final int newDpi = initialDpi + 100;
        newConfig.densityDpi = newDpi;
        final Context newContext = mContext.createConfigurationContext(newConfig);

        // Load again with the new context
        icon.loadDrawable(newContext);

        // Verify that the cached resources have NOT been updated, as the flag is off
        assertThat(icon.getResources().getConfiguration().densityDpi).isEqualTo(initialDpi);
    }

    @EnableFlags(Flags.FLAG_USE_RESOURCES_FROM_CONTEXT_TO_CREATE_DRAWABLE_ICONS)
    @Test
    public void testLoadDrawableAsUser_withConfigurationChange_flagEnabled_invalidatesCache() {
        final String resPackage = mContext.getPackageName();
        final Icon icon = Icon.createWithResource(resPackage, R.drawable.landscape);

        // Initial load with default configuration
        icon.loadDrawableAsUser(mContext, mContext.getUserId());
        final int initialDpi = icon.getResources().getConfiguration().densityDpi;

        // Create a new context with a different configuration
        final Configuration newConfig =
                new Configuration(mContext.getResources().getConfiguration());
        final int newDpi = initialDpi + 100;
        newConfig.densityDpi = newDpi;
        final Context newContext = mContext.createConfigurationContext(newConfig);

        // Load again with the new context
        icon.loadDrawableAsUser(newContext, newContext.getUserId());

        // Verify that the cached resources have been updated to the new configuration
        assertThat(icon.getResources().getConfiguration().densityDpi).isEqualTo(newDpi);
    }

    @DisableFlags(Flags.FLAG_USE_RESOURCES_FROM_CONTEXT_TO_CREATE_DRAWABLE_ICONS)
    @Test
    public void
            testLoadDrawableAsUser_withConfigurationChange_flagDisabled_doesNotInvalidateCache() {
        final String resPackage = mContext.getPackageName();
        final Icon icon = Icon.createWithResource(resPackage, R.drawable.landscape);

        // Initial load with default configuration
        icon.loadDrawableAsUser(mContext, mContext.getUserId());
        final int initialDpi = icon.getResources().getConfiguration().densityDpi;

        // Create a new context with a different configuration
        final Configuration newConfig =
                new Configuration(mContext.getResources().getConfiguration());
        final int newDpi = initialDpi + 100;
        newConfig.densityDpi = newDpi;
        final Context newContext = mContext.createConfigurationContext(newConfig);

        // Load again with the new context
        icon.loadDrawableAsUser(newContext, newContext.getUserId());

        // Verify that the cached resources have NOT been updated, as the flag is off
        assertThat(icon.getResources().getConfiguration().densityDpi).isEqualTo(initialDpi);
    }

    @EnableFlags(Flags.FLAG_USE_RESOURCES_FROM_CONTEXT_TO_CREATE_DRAWABLE_ICONS)
    @Test
    public void testLoadDrawable_withNoConfigurationChange_flagEnabled_doesNotInvalidateCache() {
        final String resPackage = mContext.getPackageName();
        final Icon icon = Icon.createWithResource(resPackage, R.drawable.landscape);

        // Initial load with default configuration
        icon.loadDrawable(mContext);
        final Resources initialResources = icon.getResources();

        // Load again with the same context
        icon.loadDrawable(mContext);

        // Verify that the cached resources have NOT been updated
        assertThat(icon.getResources()).isSameInstanceAs(initialResources);
    }

    @EnableFlags(Flags.FLAG_USE_RESOURCES_FROM_CONTEXT_TO_CREATE_DRAWABLE_ICONS)
    @Test
    public void
            testLoadDrawableAsUser_withNoConfigurationChange_flagEnabled_doesNotInvalidateCache() {
        final String resPackage = mContext.getPackageName();
        final Icon icon = Icon.createWithResource(resPackage, R.drawable.landscape);

        // Initial load with default configuration
        icon.loadDrawableAsUser(mContext, mContext.getUserId());
        final Resources initialResources = icon.getResources();

        // Load again with the same context
        icon.loadDrawableAsUser(mContext, mContext.getUserId());

        // Verify that the cached resources have NOT been updated
        assertThat(icon.getResources()).isSameInstanceAs(initialResources);
    }

    @DisableFlags(Flags.FLAG_USE_RESOURCES_FROM_CONTEXT_TO_CREATE_DRAWABLE_ICONS)
    @Test
    public void testLoadDrawable_withNoConfigurationChange_flagDisabled_doesNotInvalidateCache() {
        final String resPackage = mContext.getPackageName();
        final Icon icon = Icon.createWithResource(resPackage, R.drawable.landscape);

        // Initial load with default configuration
        icon.loadDrawable(mContext);
        final Resources initialResources = icon.getResources();

        // Load again with the same context
        icon.loadDrawable(mContext);

        // Verify that the cached resources have NOT been updated
        assertThat(icon.getResources()).isSameInstanceAs(initialResources);
    }

    @DisableFlags(Flags.FLAG_USE_RESOURCES_FROM_CONTEXT_TO_CREATE_DRAWABLE_ICONS)
    @Test
    public void
            testLoadDrawableAsUser_withNoConfigurationChange_flagDisabled_doesNotInvalidateCache() {
        final String resPackage = mContext.getPackageName();
        final Icon icon = Icon.createWithResource(resPackage, R.drawable.landscape);

        // Initial load with default configuration
        icon.loadDrawableAsUser(mContext, mContext.getUserId());
        final Resources initialResources = icon.getResources();

        // Load again with the same context
        icon.loadDrawableAsUser(mContext, mContext.getUserId());

        // Verify that the cached resources have NOT been updated
        assertThat(icon.getResources()).isSameInstanceAs(initialResources);
    }

    // ======== utils ========

+16 −0
Original line number Diff line number Diff line
@@ -485,6 +485,7 @@ public final class Icon implements Parcelable {
                return new AdaptiveIconDrawable(null,
                    new BitmapDrawable(context.getResources(), fixMaxBitmapSize(getBitmap())));
            case TYPE_RESOURCE:
                invalidateResourcesCacheIfNeeded(context);
                if (getResources() == null) {
                    // figure out where to load resources from
                    String resPackage = getResPackage();
@@ -595,6 +596,7 @@ public final class Icon implements Parcelable {
            if (TextUtils.isEmpty(resPackage)) {
                resPackage = context.getPackageName();
            }
            invalidateResourcesCacheIfNeeded(context);
            if (getResources() == null && !(getResPackage().equals("android"))) {
                // TODO(b/173307037): Move CONTEXT_INCLUDE_CODE to ContextImpl.createContextAsUser
                final Context userContext;
@@ -623,6 +625,20 @@ public final class Icon implements Parcelable {
        return loadDrawable(context);
    }

    private void invalidateResourcesCacheIfNeeded(Context context) {
        if (!DesktopExperienceFlags.USE_RESOURCES_FROM_CONTEXT_TO_CREATE_DRAWABLE_ICONS.isTrue()) {
            return;
        }
        if (mType == TYPE_RESOURCE && mObj1 != null) {
            Resources cachedResources = (Resources) mObj1;
            if (cachedResources.getConfiguration().diff(context.getResources().getConfiguration())
                    != 0) {
                // Invalidate the cache if the configuration has changed.
                mObj1 = null;
            }
        }
    }

    /**
     * Load a drawable, but in the case of URI types, it will check if the passed uid has a grant
     * to load the resource. The check will be performed using the permissions of the passed uid,
+2 −0
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
@@ -86,6 +87,7 @@ public class StatusBarIconViewTest extends SysuiTestCase {
    public void setUp() throws Exception {
        // Set up context such that asking for "mockPackage" resources returns mMockResources.
        mMockResources = mock(Resources.class);
        doReturn(new Configuration()).when(mMockResources).getConfiguration();
        mPackageManagerSpy = spy(getContext().getPackageManager());
        doReturn(mMockResources).when(mPackageManagerSpy)
                .getResourcesForApplication(eq("mockPackage"));