Use material3's dynamic color schemes in App Header

Uses dynamicLightColorScheme and dynamicDarkColorScheme based on the
app's theme to style the header. This allows the App Header to use
light/dark variants of color tokens even if the app theme and system
theme are not aligned, whereas before, "inverse" tokens (resolved with
the system context) had to be used for apps not following the system
theme, which are much more limited than non-inverse tokens.

Bug: 328668781
Test: App Header of light app on dark system theme and viceversa
always follows the app theme and focus / unfocused headers are
differentiated

Change-Id: I9321726551a54c3ac1b1bf3fe12b2ee29dc7a5be
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index ef756c5..25d3067 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -206,6 +206,7 @@
         "androidx.core_core-animation",
         "androidx.core_core-ktx",
         "androidx.arch.core_core-runtime",
+        "androidx.compose.material3_material3",
         "androidx-constraintlayout_constraintlayout",
         "androidx.dynamicanimation_dynamicanimation",
         "androidx.recyclerview_recyclerview",
diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_header_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_header_background.xml
deleted file mode 100644
index 50c5ca9..0000000
--- a/libs/WindowManager/Shell/res/drawable/desktop_mode_header_background.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?><!--
-  ~ Copyright (C) 2024 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.
-  -->
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:id="@+id/backLayer">
-        <shape android:shape="rectangle">
-            <solid android:color="#000000" />
-        </shape>
-    </item>
-
-    <item android:id="@+id/frontLayer">
-        <shape android:shape="rectangle">
-            <solid android:color="#000000" />
-        </shape>
-    </item>
-</layer-list>
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
index 84e1449..7b31c14 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
@@ -19,7 +19,6 @@
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/desktop_mode_caption"
-    android:background="@drawable/desktop_mode_header_background"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:gravity="center_horizontal"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt
new file mode 100644
index 0000000..8e6d1ee9
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 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.wm.shell.windowdecor.common
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.content.Context
+import android.content.res.Configuration
+import android.content.res.Configuration.UI_MODE_NIGHT_MASK
+import android.graphics.Color
+
+/** The theme of a window decoration. */
+internal enum class Theme { LIGHT, DARK }
+
+/** Whether a [Theme] is light. */
+internal fun Theme.isLight(): Boolean = this == Theme.LIGHT
+
+/** Whether a [Theme] is dark. */
+internal fun Theme.isDark(): Boolean = this == Theme.DARK
+
+/**
+ * Utility class for determining themes based on system settings and app's [RunningTaskInfo].
+ */
+internal class DecorThemeUtil(private val context: Context) {
+
+    private val systemTheme: Theme
+        get() = if ((context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK) ==
+            Configuration.UI_MODE_NIGHT_YES) {
+            Theme.DARK
+        } else {
+            Theme.LIGHT
+        }
+
+    /**
+     * Returns the [Theme] used by the app with the given [RunningTaskInfo].
+     */
+    fun getAppTheme(task: RunningTaskInfo): Theme {
+        // TODO: use app's uiMode to find its actual light/dark value. It needs to be added to the
+        //   TaskInfo/TaskDescription.
+        val backgroundColor = task.taskDescription?.backgroundColor ?: return systemTheme
+        return if (Color.valueOf(backgroundColor).luminance() < 0.5) {
+            Theme.DARK
+        } else {
+            Theme.LIGHT
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
index 3b714b1..0650154 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt
@@ -19,10 +19,8 @@
 import android.app.ActivityManager.RunningTaskInfo
 import android.content.res.ColorStateList
 import android.content.res.Configuration
-import android.content.res.Configuration.UI_MODE_NIGHT_MASK
 import android.graphics.Bitmap
 import android.graphics.Color
-import android.graphics.drawable.GradientDrawable
 import android.graphics.drawable.LayerDrawable
 import android.graphics.drawable.RippleDrawable
 import android.graphics.drawable.ShapeDrawable
@@ -32,19 +30,22 @@
 import android.widget.ImageButton
 import android.widget.ImageView
 import android.widget.TextView
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.ui.graphics.toArgb
 import androidx.core.content.withStyledAttributes
 import androidx.core.view.isVisible
 import com.android.internal.R.attr.materialColorOnSecondaryContainer
 import com.android.internal.R.attr.materialColorOnSurface
-import com.android.internal.R.attr.materialColorOnSurfaceInverse
 import com.android.internal.R.attr.materialColorSecondaryContainer
 import com.android.internal.R.attr.materialColorSurfaceContainerHigh
 import com.android.internal.R.attr.materialColorSurfaceContainerLow
 import com.android.internal.R.attr.materialColorSurfaceDim
-import com.android.internal.R.attr.materialColorSurfaceInverse
 import com.android.window.flags.Flags
 import com.android.wm.shell.R
 import com.android.wm.shell.windowdecor.MaximizeButtonView
+import com.android.wm.shell.windowdecor.common.DecorThemeUtil
+import com.android.wm.shell.windowdecor.common.Theme
 import com.android.wm.shell.windowdecor.extension.isLightCaptionBarAppearance
 import com.android.wm.shell.windowdecor.extension.isTransparentCaptionBarAppearance
 
@@ -64,6 +65,10 @@
         onMaximizeHoverAnimationFinishedListener: () -> Unit
 ) : WindowDecorationViewHolder(rootView) {
 
+    private val decorThemeUtil = DecorThemeUtil(context)
+    private val lightColors = dynamicLightColorScheme(context)
+    private val darkColors = dynamicDarkColorScheme(context)
+
     /**
      * The corner radius to apply to the app chip, maximize and close button's background drawable.
      **/
@@ -168,19 +173,12 @@
         val headerStyle = getHeaderStyle(header)
 
         // Caption Background
-        val headerBackground = captionView.background as LayerDrawable
-        val backLayer = headerBackground.findDrawableByLayerId(R.id.backLayer) as GradientDrawable
-        val frontLayer = headerBackground.findDrawableByLayerId(R.id.frontLayer) as GradientDrawable
         when (headerStyle.background) {
             is HeaderStyle.Background.Opaque -> {
-                backLayer.setColor(headerStyle.background.backLayerColor ?: Color.BLACK)
-                frontLayer.setColor(headerStyle.background.frontLayerColor)
-                frontLayer.alpha = headerStyle.background.frontLayerOpacity
+                captionView.setBackgroundColor(headerStyle.background.color)
             }
             HeaderStyle.Background.Transparent -> {
-                backLayer.setColor(Color.TRANSPARENT)
-                frontLayer.setColor(Color.TRANSPARENT)
-                frontLayer.alpha = OPACITY_100
+                captionView.setBackgroundColor(Color.TRANSPARENT)
             }
         }
 
@@ -204,7 +202,7 @@
         }
         // Maximize button.
         maximizeButtonView.setAnimationTints(
-            darkMode = header.appTheme == Header.Theme.DARK,
+            darkMode = header.appTheme == Theme.DARK,
             iconForegroundColor = colorStateList,
             baseForegroundColor = foregroundColor,
             rippleDrawable = createRippleDrawable(
@@ -251,186 +249,88 @@
         )
     }
 
-    private fun getHeaderBackground(
-        header: Header
-    ): HeaderStyle.Background {
-        when (header.type) {
+    private fun getHeaderBackground(header: Header): HeaderStyle.Background {
+        return when (header.type) {
             Header.Type.DEFAULT -> {
-                if (header.systemTheme.isLight() && header.appTheme.isLight() && header.isFocused) {
-                    return HeaderStyle.Background.Opaque(
-                        frontLayerColor = attrToColor(materialColorSecondaryContainer),
-                        frontLayerOpacity = OPACITY_100,
-                        backLayerColor = null
-                    )
+                when (header.appTheme) {
+                    Theme.LIGHT -> {
+                        if (header.isFocused) {
+                            HeaderStyle.Background.Opaque(lightColors.secondaryContainer.toArgb())
+                        } else {
+                            HeaderStyle.Background.Opaque(lightColors.surfaceContainerLow.toArgb())
+                        }
+                    }
+                    Theme.DARK -> {
+                        if (header.isFocused) {
+                            HeaderStyle.Background.Opaque(darkColors.surfaceContainerHigh.toArgb())
+                        } else {
+                            HeaderStyle.Background.Opaque(darkColors.surfaceDim.toArgb())
+                        }
+                    }
                 }
-                if (header.systemTheme.isLight() && header.appTheme.isLight() &&
-                    !header.isFocused) {
-                    return HeaderStyle.Background.Opaque(
-                        frontLayerColor = attrToColor(materialColorSurfaceContainerLow),
-                        frontLayerOpacity = OPACITY_100,
-                        backLayerColor = null
-                    )
-                }
-                if (header.systemTheme.isDark() && header.appTheme.isDark() && header.isFocused) {
-                    return HeaderStyle.Background.Opaque(
-                        frontLayerColor = attrToColor(materialColorSurfaceContainerHigh),
-                        frontLayerOpacity = OPACITY_100,
-                        backLayerColor = null
-                    )
-                }
-                if (header.systemTheme.isDark() && header.appTheme.isDark() && !header.isFocused) {
-                    return HeaderStyle.Background.Opaque(
-                        frontLayerColor = attrToColor(materialColorSurfaceDim),
-                        frontLayerOpacity = OPACITY_100,
-                        backLayerColor = null
-                    )
-                }
-                if (header.systemTheme.isLight() && header.appTheme.isDark() && header.isFocused) {
-                    return HeaderStyle.Background.Opaque(
-                        frontLayerColor = attrToColor(materialColorSurfaceInverse),
-                        frontLayerOpacity = OPACITY_100,
-                        backLayerColor = null
-                    )
-                }
-                if (header.systemTheme.isLight() && header.appTheme.isDark() && !header.isFocused) {
-                    return HeaderStyle.Background.Opaque(
-                        frontLayerColor = attrToColor(materialColorSurfaceInverse),
-                        frontLayerOpacity = OPACITY_30,
-                        backLayerColor = Color.BLACK
-                    )
-                }
-                if (header.systemTheme.isDark() && header.appTheme.isLight() && header.isFocused) {
-                    return HeaderStyle.Background.Opaque(
-                        frontLayerColor = attrToColor(materialColorSurfaceInverse),
-                        frontLayerOpacity = OPACITY_100,
-                        backLayerColor = null
-                    )
-                }
-                if (header.systemTheme.isDark() && header.appTheme.isLight() && !header.isFocused) {
-                    return HeaderStyle.Background.Opaque(
-                        frontLayerColor = attrToColor(materialColorSurfaceInverse),
-                        frontLayerOpacity = OPACITY_55,
-                        backLayerColor = Color.WHITE
-                    )
-                }
-                error("No other combination expected header=$header")
             }
-            Header.Type.CUSTOM -> return HeaderStyle.Background.Transparent
+            Header.Type.CUSTOM -> HeaderStyle.Background.Transparent
         }
     }
 
     private fun getHeaderForeground(header: Header): HeaderStyle.Foreground {
-        when (header.type) {
+        return when (header.type) {
             Header.Type.DEFAULT -> {
-                if (header.systemTheme.isLight() && header.appTheme.isLight() && header.isFocused) {
-                    return HeaderStyle.Foreground(
-                        color = attrToColor(materialColorOnSecondaryContainer),
-                        opacity = OPACITY_100
-                    )
+                when (header.appTheme) {
+                    Theme.LIGHT -> {
+                        if (header.isFocused) {
+                            HeaderStyle.Foreground(
+                                color = lightColors.onSecondaryContainer.toArgb(),
+                                opacity = OPACITY_100
+                            )
+                        } else {
+                            HeaderStyle.Foreground(
+                                color = lightColors.onSecondaryContainer.toArgb(),
+                                opacity = OPACITY_65
+                            )
+                        }
+                    }
+                    Theme.DARK -> {
+                        if (header.isFocused) {
+                            HeaderStyle.Foreground(
+                                color = darkColors.onSurface.toArgb(),
+                                opacity = OPACITY_100
+                            )
+                        } else {
+                            HeaderStyle.Foreground(
+                                color = darkColors.onSurface.toArgb(),
+                                opacity = OPACITY_55
+                            )
+                        }
+                    }
                 }
-                if (header.systemTheme.isLight() && header.appTheme.isLight() &&
-                    !header.isFocused) {
-                    return HeaderStyle.Foreground(
-                        color = attrToColor(materialColorOnSecondaryContainer),
-                        opacity = OPACITY_65
-                    )
-                }
-                if (header.systemTheme.isDark() && header.appTheme.isDark() && header.isFocused) {
-                    return HeaderStyle.Foreground(
-                        color = attrToColor(materialColorOnSurface),
-                        opacity = OPACITY_100
-                    )
-                }
-                if (header.systemTheme.isDark() && header.appTheme.isDark() && !header.isFocused) {
-                    return HeaderStyle.Foreground(
-                        color = attrToColor(materialColorOnSurface),
-                        opacity = OPACITY_55
-                    )
-                }
-                if (header.systemTheme.isLight() && header.appTheme.isDark() && header.isFocused) {
-                    return HeaderStyle.Foreground(
-                        color = attrToColor(materialColorOnSurfaceInverse),
-                        opacity = OPACITY_100
-                    )
-                }
-                if (header.systemTheme.isLight() && header.appTheme.isDark() && !header.isFocused) {
-                    return HeaderStyle.Foreground(
-                        color = attrToColor(materialColorOnSurfaceInverse),
-                        opacity = OPACITY_65
-                    )
-                }
-                if (header.systemTheme.isDark() && header.appTheme.isLight() && header.isFocused) {
-                    return HeaderStyle.Foreground(
-                        color = attrToColor(materialColorOnSurfaceInverse),
-                        opacity = OPACITY_100
-                    )
-                }
-                if (header.systemTheme.isDark() && header.appTheme.isLight() && !header.isFocused) {
-                    return HeaderStyle.Foreground(
-                        color = attrToColor(materialColorOnSurfaceInverse),
-                        opacity = OPACITY_70
-                    )
-                }
-                error("No other combination expected header=$header")
             }
-            Header.Type.CUSTOM -> {
-                if (header.systemTheme.isLight() && header.isAppearanceCaptionLight &&
-                    header.isFocused) {
-                    return HeaderStyle.Foreground(
-                        color = attrToColor(materialColorOnSecondaryContainer),
+            Header.Type.CUSTOM -> when {
+                header.isAppearanceCaptionLight && header.isFocused -> {
+                    HeaderStyle.Foreground(
+                        color = lightColors.onSecondaryContainer.toArgb(),
                         opacity = OPACITY_100
                     )
                 }
-                if (header.systemTheme.isLight() && header.isAppearanceCaptionLight &&
-                    !header.isFocused) {
-                    return HeaderStyle.Foreground(
-                        color = attrToColor(materialColorOnSecondaryContainer),
+                header.isAppearanceCaptionLight && !header.isFocused -> {
+                    HeaderStyle.Foreground(
+                        color = lightColors.onSecondaryContainer.toArgb(),
                         opacity = OPACITY_65
                     )
                 }
-                if (header.systemTheme.isDark() && !header.isAppearanceCaptionLight &&
-                    header.isFocused) {
-                    return HeaderStyle.Foreground(
-                        color = attrToColor(materialColorOnSurface),
+                !header.isAppearanceCaptionLight && header.isFocused -> {
+                    HeaderStyle.Foreground(
+                        color = darkColors.onSurface.toArgb(),
                         opacity = OPACITY_100
                     )
                 }
-                if (header.systemTheme.isDark() && !header.isAppearanceCaptionLight &&
-                    !header.isFocused) {
-                    return HeaderStyle.Foreground(
-                        color = attrToColor(materialColorOnSurface),
+                !header.isAppearanceCaptionLight && !header.isFocused -> {
+                    HeaderStyle.Foreground(
+                        color = darkColors.onSurface.toArgb(),
                         opacity = OPACITY_55
                     )
                 }
-                if (header.systemTheme.isLight() && !header.isAppearanceCaptionLight &&
-                    header.isFocused) {
-                    return HeaderStyle.Foreground(
-                        color = attrToColor(materialColorOnSurfaceInverse),
-                        opacity = OPACITY_100
-                    )
-                }
-                if (header.systemTheme.isLight() && !header.isAppearanceCaptionLight &&
-                    !header.isFocused) {
-                    return HeaderStyle.Foreground(
-                        color = attrToColor(materialColorOnSurfaceInverse),
-                        opacity = OPACITY_65
-                    )
-                }
-                if (header.systemTheme.isDark() && header.isAppearanceCaptionLight &&
-                    header.isFocused) {
-                    return HeaderStyle.Foreground(
-                        color = attrToColor(materialColorOnSurfaceInverse),
-                        opacity = OPACITY_100
-                    )
-                }
-                if (header.systemTheme.isDark() && header.isAppearanceCaptionLight &&
-                    !header.isFocused) {
-                    return HeaderStyle.Foreground(
-                        color = attrToColor(materialColorOnSurfaceInverse),
-                        opacity = OPACITY_70
-                    )
-                }
-                error("No other combination expected header=$header")
+                else -> error("No other combination expected header=$header")
             }
         }
     }
@@ -442,41 +342,12 @@
             } else {
                 Header.Type.DEFAULT
             },
-            systemTheme = getSystemTheme(),
-            appTheme = getAppTheme(taskInfo),
+            appTheme = decorThemeUtil.getAppTheme(taskInfo),
             isFocused = taskInfo.isFocused,
             isAppearanceCaptionLight = taskInfo.isLightCaptionBarAppearance
         )
     }
 
-    private fun getSystemTheme(): Header.Theme {
-        return if ((context.resources.configuration.uiMode and UI_MODE_NIGHT_MASK) ==
-            Configuration.UI_MODE_NIGHT_YES) {
-            Header.Theme.DARK
-        } else {
-            Header.Theme.LIGHT
-        }
-    }
-
-    private fun getAppTheme(taskInfo: RunningTaskInfo): Header.Theme {
-        // TODO: use app's uiMode to find its actual light/dark value. It needs to be added to the
-        //   TaskInfo/TaskDescription.
-        val backgroundColor = taskInfo.taskDescription?.backgroundColor ?: return getSystemTheme()
-        return if (Color.valueOf(backgroundColor).luminance() < 0.5) {
-            Header.Theme.DARK
-        } else {
-            Header.Theme.LIGHT
-        }
-    }
-
-    @ColorInt
-    private fun attrToColor(attr: Int): Int {
-        context.withStyledAttributes(null, intArrayOf(attr), 0, 0) {
-            return getColor(0, 0)
-        }
-        return Color.WHITE
-    }
-
     @ColorInt
     private fun replaceColorAlpha(@ColorInt color: Int, alpha: Int): Int {
         return Color.argb(
@@ -530,19 +401,13 @@
 
     private data class Header(
         val type: Type,
-        val systemTheme: Theme,
         val appTheme: Theme,
         val isFocused: Boolean,
         val isAppearanceCaptionLight: Boolean,
     ) {
         enum class Type { DEFAULT, CUSTOM }
-        enum class Theme { LIGHT, DARK }
     }
 
-    private fun Header.Theme.isLight(): Boolean = this == Header.Theme.LIGHT
-
-    private fun Header.Theme.isDark(): Boolean = this == Header.Theme.DARK
-
     private data class HeaderStyle(
         val background: Background,
         val foreground: Foreground
@@ -554,11 +419,7 @@
 
         sealed class Background {
             data object Transparent : Background()
-            data class Opaque(
-                @ColorInt val frontLayerColor: Int,
-                val frontLayerOpacity: Int,
-                @ColorInt val backLayerColor: Int?
-            ) : Background()
+            data class Opaque(@ColorInt val color: Int) : Background()
         }
     }
 
@@ -634,9 +495,7 @@
         private const val OPACITY_100 = 255
         private const val OPACITY_11 = 28
         private const val OPACITY_15 = 38
-        private const val OPACITY_30 = 77
         private const val OPACITY_55 = 140
         private const val OPACITY_65 = 166
-        private const val OPACITY_70 = 179
     }
 }