diff --git a/packages/SystemUI/compose/core/Android.bp b/packages/SystemUI/compose/core/Android.bp new file mode 100644 index 0000000000000..4cfe39225a9b4 --- /dev/null +++ b/packages/SystemUI/compose/core/Android.bp @@ -0,0 +1,38 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +android_library { + name: "SystemUIComposeCore", + manifest: "AndroidManifest.xml", + + srcs: [ + "src/**/*.kt", + ], + + static_libs: [ + "androidx.compose.runtime_runtime", + "androidx.compose.material3_material3", + ], + + kotlincflags: ["-Xjvm-default=all"], +} diff --git a/packages/SystemUI/compose/core/AndroidManifest.xml b/packages/SystemUI/compose/core/AndroidManifest.xml new file mode 100644 index 0000000000000..83c442d2d6f24 --- /dev/null +++ b/packages/SystemUI/compose/core/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/packages/SystemUI/compose/core/TEST_MAPPING b/packages/SystemUI/compose/core/TEST_MAPPING new file mode 100644 index 0000000000000..dc243d2fd8f58 --- /dev/null +++ b/packages/SystemUI/compose/core/TEST_MAPPING @@ -0,0 +1,37 @@ +{ + "presubmit": [ + { + "name": "SystemUIComposeCoreTests", + "options": [ + { + "exclude-annotation": "org.junit.Ignore" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + }, + { + "name": "SystemUIComposeFeaturesTests", + "options": [ + { + "exclude-annotation": "org.junit.Ignore" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + }, + { + "name": "SystemUIComposeGalleryTests", + "options": [ + { + "exclude-annotation": "org.junit.Ignore" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + } + ] +} \ No newline at end of file diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/SystemUiController.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/SystemUiController.kt new file mode 100644 index 0000000000000..c9470c8dbe3a5 --- /dev/null +++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/SystemUiController.kt @@ -0,0 +1,294 @@ +/* + * 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.systemui.compose + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.os.Build +import android.view.View +import android.view.Window +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.window.DialogWindowProvider +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat + +/** + * ************************************************************************************************* + * This file was forked from + * https://github.com/google/accompanist/blob/main/systemuicontroller/src/main/java/com/google/accompanist/systemuicontroller/SystemUiController.kt + * and will be removed once it lands in AndroidX. + */ + +/** + * A class which provides easy-to-use utilities for updating the System UI bar colors within Jetpack + * Compose. + * + * @sample com.google.accompanist.sample.systemuicontroller.SystemUiControllerSample + */ +@Stable +interface SystemUiController { + + /** + * Property which holds the status bar visibility. If set to true, show the status bar, + * otherwise hide the status bar. + */ + var isStatusBarVisible: Boolean + + /** + * Property which holds the navigation bar visibility. If set to true, show the navigation bar, + * otherwise hide the navigation bar. + */ + var isNavigationBarVisible: Boolean + + /** + * Property which holds the status & navigation bar visibility. If set to true, show both bars, + * otherwise hide both bars. + */ + var isSystemBarsVisible: Boolean + get() = isNavigationBarVisible && isStatusBarVisible + set(value) { + isStatusBarVisible = value + isNavigationBarVisible = value + } + + /** + * Set the status bar color. + * + * @param color The **desired** [Color] to set. This may require modification if running on an + * API level that only supports white status bar icons. + * @param darkIcons Whether dark status bar icons would be preferable. + * @param transformColorForLightContent A lambda which will be invoked to transform [color] if + * dark icons were requested but are not available. Defaults to applying a black scrim. + * + * @see statusBarDarkContentEnabled + */ + fun setStatusBarColor( + color: Color, + darkIcons: Boolean = color.luminance() > 0.5f, + transformColorForLightContent: (Color) -> Color = BlackScrimmed + ) + + /** + * Set the navigation bar color. + * + * @param color The **desired** [Color] to set. This may require modification if running on an + * API level that only supports white navigation bar icons. Additionally this will be ignored + * and [Color.Transparent] will be used on API 29+ where gesture navigation is preferred or the + * system UI automatically applies background protection in other navigation modes. + * @param darkIcons Whether dark navigation bar icons would be preferable. + * @param navigationBarContrastEnforced Whether the system should ensure that the navigation bar + * has enough contrast when a fully transparent background is requested. Only supported on API + * 29+. + * @param transformColorForLightContent A lambda which will be invoked to transform [color] if + * dark icons were requested but are not available. Defaults to applying a black scrim. + * + * @see navigationBarDarkContentEnabled + * @see navigationBarContrastEnforced + */ + fun setNavigationBarColor( + color: Color, + darkIcons: Boolean = color.luminance() > 0.5f, + navigationBarContrastEnforced: Boolean = true, + transformColorForLightContent: (Color) -> Color = BlackScrimmed + ) + + /** + * Set the status and navigation bars to [color]. + * + * @see setStatusBarColor + * @see setNavigationBarColor + */ + fun setSystemBarsColor( + color: Color, + darkIcons: Boolean = color.luminance() > 0.5f, + isNavigationBarContrastEnforced: Boolean = true, + transformColorForLightContent: (Color) -> Color = BlackScrimmed + ) { + setStatusBarColor(color, darkIcons, transformColorForLightContent) + setNavigationBarColor( + color, + darkIcons, + isNavigationBarContrastEnforced, + transformColorForLightContent + ) + } + + /** Property which holds whether the status bar icons + content are 'dark' or not. */ + var statusBarDarkContentEnabled: Boolean + + /** Property which holds whether the navigation bar icons + content are 'dark' or not. */ + var navigationBarDarkContentEnabled: Boolean + + /** + * Property which holds whether the status & navigation bar icons + content are 'dark' or not. + */ + var systemBarsDarkContentEnabled: Boolean + get() = statusBarDarkContentEnabled && navigationBarDarkContentEnabled + set(value) { + statusBarDarkContentEnabled = value + navigationBarDarkContentEnabled = value + } + + /** + * Property which holds whether the system is ensuring that the navigation bar has enough + * contrast when a fully transparent background is requested. Only has an affect when running on + * Android API 29+ devices. + */ + var isNavigationBarContrastEnforced: Boolean +} + +/** + * Remembers a [SystemUiController] for the given [window]. + * + * If no [window] is provided, an attempt to find the correct [Window] is made. + * + * First, if the [LocalView]'s parent is a [DialogWindowProvider], then that dialog's [Window] will + * be used. + * + * Second, we attempt to find [Window] for the [Activity] containing the [LocalView]. + * + * If none of these are found (such as may happen in a preview), then the functionality of the + * returned [SystemUiController] will be degraded, but won't throw an exception. + */ +@Composable +fun rememberSystemUiController( + window: Window? = findWindow(), +): SystemUiController { + val view = LocalView.current + return remember(view, window) { AndroidSystemUiController(view, window) } +} + +@Composable +private fun findWindow(): Window? = + (LocalView.current.parent as? DialogWindowProvider)?.window + ?: LocalView.current.context.findWindow() + +private tailrec fun Context.findWindow(): Window? = + when (this) { + is Activity -> window + is ContextWrapper -> baseContext.findWindow() + else -> null + } + +/** + * A helper class for setting the navigation and status bar colors for a [View], gracefully + * degrading behavior based upon API level. + * + * Typically you would use [rememberSystemUiController] to remember an instance of this. + */ +internal class AndroidSystemUiController(private val view: View, private val window: Window?) : + SystemUiController { + private val windowInsetsController = window?.let { WindowCompat.getInsetsController(it, view) } + + override fun setStatusBarColor( + color: Color, + darkIcons: Boolean, + transformColorForLightContent: (Color) -> Color + ) { + statusBarDarkContentEnabled = darkIcons + + window?.statusBarColor = + when { + darkIcons && windowInsetsController?.isAppearanceLightStatusBars != true -> { + // If we're set to use dark icons, but our windowInsetsController call didn't + // succeed (usually due to API level), we instead transform the color to + // maintain contrast + transformColorForLightContent(color) + } + else -> color + }.toArgb() + } + + override fun setNavigationBarColor( + color: Color, + darkIcons: Boolean, + navigationBarContrastEnforced: Boolean, + transformColorForLightContent: (Color) -> Color + ) { + navigationBarDarkContentEnabled = darkIcons + isNavigationBarContrastEnforced = navigationBarContrastEnforced + + window?.navigationBarColor = + when { + darkIcons && windowInsetsController?.isAppearanceLightNavigationBars != true -> { + // If we're set to use dark icons, but our windowInsetsController call didn't + // succeed (usually due to API level), we instead transform the color to + // maintain contrast + transformColorForLightContent(color) + } + else -> color + }.toArgb() + } + + override var isStatusBarVisible: Boolean + get() { + return ViewCompat.getRootWindowInsets(view) + ?.isVisible(WindowInsetsCompat.Type.statusBars()) == true + } + set(value) { + if (value) { + windowInsetsController?.show(WindowInsetsCompat.Type.statusBars()) + } else { + windowInsetsController?.hide(WindowInsetsCompat.Type.statusBars()) + } + } + + override var isNavigationBarVisible: Boolean + get() { + return ViewCompat.getRootWindowInsets(view) + ?.isVisible(WindowInsetsCompat.Type.navigationBars()) == true + } + set(value) { + if (value) { + windowInsetsController?.show(WindowInsetsCompat.Type.navigationBars()) + } else { + windowInsetsController?.hide(WindowInsetsCompat.Type.navigationBars()) + } + } + + override var statusBarDarkContentEnabled: Boolean + get() = windowInsetsController?.isAppearanceLightStatusBars == true + set(value) { + windowInsetsController?.isAppearanceLightStatusBars = value + } + + override var navigationBarDarkContentEnabled: Boolean + get() = windowInsetsController?.isAppearanceLightNavigationBars == true + set(value) { + windowInsetsController?.isAppearanceLightNavigationBars = value + } + + override var isNavigationBarContrastEnforced: Boolean + get() = Build.VERSION.SDK_INT >= 29 && window?.isNavigationBarContrastEnforced == true + set(value) { + if (Build.VERSION.SDK_INT >= 29) { + window?.isNavigationBarContrastEnforced = value + } + } +} + +private val BlackScrim = Color(0f, 0f, 0f, 0.3f) // 30% opaque black +private val BlackScrimmed: (Color) -> Color = { original -> BlackScrim.compositeOver(original) } diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/AndroidColorScheme.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/AndroidColorScheme.kt new file mode 100644 index 0000000000000..b8639e64e0020 --- /dev/null +++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/AndroidColorScheme.kt @@ -0,0 +1,74 @@ +/* + * 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.systemui.compose.theme + +import android.annotation.ColorInt +import android.content.Context +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import com.android.internal.R + +/** CompositionLocal used to pass [AndroidColorScheme] down the tree. */ +val LocalAndroidColorScheme = + staticCompositionLocalOf { + throw IllegalStateException( + "No AndroidColorScheme configured. Make sure to use LocalAndroidColorScheme in a " + + "Composable surrounded by a SystemUITheme {}." + ) + } + +/** + * The Android color scheme. + * + * Important: Use M3 colors from MaterialTheme.colorScheme whenever possible instead. In the future, + * most of the colors in this class will be removed in favor of their M3 counterpart. + */ +class AndroidColorScheme internal constructor(context: Context) { + val colorPrimary = getColor(context, R.attr.colorPrimary) + val colorPrimaryDark = getColor(context, R.attr.colorPrimaryDark) + val colorAccent = getColor(context, R.attr.colorAccent) + val colorAccentPrimary = getColor(context, R.attr.colorAccentPrimary) + val colorAccentSecondary = getColor(context, R.attr.colorAccentSecondary) + val colorAccentTertiary = getColor(context, R.attr.colorAccentTertiary) + val colorAccentPrimaryVariant = getColor(context, R.attr.colorAccentPrimaryVariant) + val colorAccentSecondaryVariant = getColor(context, R.attr.colorAccentSecondaryVariant) + val colorAccentTertiaryVariant = getColor(context, R.attr.colorAccentTertiaryVariant) + val colorSurface = getColor(context, R.attr.colorSurface) + val colorSurfaceHighlight = getColor(context, R.attr.colorSurfaceHighlight) + val colorSurfaceVariant = getColor(context, R.attr.colorSurfaceVariant) + val colorSurfaceHeader = getColor(context, R.attr.colorSurfaceHeader) + val colorError = getColor(context, R.attr.colorError) + val colorBackground = getColor(context, R.attr.colorBackground) + val colorBackgroundFloating = getColor(context, R.attr.colorBackgroundFloating) + val panelColorBackground = getColor(context, R.attr.panelColorBackground) + val textColorPrimary = getColor(context, R.attr.textColorPrimary) + val textColorSecondary = getColor(context, R.attr.textColorSecondary) + val textColorTertiary = getColor(context, R.attr.textColorTertiary) + val textColorPrimaryInverse = getColor(context, R.attr.textColorPrimaryInverse) + val textColorSecondaryInverse = getColor(context, R.attr.textColorSecondaryInverse) + val textColorTertiaryInverse = getColor(context, R.attr.textColorTertiaryInverse) + val textColorOnAccent = getColor(context, R.attr.textColorOnAccent) + val colorForeground = getColor(context, R.attr.colorForeground) + val colorForegroundInverse = getColor(context, R.attr.colorForegroundInverse) + + private fun getColor(context: Context, attr: Int): Color { + val ta = context.obtainStyledAttributes(intArrayOf(attr)) + @ColorInt val color = ta.getColor(0, 0) + ta.recycle() + return Color(color) + } +} diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/SystemUITheme.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/SystemUITheme.kt new file mode 100644 index 0000000000000..79e3d3d475a89 --- /dev/null +++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/theme/SystemUITheme.kt @@ -0,0 +1,53 @@ +/* + * 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.systemui.compose.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Typography +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext + +/** The Material 3 theme that should wrap all SystemUI Composables. */ +@Composable +fun SystemUITheme( + isDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + val context = LocalContext.current + + // TODO(b/230605885): Define our typography and color scheme. + val colorScheme = + if (isDarkTheme) { + dynamicDarkColorScheme(context) + } else { + dynamicLightColorScheme(context) + } + val androidColorScheme = AndroidColorScheme(context) + val typography = Typography() + + MaterialTheme(colorScheme, typography = typography) { + CompositionLocalProvider( + LocalAndroidColorScheme provides androidColorScheme, + ) { + content() + } + } +} diff --git a/packages/SystemUI/compose/core/tests/Android.bp b/packages/SystemUI/compose/core/tests/Android.bp new file mode 100644 index 0000000000000..f8023e2519f82 --- /dev/null +++ b/packages/SystemUI/compose/core/tests/Android.bp @@ -0,0 +1,48 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +// TODO(b/230606318): Make those host tests instead of device tests. +android_test { + name: "SystemUIComposeCoreTests", + manifest: "AndroidManifest.xml", + test_suites: ["device-tests"], + sdk_version: "current", + certificate: "platform", + + srcs: [ + "src/**/*.kt", + ], + + static_libs: [ + "SystemUIComposeCore", + + "androidx.test.runner", + "androidx.test.ext.junit", + + "androidx.compose.runtime_runtime", + "androidx.compose.ui_ui-test-junit4", + "androidx.compose.ui_ui-test-manifest", + ], + + kotlincflags: ["-Xjvm-default=enable"], +} diff --git a/packages/SystemUI/compose/core/tests/AndroidManifest.xml b/packages/SystemUI/compose/core/tests/AndroidManifest.xml new file mode 100644 index 0000000000000..729ab989cdc0c --- /dev/null +++ b/packages/SystemUI/compose/core/tests/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/compose/core/tests/src/com/android/systemui/compose/theme/SystemUIThemeTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/systemui/compose/theme/SystemUIThemeTest.kt new file mode 100644 index 0000000000000..20249f66d0d0b --- /dev/null +++ b/packages/SystemUI/compose/core/tests/src/com/android/systemui/compose/theme/SystemUIThemeTest.kt @@ -0,0 +1,57 @@ +/* + * 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.systemui.compose.theme + +import androidx.compose.material3.Text +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertThrows +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SystemUIThemeTest { + @get:Rule val composeRule = createComposeRule() + + @Test + fun testThemeShowsContent() { + composeRule.setContent { SystemUITheme { Text("foo") } } + + composeRule.onNodeWithText("foo").assertIsDisplayed() + } + + @Test + fun testAndroidColorsAreAvailableInsideTheme() { + composeRule.setContent { + SystemUITheme { Text("foo", color = LocalAndroidColorScheme.current.colorAccent) } + } + + composeRule.onNodeWithText("foo").assertIsDisplayed() + } + + @Test + fun testAccessingAndroidColorsWithoutThemeThrows() { + assertThrows(IllegalStateException::class.java) { + composeRule.setContent { + Text("foo", color = LocalAndroidColorScheme.current.colorAccent) + } + } + } +} diff --git a/packages/SystemUI/compose/features/Android.bp b/packages/SystemUI/compose/features/Android.bp new file mode 100644 index 0000000000000..40218de94258b --- /dev/null +++ b/packages/SystemUI/compose/features/Android.bp @@ -0,0 +1,40 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +android_library { + name: "SystemUIComposeFeatures", + manifest: "AndroidManifest.xml", + + srcs: [ + "src/**/*.kt", + ], + + static_libs: [ + "SystemUIComposeCore", + + "androidx.compose.runtime_runtime", + "androidx.compose.material3_material3", + ], + + kotlincflags: ["-Xjvm-default=all"], +} diff --git a/packages/SystemUI/compose/features/AndroidManifest.xml b/packages/SystemUI/compose/features/AndroidManifest.xml new file mode 100644 index 0000000000000..0aea99d4e9606 --- /dev/null +++ b/packages/SystemUI/compose/features/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/packages/SystemUI/compose/features/TEST_MAPPING b/packages/SystemUI/compose/features/TEST_MAPPING new file mode 100644 index 0000000000000..7430acb2e900c --- /dev/null +++ b/packages/SystemUI/compose/features/TEST_MAPPING @@ -0,0 +1,26 @@ +{ + "presubmit": [ + { + "name": "SystemUIComposeFeaturesTests", + "options": [ + { + "exclude-annotation": "org.junit.Ignore" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + }, + { + "name": "SystemUIComposeGalleryTests", + "options": [ + { + "exclude-annotation": "org.junit.Ignore" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + } + ] +} \ No newline at end of file diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/ExampleFeature.kt b/packages/SystemUI/compose/features/src/com/android/systemui/ExampleFeature.kt new file mode 100644 index 0000000000000..c58c16259abed --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/ExampleFeature.kt @@ -0,0 +1,94 @@ +/* + * 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.systemui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlin.math.roundToInt + +/** + * This is an example Compose feature, which shows a text and a count that is incremented when + * clicked. We also show the max width available to this component, which is displayed either next + * to or below the text depending on that max width. + */ +@Composable +fun ExampleFeature(text: String, modifier: Modifier = Modifier) { + BoxWithConstraints(modifier) { + val maxWidth = maxWidth + if (maxWidth < 600.dp) { + Column { + CounterTile(text) + Spacer(Modifier.size(16.dp)) + MaxWidthTile(maxWidth) + } + } else { + Row { + CounterTile(text) + Spacer(Modifier.size(16.dp)) + MaxWidthTile(maxWidth) + } + } + } +} + +@Composable +private fun CounterTile(text: String, modifier: Modifier = Modifier) { + Surface( + modifier, + color = MaterialTheme.colorScheme.primaryContainer, + shape = RoundedCornerShape(28.dp), + ) { + var count by remember { mutableStateOf(0) } + Column( + Modifier.clickable { count++ }.padding(16.dp), + ) { + Text(text) + Text("I was clicked $count times.") + } + } +} + +@Composable +private fun MaxWidthTile(maxWidth: Dp, modifier: Modifier = Modifier) { + Surface( + modifier, + color = MaterialTheme.colorScheme.tertiaryContainer, + shape = RoundedCornerShape(28.dp), + ) { + Text( + "The max available width to me is: ${maxWidth.value.roundToInt()}dp", + Modifier.padding(16.dp) + ) + } +} diff --git a/packages/SystemUI/compose/features/tests/Android.bp b/packages/SystemUI/compose/features/tests/Android.bp new file mode 100644 index 0000000000000..ff534bd01fd38 --- /dev/null +++ b/packages/SystemUI/compose/features/tests/Android.bp @@ -0,0 +1,48 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +// TODO(b/230606318): Make those host tests instead of device tests. +android_test { + name: "SystemUIComposeFeaturesTests", + manifest: "AndroidManifest.xml", + test_suites: ["device-tests"], + sdk_version: "current", + certificate: "platform", + + srcs: [ + "src/**/*.kt", + ], + + static_libs: [ + "SystemUIComposeFeatures", + + "androidx.test.runner", + "androidx.test.ext.junit", + + "androidx.compose.runtime_runtime", + "androidx.compose.ui_ui-test-junit4", + "androidx.compose.ui_ui-test-manifest", + ], + + kotlincflags: ["-Xjvm-default=enable"], +} diff --git a/packages/SystemUI/compose/features/tests/AndroidManifest.xml b/packages/SystemUI/compose/features/tests/AndroidManifest.xml new file mode 100644 index 0000000000000..5e54c1f353d2e --- /dev/null +++ b/packages/SystemUI/compose/features/tests/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/compose/features/tests/src/com/android/systemui/ExampleFeatureTest.kt b/packages/SystemUI/compose/features/tests/src/com/android/systemui/ExampleFeatureTest.kt new file mode 100644 index 0000000000000..1c2e8fab0337d --- /dev/null +++ b/packages/SystemUI/compose/features/tests/src/com/android/systemui/ExampleFeatureTest.kt @@ -0,0 +1,46 @@ +/* + * 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.systemui + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ExampleFeatureTest { + @get:Rule val composeRule = createComposeRule() + + @Test + fun testProvidedTextIsDisplayed() { + composeRule.setContent { ExampleFeature("foo") } + + composeRule.onNodeWithText("foo").assertIsDisplayed() + } + + @Test + fun testCountIsIncreasedWhenClicking() { + composeRule.setContent { ExampleFeature("foo") } + + composeRule.onNodeWithText("I was clicked 0 times.").assertIsDisplayed().performClick() + composeRule.onNodeWithText("I was clicked 1 times.").assertIsDisplayed() + } +} diff --git a/packages/SystemUI/compose/gallery/Android.bp b/packages/SystemUI/compose/gallery/Android.bp new file mode 100644 index 0000000000000..40504dc30c338 --- /dev/null +++ b/packages/SystemUI/compose/gallery/Android.bp @@ -0,0 +1,72 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +android_library { + name: "SystemUIComposeGalleryLib", + manifest: "AndroidManifest.xml", + + srcs: [ + "src/**/*.kt", + ], + + resource_dirs: [ + "res", + ], + + static_libs: [ + "SystemUI-core", + "SystemUIComposeCore", + "SystemUIComposeFeatures", + + "androidx.compose.runtime_runtime", + "androidx.compose.material3_material3", + "androidx.compose.material_material-icons-extended", + "androidx.activity_activity-compose", + "androidx.navigation_navigation-compose", + + "androidx.appcompat_appcompat", + ], + + kotlincflags: ["-Xjvm-default=all"], +} + +android_app { + name: "SystemUIComposeGallery", + defaults: ["platform_app_defaults"], + manifest: "app/AndroidManifest.xml", + + static_libs: [ + "SystemUIComposeGalleryLib", + ], + + platform_apis: true, + system_ext_specific: true, + certificate: "platform", + privileged: true, + + optimize: { + proguard_flags_files: ["proguard-rules.pro"], + }, + + dxflags: ["--multi-dex"], +} diff --git a/packages/SystemUI/compose/gallery/AndroidManifest.xml b/packages/SystemUI/compose/gallery/AndroidManifest.xml new file mode 100644 index 0000000000000..2f30651a6acf9 --- /dev/null +++ b/packages/SystemUI/compose/gallery/AndroidManifest.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/compose/gallery/TEST_MAPPING b/packages/SystemUI/compose/gallery/TEST_MAPPING new file mode 100644 index 0000000000000..c7f8a92164188 --- /dev/null +++ b/packages/SystemUI/compose/gallery/TEST_MAPPING @@ -0,0 +1,15 @@ +{ + "presubmit": [ + { + "name": "SystemUIComposeGalleryTests", + "options": [ + { + "exclude-annotation": "org.junit.Ignore" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + } + ] +} \ No newline at end of file diff --git a/packages/SystemUI/compose/gallery/app/AndroidManifest.xml b/packages/SystemUI/compose/gallery/app/AndroidManifest.xml new file mode 100644 index 0000000000000..1f3fd8c312d9e --- /dev/null +++ b/packages/SystemUI/compose/gallery/app/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/packages/SystemUI/compose/gallery/proguard-rules.pro b/packages/SystemUI/compose/gallery/proguard-rules.pro new file mode 100644 index 0000000000000..481bb43481410 --- /dev/null +++ b/packages/SystemUI/compose/gallery/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/packages/SystemUI/compose/gallery/res/drawable-v24/ic_launcher_foreground.xml b/packages/SystemUI/compose/gallery/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000000000..966abaff20743 --- /dev/null +++ b/packages/SystemUI/compose/gallery/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/compose/gallery/res/drawable/ic_launcher_background.xml b/packages/SystemUI/compose/gallery/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000000..61bb79edb7090 --- /dev/null +++ b/packages/SystemUI/compose/gallery/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher.xml b/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000000..03eed2533da28 --- /dev/null +++ b/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher_round.xml b/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000000..03eed2533da28 --- /dev/null +++ b/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher.webp b/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000..c209e78ecd372 Binary files /dev/null and b/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher_round.webp b/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000..b2dfe3d1ba5cf Binary files /dev/null and b/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher.webp b/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000..4f0f1d64e58ba Binary files /dev/null and b/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher_round.webp b/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000..62b611da08167 Binary files /dev/null and b/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher.webp b/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000..948a3070fe34c Binary files /dev/null and b/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher_round.webp b/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000..1b9a6956b3acd Binary files /dev/null and b/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher.webp b/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000..28d4b77f9f036 Binary files /dev/null and b/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher_round.webp b/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000..9287f5083623b Binary files /dev/null and b/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher.webp b/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000..aa7d6427e6fa1 Binary files /dev/null and b/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher_round.webp b/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000..9126ae37cbc35 Binary files /dev/null and b/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/packages/SystemUI/compose/gallery/res/values/colors.xml b/packages/SystemUI/compose/gallery/res/values/colors.xml new file mode 100644 index 0000000000000..a2fcbffc26c07 --- /dev/null +++ b/packages/SystemUI/compose/gallery/res/values/colors.xml @@ -0,0 +1,19 @@ + + + + #FFFFFF + \ No newline at end of file diff --git a/packages/SystemUI/compose/gallery/res/values/strings.xml b/packages/SystemUI/compose/gallery/res/values/strings.xml new file mode 100644 index 0000000000000..86bdb05688371 --- /dev/null +++ b/packages/SystemUI/compose/gallery/res/values/strings.xml @@ -0,0 +1,20 @@ + + + + + SystemUI Gallery + \ No newline at end of file diff --git a/packages/SystemUI/compose/gallery/res/values/themes.xml b/packages/SystemUI/compose/gallery/res/values/themes.xml new file mode 100644 index 0000000000000..45fa1f5dfb5ca --- /dev/null +++ b/packages/SystemUI/compose/gallery/res/values/themes.xml @@ -0,0 +1,30 @@ + + + + + diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ColorsScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ColorsScreen.kt new file mode 100644 index 0000000000000..dfa1b26f464ef --- /dev/null +++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ColorsScreen.kt @@ -0,0 +1,139 @@ +/* + * 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.systemui.compose.gallery + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.android.systemui.compose.theme.LocalAndroidColorScheme + +/** The screen that shows all the Material 3 colors. */ +@Composable +fun MaterialColorsScreen() { + val colors = MaterialTheme.colorScheme + ColorsScreen( + listOf( + "primary" to colors.primary, + "onPrimary" to colors.onPrimary, + "primaryContainer" to colors.primaryContainer, + "onPrimaryContainer" to colors.onPrimaryContainer, + "inversePrimary" to colors.inversePrimary, + "secondary" to colors.secondary, + "onSecondary" to colors.onSecondary, + "secondaryContainer" to colors.secondaryContainer, + "onSecondaryContainer" to colors.onSecondaryContainer, + "tertiary" to colors.tertiary, + "onTertiary" to colors.onTertiary, + "tertiaryContainer" to colors.tertiaryContainer, + "onTertiaryContainer" to colors.onTertiaryContainer, + "background" to colors.background, + "onBackground" to colors.onBackground, + "surface" to colors.surface, + "onSurface" to colors.onSurface, + "surfaceVariant" to colors.surfaceVariant, + "onSurfaceVariant" to colors.onSurfaceVariant, + "inverseSurface" to colors.inverseSurface, + "inverseOnSurface" to colors.inverseOnSurface, + "error" to colors.error, + "onError" to colors.onError, + "errorContainer" to colors.errorContainer, + "onErrorContainer" to colors.onErrorContainer, + "outline" to colors.outline, + ) + ) +} + +/** The screen that shows all the Android colors. */ +@Composable +fun AndroidColorsScreen() { + val colors = LocalAndroidColorScheme.current + ColorsScreen( + listOf( + "colorPrimary" to colors.colorPrimary, + "colorPrimaryDark" to colors.colorPrimaryDark, + "colorAccent" to colors.colorAccent, + "colorAccentPrimary" to colors.colorAccentPrimary, + "colorAccentSecondary" to colors.colorAccentSecondary, + "colorAccentTertiary" to colors.colorAccentTertiary, + "colorAccentPrimaryVariant" to colors.colorAccentPrimaryVariant, + "colorAccentSecondaryVariant" to colors.colorAccentSecondaryVariant, + "colorAccentTertiaryVariant" to colors.colorAccentTertiaryVariant, + "colorSurface" to colors.colorSurface, + "colorSurfaceHighlight" to colors.colorSurfaceHighlight, + "colorSurfaceVariant" to colors.colorSurfaceVariant, + "colorSurfaceHeader" to colors.colorSurfaceHeader, + "colorError" to colors.colorError, + "colorBackground" to colors.colorBackground, + "colorBackgroundFloating" to colors.colorBackgroundFloating, + "panelColorBackground" to colors.panelColorBackground, + "textColorPrimary" to colors.textColorPrimary, + "textColorSecondary" to colors.textColorSecondary, + "textColorTertiary" to colors.textColorTertiary, + "textColorPrimaryInverse" to colors.textColorPrimaryInverse, + "textColorSecondaryInverse" to colors.textColorSecondaryInverse, + "textColorTertiaryInverse" to colors.textColorTertiaryInverse, + "textColorOnAccent" to colors.textColorOnAccent, + "colorForeground" to colors.colorForeground, + "colorForegroundInverse" to colors.colorForegroundInverse, + ) + ) +} + +@Composable +private fun ColorsScreen( + colors: List>, +) { + LazyColumn( + Modifier.fillMaxWidth(), + ) { + colors.forEach { (name, color) -> item { ColorTile(color, name) } } + } +} + +@Composable +private fun ColorTile( + color: Color, + name: String, +) { + Row( + Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + val shape = RoundedCornerShape(16.dp) + Spacer( + Modifier.border(1.dp, MaterialTheme.colorScheme.onBackground, shape) + .background(color, shape) + .size(64.dp) + ) + Spacer(Modifier.width(16.dp)) + Text(name) + } +} diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ConfigurationControls.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ConfigurationControls.kt new file mode 100644 index 0000000000000..990d060207df1 --- /dev/null +++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ConfigurationControls.kt @@ -0,0 +1,210 @@ +package com.android.systemui.compose.gallery + +import android.graphics.Point +import android.os.UserHandle +import android.view.Display +import android.view.WindowManagerGlobal +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DarkMode +import androidx.compose.material.icons.filled.FormatSize +import androidx.compose.material.icons.filled.FormatTextdirectionLToR +import androidx.compose.material.icons.filled.FormatTextdirectionRToL +import androidx.compose.material.icons.filled.InvertColors +import androidx.compose.material.icons.filled.LightMode +import androidx.compose.material.icons.filled.Smartphone +import androidx.compose.material.icons.filled.Tablet +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import kotlin.math.max +import kotlin.math.min + +enum class FontScale(val scale: Float) { + Small(0.85f), + Normal(1f), + Big(1.15f), + Bigger(1.30f), +} + +/** A configuration panel that allows to toggle the theme, font scale and layout direction. */ +@Composable +fun ConfigurationControls( + theme: Theme, + fontScale: FontScale, + layoutDirection: LayoutDirection, + onChangeTheme: () -> Unit, + onChangeLayoutDirection: () -> Unit, + onChangeFontScale: () -> Unit, + modifier: Modifier = Modifier, +) { + // The display we are emulating, if any. + var emulatedDisplayName by rememberSaveable { mutableStateOf(null) } + val emulatedDisplay = + emulatedDisplayName?.let { name -> EmulatedDisplays.firstOrNull { it.name == name } } + + LaunchedEffect(emulatedDisplay) { + val wm = WindowManagerGlobal.getWindowManagerService() + + val defaultDisplayId = Display.DEFAULT_DISPLAY + if (emulatedDisplay == null) { + wm.clearForcedDisplayDensityForUser(defaultDisplayId, UserHandle.myUserId()) + wm.clearForcedDisplaySize(defaultDisplayId) + } else { + val density = emulatedDisplay.densityDpi + + // Emulate the display and make sure that we use the maximum available space possible. + val initialSize = Point() + wm.getInitialDisplaySize(defaultDisplayId, initialSize) + val width = emulatedDisplay.width + val height = emulatedDisplay.height + val minOfSize = min(width, height) + val maxOfSize = max(width, height) + if (initialSize.x < initialSize.y) { + wm.setForcedDisplaySize(defaultDisplayId, minOfSize, maxOfSize) + } else { + wm.setForcedDisplaySize(defaultDisplayId, maxOfSize, minOfSize) + } + wm.setForcedDisplayDensityForUser(defaultDisplayId, density, UserHandle.myUserId()) + } + } + + // TODO(b/231131244): Fork FlowRow from Accompanist and use that instead to make sure that users + // don't miss any available configuration. + LazyRow(modifier) { + // Dark/light theme. + item { + TextButton(onChangeTheme) { + val text: String + val icon: ImageVector + + when (theme) { + Theme.System -> { + icon = Icons.Default.InvertColors + text = "System" + } + Theme.Dark -> { + icon = Icons.Default.DarkMode + text = "Dark" + } + Theme.Light -> { + icon = Icons.Default.LightMode + text = "Light" + } + } + + Icon(icon, null) + Spacer(Modifier.width(8.dp)) + Text(text) + } + } + + // Font scale. + item { + TextButton(onChangeFontScale) { + Icon(Icons.Default.FormatSize, null) + Spacer(Modifier.width(8.dp)) + + Text(fontScale.name) + } + } + + // Layout direction. + item { + TextButton(onChangeLayoutDirection) { + when (layoutDirection) { + LayoutDirection.Ltr -> { + Icon(Icons.Default.FormatTextdirectionLToR, null) + Spacer(Modifier.width(8.dp)) + Text("LTR") + } + LayoutDirection.Rtl -> { + Icon(Icons.Default.FormatTextdirectionRToL, null) + Spacer(Modifier.width(8.dp)) + Text("RTL") + } + } + } + } + + // Display emulation. + EmulatedDisplays.forEach { display -> + item { + DisplayButton( + display, + emulatedDisplay == display, + { emulatedDisplayName = it?.name }, + ) + } + } + } +} + +@Composable +private fun DisplayButton( + display: EmulatedDisplay, + selected: Boolean, + onChangeEmulatedDisplay: (EmulatedDisplay?) -> Unit, +) { + val onClick = { + if (selected) { + onChangeEmulatedDisplay(null) + } else { + onChangeEmulatedDisplay(display) + } + } + + val content: @Composable RowScope.() -> Unit = { + Icon(display.icon, null) + Spacer(Modifier.width(8.dp)) + Text(display.name) + } + + if (selected) { + Button(onClick, contentPadding = ButtonDefaults.TextButtonContentPadding, content = content) + } else { + TextButton(onClick, content = content) + } +} + +/** The displays that can be emulated from this Gallery app. */ +private val EmulatedDisplays = + listOf( + EmulatedDisplay( + "Phone", + Icons.Default.Smartphone, + width = 1440, + height = 3120, + densityDpi = 560, + ), + EmulatedDisplay( + "Tablet", + Icons.Default.Tablet, + width = 2560, + height = 1600, + densityDpi = 320, + ), + ) + +private data class EmulatedDisplay( + val name: String, + val icon: ImageVector, + val width: Int, + val height: Int, + val densityDpi: Int, +) diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt new file mode 100644 index 0000000000000..6e1721490f983 --- /dev/null +++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt @@ -0,0 +1,28 @@ +/* + * 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.systemui.compose.gallery + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.android.systemui.ExampleFeature + +/** The screen that shows ExampleFeature. */ +@Composable +fun ExampleFeatureScreen(modifier: Modifier = Modifier) { + Column(modifier) { ExampleFeature("This is an example feature!") } +} diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryActivity.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryActivity.kt new file mode 100644 index 0000000000000..bb2d2feba39fa --- /dev/null +++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryActivity.kt @@ -0,0 +1,80 @@ +/* + * 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.systemui.compose.gallery + +import android.app.UiModeManager +import android.content.Context +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.core.view.WindowCompat +import com.android.systemui.compose.rememberSystemUiController + +class GalleryActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager + + setContent { + var theme by rememberSaveable { mutableStateOf(Theme.System) } + val onChangeTheme = { + // Change to the next theme for a toggle behavior. + theme = + when (theme) { + Theme.System -> Theme.Dark + Theme.Dark -> Theme.Light + Theme.Light -> Theme.System + } + } + + val isSystemInDarkTheme = isSystemInDarkTheme() + val isDark = theme == Theme.Dark || (theme == Theme.System && isSystemInDarkTheme) + val useDarkIcons = !isDark + val systemUiController = rememberSystemUiController() + SideEffect { + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = useDarkIcons, + ) + + uiModeManager.setApplicationNightMode( + when (theme) { + Theme.System -> UiModeManager.MODE_NIGHT_AUTO + Theme.Dark -> UiModeManager.MODE_NIGHT_YES + Theme.Light -> UiModeManager.MODE_NIGHT_NO + } + ) + } + + GalleryApp(theme, onChangeTheme) + } + } +} + +enum class Theme { + System, + Dark, + Light, +} diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt new file mode 100644 index 0000000000000..c341867bfb591 --- /dev/null +++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt @@ -0,0 +1,125 @@ +package com.android.systemui.compose.gallery + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.rememberNavController +import com.android.systemui.compose.theme.SystemUITheme + +/** The gallery app screens. */ +object GalleryAppScreens { + val Typography = ChildScreen("typography") { TypographyScreen() } + val MaterialColors = ChildScreen("material_colors") { MaterialColorsScreen() } + val AndroidColors = ChildScreen("android_colors") { AndroidColorsScreen() } + val ExampleFeature = ChildScreen("example_feature") { ExampleFeatureScreen() } + + val Home = + ParentScreen( + "home", + mapOf( + "Typography" to Typography, + "Material colors" to MaterialColors, + "Android colors" to AndroidColors, + "Example feature" to ExampleFeature, + ) + ) +} + +/** The main content of the app, that shows [GalleryAppScreens.Home] by default. */ +@Composable +private fun MainContent() { + Box(Modifier.fillMaxSize()) { + val navController = rememberNavController() + NavHost( + navController = navController, + startDestination = GalleryAppScreens.Home.identifier, + ) { + screen(GalleryAppScreens.Home, navController) + } + } +} + +/** + * The top-level composable shown when starting the app. This composable always shows a + * [ConfigurationControls] at the top of the screen, above the [MainContent]. + */ +@Composable +fun GalleryApp( + theme: Theme, + onChangeTheme: () -> Unit, +) { + val systemFontScale = LocalDensity.current.fontScale + var fontScale: FontScale by remember { + mutableStateOf( + FontScale.values().firstOrNull { it.scale == systemFontScale } ?: FontScale.Normal + ) + } + val context = LocalContext.current + val density = Density(context.resources.displayMetrics.density, fontScale.scale) + val onChangeFontScale = { + fontScale = + when (fontScale) { + FontScale.Small -> FontScale.Normal + FontScale.Normal -> FontScale.Big + FontScale.Big -> FontScale.Bigger + FontScale.Bigger -> FontScale.Small + } + } + + val systemLayoutDirection = LocalLayoutDirection.current + var layoutDirection by remember { mutableStateOf(systemLayoutDirection) } + val onChangeLayoutDirection = { + layoutDirection = + when (layoutDirection) { + LayoutDirection.Ltr -> LayoutDirection.Rtl + LayoutDirection.Rtl -> LayoutDirection.Ltr + } + } + + CompositionLocalProvider( + LocalDensity provides density, + LocalLayoutDirection provides layoutDirection, + ) { + SystemUITheme { + Surface( + Modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + Column(Modifier.fillMaxSize().systemBarsPadding().padding(16.dp)) { + ConfigurationControls( + theme, + fontScale, + layoutDirection, + onChangeTheme, + onChangeLayoutDirection, + onChangeFontScale, + ) + + Spacer(Modifier.height(4.dp)) + + MainContent() + } + } + } + } +} diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt new file mode 100644 index 0000000000000..467dac044b79c --- /dev/null +++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt @@ -0,0 +1,95 @@ +/* + * 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.systemui.compose.gallery + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.composable +import androidx.navigation.compose.navigation + +/** + * A screen in an app. It is either an [ParentScreen] which lists its child screens to navigate to + * them or a [ChildScreen] which shows some content. + */ +sealed class Screen(val identifier: String) + +class ParentScreen( + identifier: String, + val children: Map, +) : Screen(identifier) + +class ChildScreen( + identifier: String, + val content: @Composable (NavController) -> Unit, +) : Screen(identifier) + +/** Create the navigation graph for [screen]. */ +fun NavGraphBuilder.screen(screen: Screen, navController: NavController) { + when (screen) { + is ChildScreen -> composable(screen.identifier) { screen.content(navController) } + is ParentScreen -> { + val menuRoute = "${screen.identifier}_menu" + navigation(startDestination = menuRoute, route = screen.identifier) { + // The menu to navigate to one of the children screens. + composable(menuRoute) { ScreenMenu(screen, navController) } + + // The content of the child screens. + screen.children.forEach { (_, child) -> screen(child, navController) } + } + } + } +} + +@Composable +private fun ScreenMenu( + screen: ParentScreen, + navController: NavController, +) { + LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) { + screen.children.forEach { (name, child) -> + item { + Surface( + Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.secondaryContainer, + shape = CircleShape, + ) { + Column( + Modifier.clickable { navController.navigate(child.identifier) } + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text(name) + } + } + } + } + } +} diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/TypographyScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/TypographyScreen.kt new file mode 100644 index 0000000000000..147025ed1d606 --- /dev/null +++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/TypographyScreen.kt @@ -0,0 +1,67 @@ +/* + * 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.systemui.compose.gallery + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow + +/** The screen that shows the Material text styles. */ +@Composable +fun TypographyScreen() { + val typography = MaterialTheme.typography + + Column( + Modifier.fillMaxSize() + .horizontalScroll(rememberScrollState()) + .verticalScroll(rememberScrollState()), + ) { + FontLine("displayLarge", typography.displayLarge) + FontLine("displayMedium", typography.displayMedium) + FontLine("displaySmall", typography.displaySmall) + FontLine("headlineLarge", typography.headlineLarge) + FontLine("headlineMedium", typography.headlineMedium) + FontLine("headlineSmall", typography.headlineSmall) + FontLine("titleLarge", typography.titleLarge) + FontLine("titleMedium", typography.titleMedium) + FontLine("titleSmall", typography.titleSmall) + FontLine("bodyLarge", typography.bodyLarge) + FontLine("bodyMedium", typography.bodyMedium) + FontLine("bodySmall", typography.bodySmall) + FontLine("labelLarge", typography.labelLarge) + FontLine("labelMedium", typography.labelMedium) + FontLine("labelSmall", typography.labelSmall) + } +} + +@Composable +private fun FontLine(name: String, style: TextStyle) { + Text( + "$name (${style.fontSize}/${style.lineHeight}, W${style.fontWeight?.weight})", + style = style, + maxLines = 1, + overflow = TextOverflow.Visible, + ) +} diff --git a/packages/SystemUI/compose/gallery/tests/Android.bp b/packages/SystemUI/compose/gallery/tests/Android.bp new file mode 100644 index 0000000000000..3e01f7d2c431f --- /dev/null +++ b/packages/SystemUI/compose/gallery/tests/Android.bp @@ -0,0 +1,47 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +android_test { + name: "SystemUIComposeGalleryTests", + manifest: "AndroidManifest.xml", + test_suites: ["device-tests"], + sdk_version: "current", + certificate: "platform", + + srcs: [ + "src/**/*.kt", + ], + + static_libs: [ + "SystemUIComposeGalleryLib", + + "androidx.test.runner", + "androidx.test.ext.junit", + + "androidx.compose.runtime_runtime", + "androidx.compose.ui_ui-test-junit4", + "androidx.compose.ui_ui-test-manifest", + ], + + kotlincflags: ["-Xjvm-default=enable"], +} diff --git a/packages/SystemUI/compose/gallery/tests/AndroidManifest.xml b/packages/SystemUI/compose/gallery/tests/AndroidManifest.xml new file mode 100644 index 0000000000000..5eeb3ad24e5aa --- /dev/null +++ b/packages/SystemUI/compose/gallery/tests/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/compose/gallery/tests/src/com/android/systemui/compose/gallery/ScreenshotsTests.kt b/packages/SystemUI/compose/gallery/tests/src/com/android/systemui/compose/gallery/ScreenshotsTests.kt new file mode 100644 index 0000000000000..66ecc8d4fde54 --- /dev/null +++ b/packages/SystemUI/compose/gallery/tests/src/com/android/systemui/compose/gallery/ScreenshotsTests.kt @@ -0,0 +1,36 @@ +/* + * 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.systemui.compose.gallery + +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.systemui.compose.theme.SystemUITheme +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ScreenshotsTests { + @get:Rule val composeRule = createComposeRule() + + @Test + fun exampleFeatureScreenshotTest() { + // TODO(b/230832101): Wire this with the screenshot diff testing infra. We should reuse the + // configuration of the features in the gallery app to populate the UIs. + composeRule.setContent { SystemUITheme { ExampleFeatureScreen() } } + } +} diff --git a/packages/SystemUI/compose/testing/Android.bp b/packages/SystemUI/compose/testing/Android.bp new file mode 100644 index 0000000000000..293e51f68b98a --- /dev/null +++ b/packages/SystemUI/compose/testing/Android.bp @@ -0,0 +1,43 @@ +// 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 { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"], +} + +android_library { + name: "SystemUIComposeTesting", + manifest: "AndroidManifest.xml", + + srcs: [ + "src/**/*.kt", + ], + + static_libs: [ + "SystemUIComposeCore", + "SystemUIScreenshotLib", + + "androidx.compose.runtime_runtime", + "androidx.compose.material3_material3", + "androidx.compose.ui_ui-test-junit4", + "androidx.compose.ui_ui-test-manifest", + ], + + kotlincflags: ["-Xjvm-default=all"], +} diff --git a/packages/SystemUI/compose/testing/AndroidManifest.xml b/packages/SystemUI/compose/testing/AndroidManifest.xml new file mode 100644 index 0000000000000..b1f7c3be2796c --- /dev/null +++ b/packages/SystemUI/compose/testing/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/packages/SystemUI/compose/testing/src/com/android/systemui/testing/compose/ComposeScreenshotTestRule.kt b/packages/SystemUI/compose/testing/src/com/android/systemui/testing/compose/ComposeScreenshotTestRule.kt new file mode 100644 index 0000000000000..e611e8bf00687 --- /dev/null +++ b/packages/SystemUI/compose/testing/src/com/android/systemui/testing/compose/ComposeScreenshotTestRule.kt @@ -0,0 +1,89 @@ +/* + * 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.systemui.testing.compose + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.ViewRootForTest +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onRoot +import com.android.systemui.compose.theme.SystemUITheme +import com.android.systemui.testing.screenshot.ScreenshotActivity +import com.android.systemui.testing.screenshot.SystemUIGoldenImagePathManager +import com.android.systemui.testing.screenshot.UnitTestBitmapMatcher +import com.android.systemui.testing.screenshot.drawIntoBitmap +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement +import platform.test.screenshot.DeviceEmulationRule +import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.MaterialYouColorsRule +import platform.test.screenshot.ScreenshotTestRule +import platform.test.screenshot.getEmulatedDevicePathConfig + +/** A rule for Compose screenshot diff tests. */ +class ComposeScreenshotTestRule(emulationSpec: DeviceEmulationSpec) : TestRule { + private val colorsRule = MaterialYouColorsRule() + private val deviceEmulationRule = DeviceEmulationRule(emulationSpec) + private val screenshotRule = + ScreenshotTestRule( + SystemUIGoldenImagePathManager(getEmulatedDevicePathConfig(emulationSpec)) + ) + private val composeRule = createAndroidComposeRule() + private val delegateRule = + RuleChain.outerRule(colorsRule) + .around(deviceEmulationRule) + .around(screenshotRule) + .around(composeRule) + private val matcher = UnitTestBitmapMatcher + + override fun apply(base: Statement, description: Description): Statement { + return delegateRule.apply(base, description) + } + + /** + * Compare [content] with the golden image identified by [goldenIdentifier] in the context of + * [testSpec]. + */ + fun screenshotTest( + goldenIdentifier: String, + content: @Composable () -> Unit, + ) { + // Make sure that the activity draws full screen and fits the whole display instead of the + // system bars. + val activity = composeRule.activity + activity.mainExecutor.execute { activity.window.setDecorFitsSystemWindows(false) } + + // Set the content using the AndroidComposeRule to make sure that the Activity is set up + // correctly. + composeRule.setContent { + SystemUITheme { + Surface( + color = MaterialTheme.colorScheme.background, + ) { + content() + } + } + } + composeRule.waitForIdle() + + val view = (composeRule.onRoot().fetchSemanticsNode().root as ViewRootForTest).view + screenshotRule.assertBitmapAgainstGolden(view.drawIntoBitmap(), goldenIdentifier, matcher) + } +}