SilkFX HDR demos

Test: this
Bug: 266628247
Change-Id: Ib41de2de8634e03dd6f7f6b3909e4e109793fa31
diff --git a/libs/hwui/effects/GainmapRenderer.cpp b/libs/hwui/effects/GainmapRenderer.cpp
index a544ae8..8977d3c 100644
--- a/libs/hwui/effects/GainmapRenderer.cpp
+++ b/libs/hwui/effects/GainmapRenderer.cpp
@@ -48,10 +48,7 @@
         gainmapSrc.fRight *= sX;
         gainmapSrc.fTop *= sY;
         gainmapSrc.fBottom *= sY;
-        // TODO: Temporary workaround for SkGainmapShader::Make not having a const variant
-        sk_sp<SkImage> mutImage = sk_ref_sp(const_cast<SkImage*>(image.get()));
-        sk_sp<SkImage> mutGainmap = sk_ref_sp(const_cast<SkImage*>(gainmapImage.get()));
-        auto shader = SkGainmapShader::Make(mutImage, src, sampling, mutGainmap, gainmapSrc,
+        auto shader = SkGainmapShader::Make(image, src, sampling, gainmapImage, gainmapSrc,
                                             sampling, gainmapInfo, dst, targetSdrHdrRatio,
                                             c->imageInfo().refColorSpace());
         gainmapPaint.setShader(shader);
diff --git a/tests/SilkFX/Android.bp b/tests/SilkFX/Android.bp
index 088d9a2..1e467db 100644
--- a/tests/SilkFX/Android.bp
+++ b/tests/SilkFX/Android.bp
@@ -25,13 +25,17 @@
 
 android_test {
     name: "SilkFX",
-    srcs: ["**/*.java", "**/*.kt"],
+    srcs: [
+        "**/*.java",
+        "**/*.kt",
+    ],
     platform_apis: true,
     certificate: "platform",
-        static_libs: [
+    static_libs: [
         "androidx.core_core",
         "androidx.appcompat_appcompat",
         "com.google.android.material_material",
         "androidx-constraintlayout_constraintlayout",
+        "subsampling-scale-image-view",
     ],
 }
diff --git a/tests/SilkFX/AndroidManifest.xml b/tests/SilkFX/AndroidManifest.xml
index 21256d8..1b8e08b 100644
--- a/tests/SilkFX/AndroidManifest.xml
+++ b/tests/SilkFX/AndroidManifest.xml
@@ -55,5 +55,15 @@
             android:exported="true">
         </activity>
 
+        <activity android:name=".app.HdrImageViewer"
+            android:label="HDR Gainmap Image Viewer"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW"/>
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:mimeType="image/*"/>
+            </intent-filter>
+        </activity>
+
     </application>
 </manifest>
diff --git a/tests/SilkFX/assets/gainmaps/cave.jpg b/tests/SilkFX/assets/gainmaps/cave.jpg
new file mode 100644
index 0000000..fa7d3fc3
--- /dev/null
+++ b/tests/SilkFX/assets/gainmaps/cave.jpg
Binary files differ
diff --git a/tests/SilkFX/res/drawable-nodpi/blue_sweep_gradient.xml b/tests/SilkFX/res/drawable-nodpi/blue_sweep_gradient.xml
new file mode 100644
index 0000000..c183c5d
--- /dev/null
+++ b/tests/SilkFX/res/drawable-nodpi/blue_sweep_gradient.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <gradient
+        android:startColor="#000000"
+        android:endColor="#0000FF"
+        android:angle="0"/>
+</shape>
\ No newline at end of file
diff --git a/tests/SilkFX/res/drawable-nodpi/dark_gradient.xml b/tests/SilkFX/res/drawable-nodpi/dark_gradient.xml
new file mode 100644
index 0000000..d2653d0
--- /dev/null
+++ b/tests/SilkFX/res/drawable-nodpi/dark_gradient.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <gradient
+        android:startColor="#000000"
+        android:endColor="#181818"
+        android:angle="0"/>
+</shape>
\ No newline at end of file
diff --git a/tests/SilkFX/res/drawable-nodpi/green_sweep_gradient.xml b/tests/SilkFX/res/drawable-nodpi/green_sweep_gradient.xml
new file mode 100644
index 0000000..c600d0f
--- /dev/null
+++ b/tests/SilkFX/res/drawable-nodpi/green_sweep_gradient.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <gradient
+        android:startColor="#000000"
+        android:endColor="#00FF00"
+        android:angle="0"/>
+</shape>
\ No newline at end of file
diff --git a/tests/SilkFX/res/drawable-nodpi/grey_sweep_gradient.xml b/tests/SilkFX/res/drawable-nodpi/grey_sweep_gradient.xml
new file mode 100644
index 0000000..d0c17fa
--- /dev/null
+++ b/tests/SilkFX/res/drawable-nodpi/grey_sweep_gradient.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <gradient
+        android:startColor="#000000"
+        android:endColor="#FFFFFF"
+        android:angle="0"/>
+</shape>
\ No newline at end of file
diff --git a/tests/SilkFX/res/drawable-nodpi/light_gradient.xml b/tests/SilkFX/res/drawable-nodpi/light_gradient.xml
new file mode 100644
index 0000000..c75f925
--- /dev/null
+++ b/tests/SilkFX/res/drawable-nodpi/light_gradient.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <gradient
+        android:startColor="#E8E8E8"
+        android:endColor="#FFFFFF"
+        android:angle="0"/>
+</shape>
\ No newline at end of file
diff --git a/tests/SilkFX/res/drawable-nodpi/red_sweep_gradient.xml b/tests/SilkFX/res/drawable-nodpi/red_sweep_gradient.xml
new file mode 100644
index 0000000..e3b834a
--- /dev/null
+++ b/tests/SilkFX/res/drawable-nodpi/red_sweep_gradient.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <gradient
+        android:startColor="#000000"
+        android:endColor="#FF0000"
+        android:angle="0"/>
+</shape>
\ No newline at end of file
diff --git a/tests/SilkFX/res/layout/color_grid.xml b/tests/SilkFX/res/layout/color_grid.xml
new file mode 100644
index 0000000..37242ee
--- /dev/null
+++ b/tests/SilkFX/res/layout/color_grid.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 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.
+  -->
+
+<com.android.test.silkfx.hdr.ColorGrid xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" />
diff --git a/tests/SilkFX/res/layout/common_base.xml b/tests/SilkFX/res/layout/common_base.xml
index 944c684..c0eaf9b 100644
--- a/tests/SilkFX/res/layout/common_base.xml
+++ b/tests/SilkFX/res/layout/common_base.xml
@@ -24,16 +24,6 @@
 
     <FrameLayout android:id="@+id/demo_container"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content" />
-
-    <View
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1" />
-
-    <com.android.test.silkfx.common.HDRIndicator
-        android:layout_width="match_parent"
-        android:layout_height="50dp"
-        android:layout_margin="8dp" />
+        android:layout_height="match_parent" />
 
 </LinearLayout>
\ No newline at end of file
diff --git a/tests/SilkFX/res/layout/gainmap_decode_test.xml b/tests/SilkFX/res/layout/gainmap_decode_test.xml
new file mode 100644
index 0000000..e7ef61f8
--- /dev/null
+++ b/tests/SilkFX/res/layout/gainmap_decode_test.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<com.android.test.silkfx.hdr.GainmapDecodeTest xmlns:android="http://schemas.android.com/apk/res/android"
+                                               android:layout_width="match_parent"
+                                               android:layout_height="match_parent"
+                                               android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <Button
+            android:id="@+id/decode_full"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="Decode full" />
+
+        <Button
+            android:id="@+id/decode_subsampled4"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="Decode subsampled (1/4th)" />
+
+        <Button
+            android:id="@+id/decode_scaled66"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="Decode scaled (66%)" />
+
+        <Button
+            android:id="@+id/decode_crop"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="Decode croppedSquare" />
+
+        <Button
+            android:id="@+id/decode_cropScaled33"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="Decode croppedSquare(33%)" />
+
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/source_info"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="horizontal">
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:orientation="vertical"
+            android:layout_weight="1">
+
+            <TextView
+                android:id="@+id/sdr_label"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+
+            <ImageView
+                android:id="@+id/sdr_source"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_margin="8dp"
+                android:scaleType="fitStart" />
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:orientation="vertical"
+            android:layout_weight="1">
+
+            <TextView
+                android:id="@+id/gainmap_label"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content" />
+
+            <ImageView
+                android:id="@+id/gainmap"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:layout_margin="8dp"
+                android:scaleType="fitStart" />
+        </LinearLayout>
+
+    </LinearLayout>
+
+</com.android.test.silkfx.hdr.GainmapDecodeTest>
\ No newline at end of file
diff --git a/tests/SilkFX/res/layout/gainmap_image.xml b/tests/SilkFX/res/layout/gainmap_image.xml
new file mode 100644
index 0000000..89bbb70
--- /dev/null
+++ b/tests/SilkFX/res/layout/gainmap_image.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<com.android.test.silkfx.hdr.GainmapImage xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:id="@+id/gainmap_image">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <RadioGroup android:id="@+id/output_mode"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+            <RadioButton android:id="@+id/output_sdr"
+                         android:layout_width="wrap_content"
+                         android:layout_weight="1"
+                         android:layout_height="wrap_content"
+                         android:text="SDR original" />
+
+            <RadioButton android:id="@+id/output_gainmap"
+                         android:layout_width="wrap_content"
+                         android:layout_weight="1"
+                         android:layout_height="wrap_content"
+                         android:text="Gainmap" />
+
+            <RadioButton android:id="@+id/output_hdr"
+                         android:layout_width="wrap_content"
+                         android:layout_weight="1"
+                         android:layout_height="wrap_content"
+                         android:text="HDR (sdr+gainmap)" />
+        </RadioGroup>
+
+        <Spinner
+            android:id="@+id/image_selection"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+
+        <TextView
+            android:id="@+id/error_msg"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:visibility="gone" />
+
+        <com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
+            android:id="@+id/image"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+
+    </LinearLayout>
+
+</com.android.test.silkfx.hdr.GainmapImage>
\ No newline at end of file
diff --git a/tests/SilkFX/res/layout/gradient_sweep.xml b/tests/SilkFX/res/layout/gradient_sweep.xml
new file mode 100644
index 0000000..261022a
--- /dev/null
+++ b/tests/SilkFX/res/layout/gradient_sweep.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical">
+
+    <View android:background="@drawable/dark_gradient"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1" />
+
+    <View android:background="@drawable/light_gradient"
+          android:layout_width="match_parent"
+          android:layout_height="0dp"
+          android:layout_weight="1" />
+
+    <View android:background="@drawable/grey_sweep_gradient"
+          android:layout_width="match_parent"
+          android:layout_height="0dp"
+          android:layout_weight="1" />
+
+    <View android:background="@drawable/red_sweep_gradient"
+          android:layout_width="match_parent"
+          android:layout_height="0dp"
+          android:layout_weight="1" />
+
+    <View android:background="@drawable/green_sweep_gradient"
+          android:layout_width="match_parent"
+          android:layout_height="0dp"
+          android:layout_weight="1" />
+
+    <View android:background="@drawable/blue_sweep_gradient"
+          android:layout_width="match_parent"
+          android:layout_height="0dp"
+          android:layout_weight="1" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/tests/SilkFX/res/layout/hdr_image_viewer.xml b/tests/SilkFX/res/layout/hdr_image_viewer.xml
new file mode 100644
index 0000000..9816430
--- /dev/null
+++ b/tests/SilkFX/res/layout/hdr_image_viewer.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"
+              android:orientation="vertical">
+
+    <include layout="@layout/color_mode_controls" />
+    <include layout="@layout/gainmap_image" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/tests/SilkFX/src/com/android/test/silkfx/Main.kt b/tests/SilkFX/src/com/android/test/silkfx/Main.kt
index 7132ae8..a6cdbb9 100644
--- a/tests/SilkFX/src/com/android/test/silkfx/Main.kt
+++ b/tests/SilkFX/src/com/android/test/silkfx/Main.kt
@@ -26,6 +26,7 @@
 import android.widget.ExpandableListView
 import android.widget.TextView
 import com.android.test.silkfx.app.CommonDemoActivity
+import com.android.test.silkfx.app.EXTRA_COMMON_CONTROLS
 import com.android.test.silkfx.app.EXTRA_LAYOUT
 import com.android.test.silkfx.app.EXTRA_TITLE
 import com.android.test.silkfx.hdr.GlowActivity
@@ -37,10 +38,11 @@
     constructor(name: String, activity: KClass<out Activity>) : this(name, { context ->
         Intent(context, activity.java)
     })
-    constructor(name: String, layout: Int) : this(name, { context ->
+    constructor(name: String, layout: Int, commonControls: Boolean = true) : this(name, { context ->
         Intent(context, CommonDemoActivity::class.java).apply {
             putExtra(EXTRA_LAYOUT, layout)
             putExtra(EXTRA_TITLE, name)
+            putExtra(EXTRA_COMMON_CONTROLS, commonControls)
         }
     })
 }
@@ -49,7 +51,11 @@
 private val AllDemos = listOf(
         DemoGroup("HDR", listOf(
                 Demo("Glow", GlowActivity::class),
-                Demo("Blingy Notifications", R.layout.bling_notifications)
+                Demo("Blingy Notifications", R.layout.bling_notifications),
+                Demo("Color Grid", R.layout.color_grid),
+                Demo("Gradient Sweep", R.layout.gradient_sweep),
+                Demo("Gainmap Image", R.layout.gainmap_image),
+                Demo("Gainmap Decode Test", R.layout.gainmap_decode_test, commonControls = false)
         )),
         DemoGroup("Materials", listOf(
                 Demo("Glass", GlassActivity::class),
diff --git a/tests/SilkFX/src/com/android/test/silkfx/app/CommonDemoActivity.kt b/tests/SilkFX/src/com/android/test/silkfx/app/CommonDemoActivity.kt
index e0a0a20..e56ce40 100644
--- a/tests/SilkFX/src/com/android/test/silkfx/app/CommonDemoActivity.kt
+++ b/tests/SilkFX/src/com/android/test/silkfx/app/CommonDemoActivity.kt
@@ -22,6 +22,7 @@
 
 const val EXTRA_LAYOUT = "layout"
 const val EXTRA_TITLE = "title"
+const val EXTRA_COMMON_CONTROLS = "common_controls"
 
 class CommonDemoActivity : BaseDemoActivity() {
 
@@ -38,8 +39,13 @@
         val title = extras.getString(EXTRA_TITLE, "SilkFX")
         window.setTitle(title)
 
-        setContentView(R.layout.common_base)
+        if (extras.getBoolean(EXTRA_COMMON_CONTROLS, true)) {
+            setContentView(R.layout.common_base)
+            LayoutInflater.from(this).inflate(layout, findViewById(R.id.demo_container), true)
+        } else {
+            setContentView(layout)
+        }
+
         actionBar?.title = title
-        LayoutInflater.from(this).inflate(layout, findViewById(R.id.demo_container), true)
     }
 }
\ No newline at end of file
diff --git a/tests/SilkFX/src/com/android/test/silkfx/app/HdrImageViewer.kt b/tests/SilkFX/src/com/android/test/silkfx/app/HdrImageViewer.kt
new file mode 100644
index 0000000..f16f0e4
--- /dev/null
+++ b/tests/SilkFX/src/com/android/test/silkfx/app/HdrImageViewer.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 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.test.silkfx.app
+
+import android.content.Intent
+import android.graphics.ImageDecoder
+import android.os.Bundle
+import com.android.test.silkfx.R
+import com.android.test.silkfx.hdr.GainmapImage
+
+class HdrImageViewer : BaseDemoActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.hdr_image_viewer)
+
+        val intent = this.intent ?: return finish()
+        if (Intent.ACTION_VIEW != intent.action) {
+            finish()
+            return
+        }
+
+        val data = intent.data ?: return finish()
+
+        val source = ImageDecoder.createSource(contentResolver, data)
+        findViewById<GainmapImage>(R.id.gainmap_image)!!.setImageSource(source)
+    }
+}
\ No newline at end of file
diff --git a/tests/SilkFX/src/com/android/test/silkfx/common/BaseDrawingView.kt b/tests/SilkFX/src/com/android/test/silkfx/common/BaseDrawingView.kt
index 4b85953..f88e6b0 100644
--- a/tests/SilkFX/src/com/android/test/silkfx/common/BaseDrawingView.kt
+++ b/tests/SilkFX/src/com/android/test/silkfx/common/BaseDrawingView.kt
@@ -23,8 +23,11 @@
 import android.view.View
 
 open class BaseDrawingView : View {
+    val sRGB = ColorSpace.get(ColorSpace.Named.SRGB)
+    val displayP3 = ColorSpace.get(ColorSpace.Named.DISPLAY_P3)
     val scRGB = ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)
     val bt2020 = ColorSpace.get(ColorSpace.Named.BT2020)
+    val bt2020_pq = ColorSpace.get(ColorSpace.Named.BT2020_PQ)
     val lab = ColorSpace.get(ColorSpace.Named.CIE_LAB)
 
     val density: Float
diff --git a/tests/SilkFX/src/com/android/test/silkfx/hdr/ColorGrid.kt b/tests/SilkFX/src/com/android/test/silkfx/hdr/ColorGrid.kt
new file mode 100644
index 0000000..6920f83
--- /dev/null
+++ b/tests/SilkFX/src/com/android/test/silkfx/hdr/ColorGrid.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 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.test.silkfx.hdr
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.ColorSpace
+import android.graphics.Paint
+import android.graphics.Rect
+import android.util.AttributeSet
+import com.android.test.silkfx.common.BaseDrawingView
+
+class ColorGrid(context: Context, attrs: AttributeSet?) : BaseDrawingView(context, attrs) {
+
+    init {
+        isClickable = true
+        setOnClickListener {
+            invalidate()
+        }
+    }
+
+    fun toMaxColor(color: Int, colorspace: ColorSpace): Long {
+        val red = (Color.red(color) / 255f) * colorspace.getMaxValue(0)
+        val green = (Color.green(color) / 255f) * colorspace.getMaxValue(1)
+        val blue = (Color.blue(color) / 255f) * colorspace.getMaxValue(2)
+        val alpha = Color.alpha(color) / 255f
+        return Color.pack(red, green, blue, alpha, colorspace)
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        super.onDraw(canvas)
+
+        val paint = Paint()
+        paint.isDither = true
+        paint.isAntiAlias = true
+        paint.textSize = 18.dp()
+        paint.textAlign = Paint.Align.LEFT
+
+        val labels = arrayOf("sRGB", "Display P3", "BT2020_PQ", "scRGB(max)")
+        val colorSpaces = arrayOf(sRGB, displayP3, bt2020_pq, scRGB)
+
+        val colWidth = width / colorSpaces.size.toFloat()
+        val rowHeight = minOf((height - 20.dp()) / 4f, colWidth)
+
+        val dest = Rect(0, 0, rowHeight.toInt(), colWidth.toInt())
+
+        for (colIndex in labels.indices) {
+            canvas.save()
+            canvas.translate(colIndex * colWidth, 20.dp())
+
+            paint.color = Color.WHITE
+            canvas.drawText(labels[colIndex], 0f, 1f, paint)
+
+            arrayOf(Color.WHITE, Color.RED, Color.BLUE, Color.GREEN).forEach {
+                paint.setColor(toMaxColor(it, colorSpaces[colIndex]))
+                canvas.drawRect(dest, paint)
+                canvas.translate(0f, rowHeight)
+            }
+            canvas.restore()
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapDecodeTest.kt b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapDecodeTest.kt
new file mode 100644
index 0000000..db812ac
--- /dev/null
+++ b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapDecodeTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 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.test.silkfx.hdr
+
+import android.content.Context
+import android.graphics.ImageDecoder
+import android.graphics.Rect
+import android.util.AttributeSet
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.android.test.silkfx.R
+
+enum class DecodeMode {
+    Full,
+    Subsampled4,
+    Scaled66,
+    CropedSquared,
+    CropedSquaredScaled33
+}
+
+class GainmapDecodeTest(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) {
+
+    private fun decode(mode: DecodeMode) {
+        val source = ImageDecoder.createSource(resources.assets,
+            "gainmaps/cave.jpg")
+
+        val sourceInfo = findViewById<TextView>(R.id.source_info)!!
+
+        val gainmapImage = ImageDecoder.decodeBitmap(source) { decoder, info, source ->
+            decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
+            sourceInfo.text =
+                "Original size ${info.size.width}x${info.size.height}; mime-type = ${info.mimeType}"
+
+            when (mode) {
+                DecodeMode.Full -> {}
+                DecodeMode.Subsampled4 -> {
+                    decoder.setTargetSampleSize(4)
+                }
+                DecodeMode.Scaled66 -> {
+                    val size = info.size
+                    decoder.setTargetSize((size.width * .66).toInt(), (size.height * .66).toInt())
+                }
+                DecodeMode.CropedSquared -> {
+                    val dimen = minOf(info.size.width, info.size.height)
+                    decoder.crop = Rect(50, 50, dimen - 100, dimen - 100)
+                }
+                DecodeMode.CropedSquaredScaled33 -> {
+                    val size = info.size
+                    val targetWidth = (size.width * .33).toInt()
+                    val targetHeight = (size.height * .33).toInt()
+                    decoder.setTargetSize(targetWidth, targetHeight)
+                    val dimen = minOf(targetWidth, targetHeight)
+                    decoder.crop = Rect(50, 50, dimen - 100, dimen - 100)
+                }
+            }
+        }
+
+        val gainmapContents = gainmapImage.gainmap!!.gainmapContents!!
+        val sdrBitmap = gainmapImage.also { it.gainmap = null }
+
+        findViewById<ImageView>(R.id.sdr_source)!!.setImageBitmap(sdrBitmap)
+        findViewById<TextView>(R.id.sdr_label)!!.text =
+            "SDR Size: ${sdrBitmap.width}x${sdrBitmap.height}"
+
+        findViewById<ImageView>(R.id.gainmap)!!.setImageBitmap(gainmapContents)
+        findViewById<TextView>(R.id.gainmap_label)!!.text =
+            "Gainmap Size: ${gainmapContents.width}x${gainmapContents.height}"
+    }
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        decode(DecodeMode.Full)
+
+        findViewById<Button>(R.id.decode_full)!!.setOnClickListener {
+            decode(DecodeMode.Full)
+        }
+        findViewById<Button>(R.id.decode_subsampled4)!!.setOnClickListener {
+            decode(DecodeMode.Subsampled4)
+        }
+        findViewById<Button>(R.id.decode_scaled66)!!.setOnClickListener {
+            decode(DecodeMode.Scaled66)
+        }
+        findViewById<Button>(R.id.decode_crop)!!.setOnClickListener {
+            decode(DecodeMode.CropedSquared)
+        }
+        findViewById<Button>(R.id.decode_cropScaled33)!!.setOnClickListener {
+            decode(DecodeMode.CropedSquaredScaled33)
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt
new file mode 100644
index 0000000..3875644
--- /dev/null
+++ b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2023 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.test.silkfx.hdr
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.Gainmap
+import android.graphics.ImageDecoder
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.view.View
+import android.widget.AdapterView
+import android.widget.ArrayAdapter
+import android.widget.FrameLayout
+import android.widget.RadioGroup
+import android.widget.Spinner
+import android.widget.TextView
+import com.android.test.silkfx.R
+import com.davemorrissey.labs.subscaleview.ImageSource
+import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
+
+class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) {
+
+    private val gainmapImages: Array<String>
+    private var selectedImage = -1
+    private var outputMode = R.id.output_hdr
+    private var bitmap: Bitmap? = null
+    private var gainmap: Gainmap? = null
+    private var gainmapVisualizer: Bitmap? = null
+    private lateinit var imageView: SubsamplingScaleImageView
+
+    init {
+        gainmapImages = context.assets.list("gainmaps")!!
+    }
+
+    fun setImageSource(source: ImageDecoder.Source) {
+        findViewById<Spinner>(R.id.image_selection)!!.visibility = View.GONE
+        doDecode(source)
+    }
+
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+
+        imageView = findViewById(R.id.image)!!
+
+        findViewById<RadioGroup>(R.id.output_mode)!!.also {
+            it.check(outputMode)
+            it.setOnCheckedChangeListener { _, checkedId ->
+                val previousMode = outputMode
+                outputMode = checkedId
+                if (previousMode == R.id.output_sdr && checkedId == R.id.output_hdr) {
+                    animateToHdr()
+                } else if (previousMode == R.id.output_hdr && checkedId == R.id.output_sdr) {
+                    animateToSdr()
+                } else {
+                    updateDisplay()
+                }
+            }
+        }
+
+        val spinner = findViewById<Spinner>(R.id.image_selection)!!
+        val adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, gainmapImages)
+        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
+        spinner.adapter = adapter
+        spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
+            override fun onItemSelected(
+                parent: AdapterView<*>?,
+                view: View?,
+                position: Int,
+                id: Long
+            ) {
+                setImage(position)
+            }
+
+            override fun onNothingSelected(parent: AdapterView<*>?) {
+            }
+        }
+
+        setImage(gainmapImages.indexOf("cave.jpg"))
+
+        imageView.apply {
+            isClickable = true
+            setOnClickListener {
+                animate().alpha(.5f).withEndAction {
+                    animate().alpha(1f).start()
+                }.start()
+            }
+        }
+    }
+
+    private fun setImage(position: Int) {
+        if (selectedImage == position) return
+        selectedImage = position
+        val source = ImageDecoder.createSource(resources.assets,
+            "gainmaps/${gainmapImages[position]}")
+        doDecode(source)
+    }
+
+    private fun doDecode(source: ImageDecoder.Source) {
+        gainmap = null
+        bitmap = ImageDecoder.decodeBitmap(source) { decoder, info, source ->
+            decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
+        }
+        if (!bitmap!!.hasGainmap()) {
+            outputMode = R.id.output_sdr
+            findViewById<TextView>(R.id.error_msg)!!.also {
+                it.visibility = View.VISIBLE
+                it.text = "Image doesn't have a gainmap, only showing in SDR"
+            }
+            findViewById<RadioGroup>(R.id.output_mode)!!.also {
+                it.check(R.id.output_sdr)
+                it.visibility = View.GONE
+            }
+        } else {
+            findViewById<TextView>(R.id.error_msg)!!.visibility = View.GONE
+            findViewById<RadioGroup>(R.id.output_mode)!!.visibility = View.VISIBLE
+
+            gainmap = bitmap!!.gainmap
+            val map = gainmap!!.gainmapContents
+            if (map.config != Bitmap.Config.ALPHA_8) {
+                gainmapVisualizer = map
+            } else {
+                gainmapVisualizer = Bitmap.createBitmap(map.width, map.height,
+                    Bitmap.Config.ARGB_8888)
+                val canvas = Canvas(gainmapVisualizer!!)
+                val paint = Paint()
+                paint.colorFilter = ColorMatrixColorFilter(
+                    floatArrayOf(
+                        0f, 0f, 0f, 1f, 0f,
+                        0f, 0f, 0f, 1f, 0f,
+                        0f, 0f, 0f, 1f, 0f,
+                        0f, 0f, 0f, 0f, 255f
+                    )
+                )
+                canvas.drawBitmap(map, 0f, 0f, paint)
+                canvas.setBitmap(null)
+            }
+        }
+
+        updateDisplay()
+    }
+
+    private fun animateToHdr() {
+        if (bitmap == null || gainmap == null) return
+
+        // TODO: Trigger an animation
+        updateDisplay()
+    }
+
+    private fun animateToSdr() {
+        if (bitmap == null) return
+
+        // TODO: Trigger an animation
+        updateDisplay()
+    }
+
+    private fun updateDisplay() {
+        if (bitmap == null) return
+
+        imageView.setImage(ImageSource.cachedBitmap(when (outputMode) {
+            R.id.output_hdr -> { bitmap!!.gainmap = gainmap; bitmap!! }
+            R.id.output_sdr -> { bitmap!!.gainmap = null; bitmap!! }
+            R.id.output_gainmap -> gainmapVisualizer!!
+            else -> throw IllegalStateException()
+        }))
+    }
+}
\ No newline at end of file
diff --git a/tests/SilkFX/src/com/android/test/silkfx/hdr/RadialGlow.kt b/tests/SilkFX/src/com/android/test/silkfx/hdr/RadialGlow.kt
index 599585e..20acb49 100644
--- a/tests/SilkFX/src/com/android/test/silkfx/hdr/RadialGlow.kt
+++ b/tests/SilkFX/src/com/android/test/silkfx/hdr/RadialGlow.kt
@@ -26,9 +26,7 @@
 import com.android.test.silkfx.common.BaseDrawingView
 import kotlin.math.min
 
-class RadialGlow : BaseDrawingView {
-
-    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+class RadialGlow(context: Context, attrs: AttributeSet?) : BaseDrawingView(context, attrs) {
 
     var glowToggle = false