diff options
author | Jerome Gaillard <jgaillard@google.com> | 2022-06-06 18:02:13 +0100 |
---|---|---|
committer | Jerome Gaillard <jgaillard@google.com> | 2023-02-07 18:44:53 +0000 |
commit | 6dc349b485562a32231ad559519d0efc619db298 (patch) | |
tree | 448dbf0354d8eb19d90ad4228612c0e5ee6d716a | |
parent | 4750e1cdf2158de3d76cbbbe9b8d159683caa3a0 (diff) | |
download | layoutlib-6dc349b485562a32231ad559519d0efc619db298.tar.gz |
Add dynamic theming to layoutlib
Layoutlib receives the path to an image file to use as a wallpaper.
It creates a dynamic theme reusing/copying what the framework is doing.
It sets the dynamic theme by overriding access to system colors through
the DynamicRenderResources wrapper.
Bug: 227467236
Test: tests added
Change-Id: Id37b531c5f29e57c37f5390804ddc58d27a9eeb0
(cherry picked from commit c8a05ec2abe059f44ea4371d71dbdcb2630ce4ec)
Merged-In: Id37b531c5f29e57c37f5390804ddc58d27a9eeb0
20 files changed, 488 insertions, 3 deletions
diff --git a/.idea/libraries/kotlin_stdlib.xml b/.idea/libraries/kotlin_stdlib.xml new file mode 100644 index 0000000000..9673da8948 --- /dev/null +++ b/.idea/libraries/kotlin_stdlib.xml @@ -0,0 +1,9 @@ +<component name="libraryTable"> + <library name="kotlin-stdlib"> + <CLASSES> + <root url="jar://$PROJECT_DIR$/../../external/kotlinc/lib/kotlin-stdlib.jar!/" /> + </CLASSES> + <JAVADOC /> + <SOURCES /> + </library> +</component>
\ No newline at end of file diff --git a/Android.bp b/Android.bp index 4461b77d2b..e81683853c 100644 --- a/Android.bp +++ b/Android.bp @@ -37,6 +37,7 @@ java_genrule_host { ":ext{.jar}", ":icu4j-icudata-jarjar{.jar}", // HOST ":icu4j-icutzdata-jarjar{.jar}", // HOST + ":monet{.jar}", ], cmd: "rm -f $(out) && $(location layoutlib_create) --create-stub $(out) $(in)", } @@ -51,5 +52,6 @@ java_device_for_host { "framework-all", "icu4j-icudata-jarjar", "icu4j-icutzdata-jarjar", + "monet", ], } diff --git a/bridge/bridge.iml b/bridge/bridge.iml index 62d9cf8318..bc0f30d074 100644 --- a/bridge/bridge.iml +++ b/bridge/bridge.iml @@ -93,5 +93,6 @@ </library> </orderEntry> <orderEntry type="module" module-name="validator" /> + <orderEntry type="library" scope="TEST" name="kotlin-stdlib" level="project" /> </component> </module>
\ No newline at end of file diff --git a/bridge/src/android/graphics/drawable/AdaptiveIconDrawable_Delegate.java b/bridge/src/android/graphics/drawable/AdaptiveIconDrawable_Delegate.java index 3978af0701..fe307cab68 100644 --- a/bridge/src/android/graphics/drawable/AdaptiveIconDrawable_Delegate.java +++ b/bridge/src/android/graphics/drawable/AdaptiveIconDrawable_Delegate.java @@ -17,8 +17,12 @@ package android.graphics.drawable; import com.android.internal.R; +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.tools.layoutlib.annotations.LayoutlibDelegate; import android.content.res.Resources; +import android.content.res.Resources_Delegate; +import android.graphics.Canvas; public class AdaptiveIconDrawable_Delegate { public static String sPath; @@ -35,4 +39,42 @@ public class AdaptiveIconDrawable_Delegate { } return res.getString(resId); } + + @LayoutlibDelegate + public static void draw(AdaptiveIconDrawable thisDrawable, Canvas canvas) { + Resources res = Resources.getSystem(); + BridgeContext context = Resources_Delegate.getContext(res); + if (context.hasDynamicColors()) { + AdaptiveIconDrawable themedIcon = createThemedVersion(thisDrawable, res); + themedIcon.onBoundsChange(thisDrawable.getBounds()); + themedIcon.draw_Original(canvas); + } else { + thisDrawable.draw_Original(canvas); + } + } + + /** + * This builds the themed version of {@link AdaptiveIconDrawable}, copying what the + * framework does in {@link com.android.launcher3.Utilities#getFullDrawable} + */ + private static AdaptiveIconDrawable createThemedVersion(AdaptiveIconDrawable base, + Resources resources) { + Drawable mono = base.getMonochrome(); + mono = mono.mutate(); + int[] colors = getColors(resources); + mono.setTint(colors[1]); + return new AdaptiveIconDrawable(new ColorDrawable(colors[0]), mono); + } + + private static int[] getColors(Resources resources) { + int[] colors = new int[2]; + if (resources.getConfiguration().isNightModeActive()) { + colors[0] = resources.getColor(android.R.color.system_neutral1_800, null); + colors[1] = resources.getColor(android.R.color.system_accent1_100, null); + } else { + colors[0] = resources.getColor(android.R.color.system_accent1_100, null); + colors[1] = resources.getColor(android.R.color.system_neutral2_700, null); + } + return colors; + } } diff --git a/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java index c3b5ede7c5..b3f985afd6 100644 --- a/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java +++ b/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java @@ -70,6 +70,7 @@ import android.database.DatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; import android.hardware.display.DisplayManager; import android.hardware.input.InputManager; @@ -164,7 +165,7 @@ public class BridgeContext extends Context { private Resources mSystemResources; private final Object mProjectKey; private final DisplayMetrics mMetrics; - private final RenderResources mRenderResources; + private final DynamicRenderResources mRenderResources; private final Configuration mConfig; private final ApplicationInfo mApplicationInfo; private final LayoutlibCallback mLayoutlibCallback; @@ -242,7 +243,7 @@ public class BridgeContext extends Context { mMetrics = metrics; mLayoutlibCallback = layoutlibCallback; - mRenderResources = renderResources; + mRenderResources = new DynamicRenderResources(renderResources); mConfig = config; AssetManager systemAssetManager = AssetManager.getSystem(); if (systemAssetManager instanceof BridgeAssetManager) { @@ -2271,4 +2272,12 @@ public class BridgeContext extends Context { public SessionInteractiveData getSessionInteractiveData() { return mSessionInteractiveData; } + + public boolean hasDynamicColors() { + return mRenderResources.hasDynamicColors(); + } + + public void applyWallpaper(String wallpaperPath) { + mRenderResources.setWallpaper(wallpaperPath, mConfig.isNightModeActive()); + } } diff --git a/bridge/src/com/android/layoutlib/bridge/android/DynamicRenderResources.java b/bridge/src/com/android/layoutlib/bridge/android/DynamicRenderResources.java new file mode 100644 index 0000000000..2f01a85f9d --- /dev/null +++ b/bridge/src/com/android/layoutlib/bridge/android/DynamicRenderResources.java @@ -0,0 +1,219 @@ +/* + * 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 com.android.layoutlib.bridge.android; + +import com.android.ide.common.rendering.api.ILayoutLog; +import com.android.ide.common.rendering.api.RenderResources; +import com.android.ide.common.rendering.api.ResourceReference; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.ResourceValueImpl; +import com.android.ide.common.rendering.api.StyleResourceValue; +import com.android.internal.graphics.ColorUtils; +import com.android.resources.ResourceType; +import com.android.systemui.monet.ColorScheme; +import com.android.systemui.monet.Style; +import com.android.tools.layoutlib.annotations.VisibleForTesting; + +import android.app.WallpaperColors; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Wrapper for RenderResources that allows overriding default system colors + * when using dynamic theming. + */ +public class DynamicRenderResources extends RenderResources { + private final RenderResources mBaseResources; + private Map<String, Integer> mDynamicColorMap; + + public DynamicRenderResources(RenderResources baseResources) { + mBaseResources = baseResources; + } + + @Override + public void setLogger(ILayoutLog logger) { + mBaseResources.setLogger(logger); + } + + @Override + public StyleResourceValue getDefaultTheme() { + return mBaseResources.getDefaultTheme(); + } + + @Override + public void applyStyle(StyleResourceValue theme, boolean useAsPrimary) { + mBaseResources.applyStyle(theme, useAsPrimary); + } + + @Override + public void clearStyles() { + mBaseResources.clearStyles(); + } + + @Override + public List<StyleResourceValue> getAllThemes() { + return mBaseResources.getAllThemes(); + } + + @Override + public ResourceValue findItemInTheme(ResourceReference attr) { + ResourceValue baseValue = mBaseResources.findItemInTheme(attr); + return resolveDynamicColors(baseValue); + } + + @Override + public ResourceValue findItemInStyle(StyleResourceValue style, ResourceReference attr) { + ResourceValue baseValue = mBaseResources.findItemInStyle(style, attr); + return resolveDynamicColors(baseValue); + } + + @Override + public ResourceValue findResValue(String reference, boolean forceFrameworkOnly) { + ResourceValue baseValue = mBaseResources.findResValue(reference, forceFrameworkOnly); + return resolveDynamicColors(baseValue); + } + + @Override + public ResourceValue dereference(ResourceValue resourceValue) { + ResourceValue baseValue = mBaseResources.dereference(resourceValue); + return resolveDynamicColors(baseValue); + } + + @Override + public ResourceValue getUnresolvedResource(ResourceReference reference) { + ResourceValue baseValue = mBaseResources.getUnresolvedResource(reference); + return resolveDynamicColors(baseValue); + } + + @Override + public ResourceValue getResolvedResource(ResourceReference reference) { + ResourceValue baseValue = mBaseResources.getResolvedResource(reference); + return resolveDynamicColors(baseValue); + } + + @Override + public ResourceValue resolveResValue(ResourceValue value) { + ResourceValue baseValue = mBaseResources.resolveResValue(value); + return resolveDynamicColors(baseValue); + } + + @Override + public StyleResourceValue getParent(StyleResourceValue style) { + return mBaseResources.getParent(style); + } + + @Override + public StyleResourceValue getStyle(ResourceReference reference) { + return mBaseResources.getStyle(reference); + } + + private ResourceValue resolveDynamicColors(ResourceValue baseValue) { + if (hasDynamicColors() && baseValue != null && isDynamicColor(baseValue)) { + int dynamicColor = mDynamicColorMap.get(baseValue.getName()); + String colorHex = "#" + Integer.toHexString(dynamicColor).substring(2); + return new ResourceValueImpl(baseValue.getNamespace(), baseValue.getResourceType(), + baseValue.getName(), colorHex); + } + return baseValue; + } + + public void setWallpaper(String wallpaperPath, boolean isNightMode) { + if (wallpaperPath == null) { + mDynamicColorMap = null; + return; + } + try { + Bitmap wallpaper = BitmapFactory.decodeFile(wallpaperPath); + mDynamicColorMap = createDynamicColorMap(wallpaper, isNightMode); + } catch (IllegalArgumentException ignore) { + mDynamicColorMap = null; + } + } + + /** + * Extracts colors from the wallpaper and creates the corresponding dynamic theme. + * It uses the main wallpaper color and the {@link Style#TONAL_SPOT} style. + * + * @param wallpaper bitmap containing the wallpaper to use + * @param isNightMode whether to use night mode or not + * + * @return map of system color names to their dynamic values + */ + @VisibleForTesting + static Map<String, Integer> createDynamicColorMap(Bitmap wallpaper, boolean isNightMode) { + if (wallpaper == null) { + return null; + } + WallpaperColors wallpaperColors = WallpaperColors.fromBitmap(wallpaper); + int seed = ColorScheme.getSeedColor(wallpaperColors); + ColorScheme scheme = new ColorScheme(seed, isNightMode); + Map<String, Integer> dynamicColorMap = new HashMap<>(); + int paletteSize = scheme.getAccent1().size(); + extractPalette(scheme.getAllAccentColors(), "accent", paletteSize, dynamicColorMap); + extractPalette(scheme.getAllNeutralColors(), "neutral", paletteSize, dynamicColorMap); + return dynamicColorMap; + } + + /** + * Builds the dynamic theme from the {@link ColorScheme} copying what is done + * in {@link ThemeOverlayController#getOverlay} + */ + private static void extractPalette(List<Integer> shades, String name, int paletteSize, + Map<String, Integer> colorMap) { + for (int i = 0; i < shades.size(); i++) { + int luminosity = i % paletteSize; + int paletteIndex = i / paletteSize + 1; + String resourceName; + String baseResourceName = "system_" + name + paletteIndex; + switch (luminosity) { + case 0: + resourceName = baseResourceName + "_0"; + colorMap.put(resourceName, Color.BLACK); + resourceName = baseResourceName + "_10"; + break; + case 1: + resourceName = baseResourceName + "_50"; + break; + default: + resourceName = baseResourceName + "_" + (luminosity - 1) + "00"; + } + colorMap.put(resourceName, ColorUtils.setAlphaComponent(shades.get(i), 0xFF)); + } + } + + private static boolean isDynamicColor(ResourceValue resourceValue) { + if (!resourceValue.isFramework() || resourceValue.getResourceType() != ResourceType.COLOR) { + return false; + } + return resourceValue.getName().startsWith("system_accent") + || resourceValue.getName().startsWith("system_neutral"); + } + + public boolean hasDynamicColors() { + return mDynamicColorMap != null; + } + + @VisibleForTesting + void setDynamicColorMap(Map<String, Integer> dynamicColorMap) { + mDynamicColorMap = dynamicColorMap; + } +} diff --git a/bridge/src/com/android/layoutlib/bridge/android/RenderParamsFlags.java b/bridge/src/com/android/layoutlib/bridge/android/RenderParamsFlags.java index 678b244893..08fb9caa26 100644 --- a/bridge/src/com/android/layoutlib/bridge/android/RenderParamsFlags.java +++ b/bridge/src/com/android/layoutlib/bridge/android/RenderParamsFlags.java @@ -70,6 +70,13 @@ public final class RenderParamsFlags { public static final Key<Boolean> FLAG_ENABLE_LAYOUT_VALIDATOR_IMAGE_CHECK = new Key<>("enableLayoutValidatorImageCheck", Boolean.class); + /** + * To tell Layoutlib the path of the image file of the wallpaper to use for dynamic theming. + * If null, use default system colors. + */ + public static final Key<String> FLAG_KEY_WALLPAPER_PATH = + new Key<>("wallpaperPath", String.class); + // Disallow instances. private RenderParamsFlags() {} } diff --git a/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java b/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java index 2b87435669..31cc1e18ee 100644 --- a/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java +++ b/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java @@ -261,6 +261,7 @@ public abstract class RenderAction<T extends RenderParams> { // scene mContext.initResources(mParams.getAssets()); sCurrentContext = mContext; + mContext.applyWallpaper(mParams.getFlag(RenderParamsFlags.FLAG_KEY_WALLPAPER_PATH)); // Set-up WindowManager // FIXME: find those out, and possibly add them to the render params diff --git a/bridge/tests/res/com/android/layoutlib/testdata/wallpaper1.webp b/bridge/tests/res/com/android/layoutlib/testdata/wallpaper1.webp Binary files differnew file mode 100644 index 0000000000..6460b17b01 --- /dev/null +++ b/bridge/tests/res/com/android/layoutlib/testdata/wallpaper1.webp diff --git a/bridge/tests/res/com/android/layoutlib/testdata/wallpaper2.webp b/bridge/tests/res/com/android/layoutlib/testdata/wallpaper2.webp Binary files differnew file mode 100644 index 0000000000..786712d078 --- /dev/null +++ b/bridge/tests/res/com/android/layoutlib/testdata/wallpaper2.webp diff --git a/bridge/tests/res/testApp/MyApplication/golden/adaptive_icon_dynamic_green.png b/bridge/tests/res/testApp/MyApplication/golden/adaptive_icon_dynamic_green.png Binary files differnew file mode 100644 index 0000000000..61f1f18af1 --- /dev/null +++ b/bridge/tests/res/testApp/MyApplication/golden/adaptive_icon_dynamic_green.png diff --git a/bridge/tests/res/testApp/MyApplication/golden/adaptive_icon_dynamic_orange.png b/bridge/tests/res/testApp/MyApplication/golden/adaptive_icon_dynamic_orange.png Binary files differnew file mode 100644 index 0000000000..dd1dd57027 --- /dev/null +++ b/bridge/tests/res/testApp/MyApplication/golden/adaptive_icon_dynamic_orange.png diff --git a/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/adaptive.xml b/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/adaptive.xml index cc9c449563..80f760a278 100644 --- a/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/adaptive.xml +++ b/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/adaptive.xml @@ -3,4 +3,5 @@ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <background android:drawable="@color/red" /> <foreground android:drawable="@drawable/headset" /> + <monochrome android:drawable="@drawable/headset" /> </adaptive-icon>
\ No newline at end of file diff --git a/bridge/tests/run_tests.sh b/bridge/tests/run_tests.sh index ae737fbe20..e25d272591 100755 --- a/bridge/tests/run_tests.sh +++ b/bridge/tests/run_tests.sh @@ -50,7 +50,7 @@ ${STUDIO_JDK}/bin/java -ea \ -Dplatform.dir=${PLATFORM} \ -Dtest_res.dir=${SCRIPT_DIR}/res \ -Dtest_failure.dir=${OUT_DIR}/${FAILURE_DIR} \ - -cp ${MISC_COMMON}/tools-common/tools-common-prebuilt.jar:${MISC_COMMON}/ninepatch/ninepatch-prebuilt.jar:${MISC_COMMON}/sdk-common/sdk-common.jar:${MISC_COMMON}/kxml2/kxml2-2.3.0.jar:${MISC_COMMON}/layoutlib_api/layoutlib_api-prebuilt.jar:${OUT_INTERMEDIATES}/prebuilts/tools/common/m2/trove-prebuilt/linux_glibc_common/combined/trove-prebuilt.jar:${OUT_INTERMEDIATES}/external/junit/junit/linux_glibc_common/javac/junit.jar:${OUT_INTERMEDIATES}/external/guava/guava-jre/linux_glibc_common/javac/guava-jre.jar:${OUT_INTERMEDIATES}/external/hamcrest/hamcrest-core/hamcrest/linux_glibc_common/javac/hamcrest.jar:${OUT_INTERMEDIATES}/external/mockito/mockito/linux_glibc_common/combined/mockito.jar:${OUT_INTERMEDIATES}/external/objenesis/objenesis/linux_glibc_common/javac/objenesis.jar:${OUT_INTERMEDIATES}/frameworks/layoutlib/bridge/layoutlib/linux_glibc_common/withres/layoutlib.jar:${OUT_INTERMEDIATES}/frameworks/layoutlib/temp_layoutlib/linux_glibc_common/gen/temp_layoutlib.jar:${OUT_INTERMEDIATES}/frameworks/layoutlib/bridge/tests/layoutlib-tests/linux_glibc_common/withres/layoutlib-tests.jar \ + -cp ${MISC_COMMON}/tools-common/tools-common-prebuilt.jar:${MISC_COMMON}/ninepatch/ninepatch-prebuilt.jar:${MISC_COMMON}/sdk-common/sdk-common.jar:${MISC_COMMON}/kxml2/kxml2-2.3.0.jar:${MISC_COMMON}/layoutlib_api/layoutlib_api-prebuilt.jar:${OUT_INTERMEDIATES}/prebuilts/tools/common/m2/trove-prebuilt/linux_glibc_common/combined/trove-prebuilt.jar:${OUT_INTERMEDIATES}/external/junit/junit/linux_glibc_common/javac/junit.jar:${OUT_INTERMEDIATES}/external/guava/guava-jre/linux_glibc_common/javac/guava-jre.jar:${OUT_INTERMEDIATES}/external/hamcrest/hamcrest-core/hamcrest/linux_glibc_common/javac/hamcrest.jar:${OUT_INTERMEDIATES}/external/mockito/mockito/linux_glibc_common/combined/mockito.jar:${OUT_INTERMEDIATES}/external/objenesis/objenesis/linux_glibc_common/javac/objenesis.jar:${OUT_INTERMEDIATES}/frameworks/layoutlib/bridge/layoutlib/linux_glibc_common/withres/layoutlib.jar:${OUT_INTERMEDIATES}/frameworks/layoutlib/temp_layoutlib/linux_glibc_common/gen/temp_layoutlib.jar:${OUT_INTERMEDIATES}/frameworks/layoutlib/bridge/tests/layoutlib-tests/linux_glibc_common/withres/layoutlib-tests.jar:${OUT_INTERMEDIATES}/external/kotlinc/kotlin-stdlib/linux_glibc_common/combined/kotlin-stdlib.jar \ org.junit.runner.JUnitCore \ com.android.layoutlib.bridge.intensive.Main diff --git a/bridge/tests/src/com/android/layoutlib/bridge/android/BridgeContextTest.java b/bridge/tests/src/com/android/layoutlib/bridge/android/BridgeContextTest.java index 5266c57909..fd962c1c82 100644 --- a/bridge/tests/src/com/android/layoutlib/bridge/android/BridgeContextTest.java +++ b/bridge/tests/src/com/android/layoutlib/bridge/android/BridgeContextTest.java @@ -16,6 +16,7 @@ package com.android.layoutlib.bridge.android; +import com.android.ide.common.rendering.api.RenderResources; import com.android.ide.common.rendering.api.SessionParams; import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.impl.RenderAction; @@ -32,9 +33,14 @@ import android.R.style; import android.content.Context; import android.content.res.Configuration; import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.util.DisplayMetrics; import android.view.ContextThemeWrapper; +import java.util.Map; + +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -148,4 +154,47 @@ public class BridgeContextTest extends RenderTestBase { assertNull(context.getSystemService("my_custom_service")); sRenderMessages.removeIf(message -> message.equals("Service my_custom_service was not found or is unsupported")); } + + @Test + public void dynamicTheming() throws ClassNotFoundException { + LayoutPullParser parser = LayoutPullParser.createFromPath("/empty.xml"); + LayoutLibTestCallback layoutLibCallback = + new LayoutLibTestCallback(getLogger(), mDefaultClassLoader); + layoutLibCallback.initResources(); + SessionParams params = getSessionParamsBuilder() + .setParser(parser) + .setCallback(layoutLibCallback) + .setTheme("Theme.Material", false) + .build(); + DisplayMetrics metrics = new DisplayMetrics(); + Configuration configuration = RenderAction.getConfiguration(params); + BridgeContext context = new BridgeContext(params.getProjectKey(), metrics, params.getResources(), + params.getAssets(), params.getLayoutlibCallback(), configuration, + params.getTargetSdkVersion(), params.isRtlSupported()); + context.initResources(params.getAssets()); + try { + assertEquals(-13749965, context.getResources().getColor(android.R.color.system_neutral1_800, null)); + + Bitmap wallpaper = BitmapFactory.decodeStream( + getClass().getResourceAsStream( + "/com/android/layoutlib/testdata/wallpaper1.webp" + )); + Map<String, Integer> dynamicColorMap = + DynamicRenderResources.createDynamicColorMap(wallpaper, + configuration.isNightModeActive()); + ((DynamicRenderResources)context.getRenderResources()).setDynamicColorMap(dynamicColorMap); + assertEquals(-13226195, context.getResources().getColor(android.R.color.system_neutral1_800, null)); + + wallpaper = BitmapFactory.decodeStream( + getClass().getResourceAsStream( + "/com/android/layoutlib/testdata/wallpaper2.webp" + )); + dynamicColorMap = DynamicRenderResources.createDynamicColorMap(wallpaper, + configuration.isNightModeActive()); + ((DynamicRenderResources)context.getRenderResources()).setDynamicColorMap(dynamicColorMap); + assertEquals(-13749969, context.getResources().getColor(android.R.color.system_neutral1_800, null)); + } finally { + context.disposeResources(); + } + } } diff --git a/bridge/tests/src/com/android/layoutlib/bridge/android/DynamicRenderResourcesTest.java b/bridge/tests/src/com/android/layoutlib/bridge/android/DynamicRenderResourcesTest.java new file mode 100644 index 0000000000..0f63a12790 --- /dev/null +++ b/bridge/tests/src/com/android/layoutlib/bridge/android/DynamicRenderResourcesTest.java @@ -0,0 +1,69 @@ +/* + * 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 com.android.layoutlib.bridge.android; + +import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.intensive.RenderTestBase; + +import org.junit.BeforeClass; +import org.junit.Test; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class DynamicRenderResourcesTest extends RenderTestBase { + @BeforeClass + public static void setUp() { + Bridge.prepareThread(); + } + + @Test + public void createDynamicTheme() { + Bitmap wallpaper = BitmapFactory.decodeStream( + getClass().getResourceAsStream( + "/com/android/layoutlib/testdata/wallpaper1.webp" + )); + Map<String, Integer> dynamicColorMap = + DynamicRenderResources.createDynamicColorMap(wallpaper, false); + assertNotNull(dynamicColorMap); + assertEquals(-16777216, (int)dynamicColorMap.get("system_accent1_0")); + assertEquals(-4632, (int)dynamicColorMap.get("system_accent1_50")); + assertEquals(-1403268, (int)dynamicColorMap.get("system_accent1_300")); + assertEquals(-11198451, (int)dynamicColorMap.get("system_accent1_800")); + assertEquals(-16777216, (int)dynamicColorMap.get("system_accent2_0")); + assertEquals(-4632, (int)dynamicColorMap.get("system_accent2_50")); + assertEquals(-3497321, (int)dynamicColorMap.get("system_accent2_300")); + assertEquals(-12309982, (int)dynamicColorMap.get("system_accent2_800")); + assertEquals(-16777216, (int)dynamicColorMap.get("system_accent3_0")); + assertEquals(-3900, (int)dynamicColorMap.get("system_accent3_50")); + assertEquals(-4478092, (int)dynamicColorMap.get("system_accent3_300")); + assertEquals(-12963835, (int)dynamicColorMap.get("system_accent3_800")); + assertEquals(-16777216, (int)dynamicColorMap.get("system_neutral1_0")); + assertEquals(-266518, (int)dynamicColorMap.get("system_neutral1_50")); + assertEquals(-4937306, (int)dynamicColorMap.get("system_neutral1_300")); + assertEquals(-13226195, (int)dynamicColorMap.get("system_neutral1_800")); + assertEquals(-16777216, (int)dynamicColorMap.get("system_neutral2_0")); + assertEquals(-4632, (int)dynamicColorMap.get("system_neutral2_50")); + assertEquals(-4413535, (int)dynamicColorMap.get("system_neutral2_300")); + assertEquals(-12899031, (int)dynamicColorMap.get("system_neutral2_800")); + } +} diff --git a/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTests.java b/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTests.java index 7cbf777956..ead08aeff0 100644 --- a/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTests.java +++ b/bridge/tests/src/com/android/layoutlib/bridge/intensive/RenderTests.java @@ -28,6 +28,7 @@ import com.android.ide.common.rendering.api.XmlParserFactory; import com.android.internal.R; import com.android.internal.lang.System_Delegate; import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.layoutlib.bridge.android.DynamicRenderResources; import com.android.layoutlib.bridge.android.RenderParamsFlags; import com.android.layoutlib.bridge.impl.ParserFactory; import com.android.layoutlib.bridge.impl.RenderAction; @@ -54,6 +55,8 @@ import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.Resources_Delegate; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Color; import android.util.DisplayMetrics; import android.util.StateSet; @@ -67,10 +70,15 @@ import java.awt.image.BufferedImage; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.io.PrintWriter; import java.lang.reflect.Field; +import java.nio.file.StandardCopyOption; import java.util.concurrent.TimeUnit; +import com.google.common.io.Files; + import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -2011,4 +2019,69 @@ public class RenderTests extends RenderTestBase { renderAndVerify(params, "window_background.png", TimeUnit.SECONDS.toNanos(2)); } + + @Test + public void testThemedAdaptiveIcon() throws ClassNotFoundException, IOException { + // Create the layout pull parser. + String layout = + "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + + " android:padding=\"16dp\"\n" + + " android:orientation=\"horizontal\"\n" + + " android:layout_width=\"fill_parent\"\n" + + " android:layout_height=\"fill_parent\">\n" + + " <ImageView\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:layout_width=\"wrap_content\"\n" + + " android:src=\"@drawable/adaptive\" />\n" + + "</LinearLayout>\n"; + LayoutPullParser parser = LayoutPullParser.createFromString(layout); + // Create LayoutLibCallback. + LayoutLibTestCallback layoutLibCallback = + new LayoutLibTestCallback(getLogger(), mDefaultClassLoader); + layoutLibCallback.initResources(); + SessionParams params = getSessionParamsBuilder() + .setParser(LayoutPullParser.createFromString(layout)) + .setCallback(layoutLibCallback) + .setTheme("Theme.Material.NoActionBar.Fullscreen", false) + .setRenderingMode(RenderingMode.V_SCROLL) + .build(); + params.setFlag(RenderParamsFlags.FLAG_KEY_ADAPTIVE_ICON_MASK_PATH, + "M50 0C77.6 0 100 22.4 100 50C100 77.6 77.6 100 50 100C22.4 100 0 77.6 0 50C0 " + + "22.4 22.4 0 50 0Z"); + renderAndVerify(params, "adaptive_icon_circle.png"); + + File w1 = File.createTempFile("wallpaper1", ".webp"); + try (InputStream inputStream = getClass().getResourceAsStream( + "/com/android/layoutlib/testdata/wallpaper1.webp")) { + java.nio.file.Files.copy(inputStream, w1.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + params = getSessionParamsBuilder() + .setParser(LayoutPullParser.createFromString(layout)) + .setCallback(layoutLibCallback) + .setTheme("Theme.Material.NoActionBar.Fullscreen", false) + .setRenderingMode(RenderingMode.V_SCROLL) + .build(); + params.setFlag(RenderParamsFlags.FLAG_KEY_ADAPTIVE_ICON_MASK_PATH, + "M50 0C77.6 0 100 22.4 100 50C100 77.6 77.6 100 50 100C22.4 100 0 77.6 0 50C0 " + + "22.4 22.4 0 50 0Z"); + params.setFlag(RenderParamsFlags.FLAG_KEY_WALLPAPER_PATH, w1.getPath()); + renderAndVerify(params, "adaptive_icon_dynamic_orange.png"); + + File w2 = File.createTempFile("wallpaper2", ".webp"); + try (InputStream inputStream = getClass().getResourceAsStream( + "/com/android/layoutlib/testdata/wallpaper2.webp")) { + java.nio.file.Files.copy(inputStream, w2.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + params = getSessionParamsBuilder() + .setParser(LayoutPullParser.createFromString(layout)) + .setCallback(layoutLibCallback) + .setTheme("Theme.Material.NoActionBar.Fullscreen", false) + .setRenderingMode(RenderingMode.V_SCROLL) + .build(); + params.setFlag(RenderParamsFlags.FLAG_KEY_ADAPTIVE_ICON_MASK_PATH, + "M50 0C77.6 0 100 22.4 100 50C100 77.6 77.6 100 50 100C22.4 100 0 77.6 0 50C0 " + + "22.4 22.4 0 50 0Z"); + params.setFlag(RenderParamsFlags.FLAG_KEY_WALLPAPER_PATH, w2.getPath()); + renderAndVerify(params, "adaptive_icon_dynamic_green.png"); + } } diff --git a/common/src/com/android/tools/layoutlib/create/NativeConfig.java b/common/src/com/android/tools/layoutlib/create/NativeConfig.java index 41df7c97f9..70146428e7 100644 --- a/common/src/com/android/tools/layoutlib/create/NativeConfig.java +++ b/common/src/com/android/tools/layoutlib/create/NativeConfig.java @@ -87,6 +87,7 @@ public class NativeConfig { "android.graphics.Canvas#getClipBounds", "android.graphics.ImageDecoder#decodeBitmapImpl", "android.graphics.Typeface#create", + "android.graphics.drawable.AdaptiveIconDrawable#draw", "android.graphics.drawable.AnimatedVectorDrawable#draw", "android.graphics.drawable.AnimatedVectorDrawable$VectorDrawableAnimatorUI#onDraw", "android.graphics.drawable.DrawableInflater#inflateFromClass", diff --git a/create/src/com/android/tools/layoutlib/create/CreateInfo.java b/create/src/com/android/tools/layoutlib/create/CreateInfo.java index e4c6993d43..1badaf60a7 100644 --- a/create/src/com/android/tools/layoutlib/create/CreateInfo.java +++ b/create/src/com/android/tools/layoutlib/create/CreateInfo.java @@ -300,6 +300,7 @@ public final class CreateInfo implements ICreateInfo { new String[] { "android.preference.PreferenceActivity", "java.**", + "kotlin.**", "org.kxml2.io.KXmlParser", "org.xmlpull.**", "sun.**", diff --git a/create/src/com/android/tools/layoutlib/create/Main.java b/create/src/com/android/tools/layoutlib/create/Main.java index a32efafdb6..2de360b688 100644 --- a/create/src/com/android/tools/layoutlib/create/Main.java +++ b/create/src/com/android/tools/layoutlib/create/Main.java @@ -132,6 +132,7 @@ public class Main { "com.android.internal.util.*", "com.android.internal.view.menu.ActionMenu", "com.android.internal.widget.*", + "com.android.systemui.monet.*", // needed for dynamic theming "com.google.android.apps.common.testing.accessibility.**", "com.google.android.libraries.accessibility.**", "libcore.icu.ICU", // needed by ICU_Delegate in LayoutLib |