Add first widget Preference for SPA
This also including a codelab app to show the demo.
Bug: 235727273
Test: android unit test
Ignore-AOSP-First: new library not in AOSP
Change-Id: Ib4d6e05c24ba10ca7749702524e3c64c114b1471
diff --git a/packages/SettingsLib/Spa/.gitignore b/packages/SettingsLib/Spa/.gitignore
new file mode 100644
index 0000000..b2ed268
--- /dev/null
+++ b/packages/SettingsLib/Spa/.gitignore
@@ -0,0 +1,15 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
diff --git a/packages/SettingsLib/Spa/OWNERS b/packages/SettingsLib/Spa/OWNERS
new file mode 100644
index 0000000..b352b04
--- /dev/null
+++ b/packages/SettingsLib/Spa/OWNERS
@@ -0,0 +1,6 @@
[email protected]
[email protected]
[email protected]
[email protected]
+
+per-file *.xml = set noparent
diff --git a/packages/SettingsLib/Spa/TEST_MAPPING b/packages/SettingsLib/Spa/TEST_MAPPING
new file mode 100644
index 0000000..ef3db4a
--- /dev/null
+++ b/packages/SettingsLib/Spa/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "SpaLibTests"
+ }
+ ]
+}
diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle
new file mode 100644
index 0000000..8c97eca
--- /dev/null
+++ b/packages/SettingsLib/Spa/build.gradle
@@ -0,0 +1,28 @@
+/*
+ * Copyright 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.
+ */
+
+buildscript {
+ ext {
+ minSdk_version = 31
+ compose_version = '1.2.0-alpha04'
+ compose_material3_version = '1.0.0-alpha06'
+ }
+}
+plugins {
+ id 'com.android.application' version '7.3.0-beta04' apply false
+ id 'com.android.library' version '7.3.0-beta04' apply false
+ id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
+}
diff --git a/packages/SettingsLib/Spa/codelab/Android.bp b/packages/SettingsLib/Spa/codelab/Android.bp
new file mode 100644
index 0000000..8fbbf9a
--- /dev/null
+++ b/packages/SettingsLib/Spa/codelab/Android.bp
@@ -0,0 +1,33 @@
+//
+// 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 {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_app {
+ name: "SpaLibCodelab",
+
+ srcs: ["src/**/*.kt"],
+
+ static_libs: [
+ "SpaLib",
+ "androidx.compose.runtime_runtime",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
+ platform_apis: true,
+ min_sdk_version: "31",
+}
diff --git a/packages/SettingsLib/Spa/codelab/AndroidManifest.xml b/packages/SettingsLib/Spa/codelab/AndroidManifest.xml
new file mode 100644
index 0000000..9a89e5e
--- /dev/null
+++ b/packages/SettingsLib/Spa/codelab/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.spa.codelab">
+
+ <application
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:theme="@style/Theme.SettingsLib.Compose.DayNight">
+ <activity
+ android:name="com.android.settingslib.spa.codelab.MainActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/packages/SettingsLib/Spa/codelab/build.gradle b/packages/SettingsLib/Spa/codelab/build.gradle
new file mode 100644
index 0000000..5251ddd
--- /dev/null
+++ b/packages/SettingsLib/Spa/codelab/build.gradle
@@ -0,0 +1,66 @@
+/*
+ * Copyright 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.
+ */
+
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+}
+
+android {
+ namespace 'com.android.settingslib.spa.codelab'
+ compileSdk 33
+
+ defaultConfig {
+ applicationId "com.android.settingslib.spa.codelab"
+ minSdk minSdk_version
+ targetSdk 33
+ versionCode 1
+ versionName "1.0"
+ }
+
+ sourceSets {
+ main {
+ kotlin {
+ srcDir "src"
+ }
+ res.srcDirs = ["res"]
+ manifest.srcFile "AndroidManifest.xml"
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ freeCompilerArgs = ["-Xjvm-default=all"]
+ }
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion compose_version
+ }
+ packagingOptions {
+ resources {
+ excludes += '/META-INF/{AL2.0,LGPL2.1}'
+ }
+ }
+}
+
+dependencies {
+ implementation(project(":spa"))
+}
diff --git a/packages/SettingsLib/Spa/codelab/res/values/strings.xml b/packages/SettingsLib/Spa/codelab/res/values/strings.xml
new file mode 100644
index 0000000..6fdbba0
--- /dev/null
+++ b/packages/SettingsLib/Spa/codelab/res/values/strings.xml
@@ -0,0 +1,19 @@
+<!--
+ 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.
+ -->
+<resources>
+ <!-- Codelab App name. [DO NOT TRANSLATE] -->
+ <string name="app_name" translatable="false">SpaLib Codelab</string>
+</resources>
diff --git a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/HomeScreen.kt b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/HomeScreen.kt
new file mode 100644
index 0000000..574857f
--- /dev/null
+++ b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/HomeScreen.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.settingslib.spa.codelab
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.onClickNavigateTo
+import com.android.settingslib.spa.theme.SettingsDimension
+import com.android.settingslib.spa.theme.SettingsTheme
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+
+@Composable
+fun HomeScreen() {
+ Column {
+ Text(
+ text = stringResource(R.string.app_name),
+ modifier = Modifier.padding(SettingsDimension.itemPadding),
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.headlineMedium,
+ )
+
+ Preference(object : PreferenceModel {
+ override val title = "Preference"
+ override val onClick: (() -> Unit) = onClickNavigateTo(Destinations.Preference)
+ })
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun HomeScreenPreview() {
+ SettingsTheme {
+ HomeScreen()
+ }
+}
diff --git a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/MainActivity.kt b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/MainActivity.kt
new file mode 100644
index 0000000..8ecc2a2
--- /dev/null
+++ b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/MainActivity.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.settingslib.spa.codelab
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import com.android.settingslib.spa.framework.localNavController
+import com.android.settingslib.spa.theme.SettingsTheme
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ MainContent()
+ }
+ }
+}
+
+object Destinations {
+ const val Home = "Home"
+ const val Preference = "Preference"
+}
+
+@Composable
+private fun MainContent() {
+ SettingsTheme {
+ val navController = rememberNavController()
+ CompositionLocalProvider(navController.localNavController()) {
+ NavHost(
+ navController = navController,
+ startDestination = Destinations.Home,
+ ) {
+ composable(Destinations.Home) { HomeScreen() }
+ composable(Destinations.Preference) { PreferenceCodelab() }
+ }
+ }
+ }
+}
diff --git a/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/PreferenceCodelab.kt b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/PreferenceCodelab.kt
new file mode 100644
index 0000000..4e24aa1
--- /dev/null
+++ b/packages/SettingsLib/Spa/codelab/src/com/android/settingslib/spa/codelab/PreferenceCodelab.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.settingslib.spa.codelab
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.toState
+import com.android.settingslib.spa.theme.SettingsTheme
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import kotlinx.coroutines.delay
+
+@Composable
+fun PreferenceCodelab() {
+ Column(Modifier.verticalScroll(rememberScrollState())) {
+ Preference(object : PreferenceModel {
+ override val title = "Preference"
+ })
+
+ Preference(object : PreferenceModel {
+ override val title = "Preference"
+ override val summary = "With summary".toState()
+ })
+
+ Preference(object : PreferenceModel {
+ override val title = "Preference"
+ override val summary = produceState(initialValue = " ") {
+ delay(1000L)
+ value = "Async summary"
+ }
+ })
+
+ var count by remember { mutableStateOf(0) }
+ Preference(object : PreferenceModel {
+ override val title = "Click me"
+ override val summary = derivedStateOf { count.toString() }
+ override val onClick: (() -> Unit) = { count++ }
+ })
+
+ var ticks by remember { mutableStateOf(0) }
+ LaunchedEffect(ticks) {
+ delay(1000L)
+ ticks++
+ }
+ Preference(object : PreferenceModel {
+ override val title = "Ticker"
+ override val summary = derivedStateOf { ticks.toString() }
+ })
+
+ Preference(object : PreferenceModel {
+ override val title = "Disabled"
+ override val summary = "Disabled".toState()
+ override val enabled = false.toState()
+ })
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun PreferenceCodelabPreview() {
+ SettingsTheme {
+ PreferenceCodelab()
+ }
+}
diff --git a/packages/SettingsLib/Spa/gradle.properties b/packages/SettingsLib/Spa/gradle.properties
new file mode 100644
index 0000000..82bd512
--- /dev/null
+++ b/packages/SettingsLib/Spa/gradle.properties
@@ -0,0 +1,39 @@
+#
+# 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.
+#
+
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.jar b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
--- /dev/null
+++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..52095e1
--- /dev/null
+++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+
+#Thu Jul 14 10:36:06 CST 2022
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/packages/SettingsLib/Spa/gradlew b/packages/SettingsLib/Spa/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/packages/SettingsLib/Spa/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# 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
+#
+# https://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.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/packages/SettingsLib/Spa/gradlew.bat b/packages/SettingsLib/Spa/gradlew.bat
new file mode 100644
index 0000000..107acd32
--- /dev/null
+++ b/packages/SettingsLib/Spa/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/packages/SettingsLib/Spa/settings.gradle b/packages/SettingsLib/Spa/settings.gradle
new file mode 100644
index 0000000..1b69c1e
--- /dev/null
+++ b/packages/SettingsLib/Spa/settings.gradle
@@ -0,0 +1,34 @@
+/*
+ * Copyright 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.
+ */
+
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+rootProject.name = "SpaLib"
+include ':spa'
+include ':codelab'
+include ':tests'
diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp
new file mode 100644
index 0000000..463c076
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/Android.bp
@@ -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 {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_library {
+ name: "SpaLib",
+
+ srcs: ["src/**/*.kt"],
+
+ static_libs: [
+ "androidx.compose.material3_material3",
+ "androidx.compose.material_material-icons-extended",
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.ui_ui-tooling-preview",
+ "androidx.navigation_navigation-compose",
+ "com.google.android.material_material",
+ ],
+ kotlincflags: ["-Xjvm-default=all"],
+ min_sdk_version: "31",
+}
diff --git a/packages/SettingsLib/Spa/spa/AndroidManifest.xml b/packages/SettingsLib/Spa/spa/AndroidManifest.xml
new file mode 100644
index 0000000..410bcdb
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<manifest package="com.android.settingslib.spa" />
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
new file mode 100644
index 0000000..60794c8
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/build.gradle
@@ -0,0 +1,69 @@
+/*
+ * Copyright 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.
+ */
+
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+}
+
+android {
+ namespace 'com.android.settingslib.spa'
+ compileSdk 33
+
+ defaultConfig {
+ minSdk minSdk_version
+ targetSdk 33
+ }
+
+ sourceSets {
+ main {
+ kotlin {
+ srcDir "src"
+ }
+ res.srcDirs = ["res"]
+ manifest.srcFile "AndroidManifest.xml"
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ freeCompilerArgs = ["-Xjvm-default=all"]
+ }
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion compose_version
+ }
+ packagingOptions {
+ resources {
+ excludes += '/META-INF/{AL2.0,LGPL2.1}'
+ }
+ }
+}
+
+dependencies {
+ api "androidx.compose.material3:material3:$compose_material3_version"
+ api "androidx.compose.material:material-icons-extended:$compose_version"
+ api "androidx.compose.runtime:runtime-livedata:$compose_version"
+ api "androidx.compose.ui:ui-tooling-preview:$compose_version"
+ api 'androidx.navigation:navigation-compose:2.5.0'
+ api 'com.google.android.material:material:1.6.1'
+ debugApi "androidx.compose.ui:ui-tooling:$compose_version"
+}
diff --git a/packages/SettingsLib/Spa/spa/res/values-night/themes.xml b/packages/SettingsLib/Spa/spa/res/values-night/themes.xml
new file mode 100644
index 0000000..8b52b50
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/res/values-night/themes.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+<resources>
+
+ <style name="Theme.SettingsLib.Compose.DayNight" />
+</resources>
diff --git a/packages/SettingsLib/Spa/spa/res/values/themes.xml b/packages/SettingsLib/Spa/spa/res/values/themes.xml
new file mode 100644
index 0000000..01f9ea5
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/res/values/themes.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+ -->
+<resources>
+
+ <style name="Theme.SettingsLib.Compose" parent="Theme.Material3.DayNight.NoActionBar">
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ </style>
+
+ <style name="Theme.SettingsLib.Compose.DayNight">
+ <item name="android:windowLightStatusBar">true</item>
+ </style>
+</resources>
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/NavControllerWrapper.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/NavControllerWrapper.kt
new file mode 100644
index 0000000..55bc78b
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/NavControllerWrapper.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.settingslib.spa.framework
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.remember
+import androidx.navigation.NavHostController
+
+interface NavControllerWrapper {
+ fun navigate(route: String)
+ fun navigateUp()
+}
+
+@Composable
+fun NavHostController.localNavController() =
+ LocalNavController provides remember { NavControllerWrapperImpl(this) }
+
+val LocalNavController = compositionLocalOf<NavControllerWrapper> {
+ object : NavControllerWrapper {
+ override fun navigate(route: String) {}
+
+ override fun navigateUp() {}
+ }
+}
+
+@Composable
+fun onClickNavigateTo(route: String): () -> Unit {
+ val navController = LocalNavController.current
+ return { navController.navigate(route) }
+}
+
+internal class NavControllerWrapperImpl(
+ private val navController: NavHostController,
+) : NavControllerWrapper {
+ override fun navigate(route: String) {
+ navController.navigate(route)
+ }
+
+ override fun navigateUp() {
+ navController.navigateUp()
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/RuntimeUtils.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/RuntimeUtils.kt
new file mode 100644
index 0000000..22d85e0
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/RuntimeUtils.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.settingslib.spa.framework
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.remember
+
+/**
+ * Remember the [State] initialized with the [this].
+ */
+@Composable
+fun <T> T.toState(): State<T> = remember { stateOf(this) }
+
+/**
+ * Return a new [State] initialized with the passed in [value].
+ */
+fun <T> stateOf(value: T) = object : State<T> {
+ override val value = value
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/theme/MaterialColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/theme/MaterialColors.kt
new file mode 100644
index 0000000..8cd9184
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/theme/MaterialColors.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.settingslib.spa.theme
+
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+
+@Composable
+internal fun materialColorScheme(isDarkTheme: Boolean): ColorScheme {
+ val context = LocalContext.current
+ return remember(isDarkTheme) {
+ when {
+ isDarkTheme -> dynamicDarkColorScheme(context)
+ else -> dynamicLightColorScheme(context)
+ }
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/theme/SettingsDimension.kt
new file mode 100644
index 0000000..51e75c5
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/theme/SettingsDimension.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.settingslib.spa.theme
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.ui.unit.dp
+
+object SettingsDimension {
+ val itemIconContainerSize = 72.dp
+ val itemPaddingStart = 24.dp
+ val itemPaddingEnd = 16.dp
+ val itemPaddingVertical = 16.dp
+ val itemPadding = PaddingValues(
+ start = itemPaddingStart,
+ top = itemPaddingVertical,
+ end = itemPaddingEnd,
+ bottom = itemPaddingVertical,
+ )
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/theme/SettingsOpacity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/theme/SettingsOpacity.kt
new file mode 100644
index 0000000..5f4da8a
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/theme/SettingsOpacity.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.settingslib.spa.theme
+
+object SettingsOpacity {
+ const val Full = 1f
+ const val Disabled = 0.38f
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/theme/SettingsTheme.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/theme/SettingsTheme.kt
new file mode 100644
index 0000000..fce9f2b
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/theme/SettingsTheme.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.settingslib.spa.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+
+/**
+ * The Material 3 Theme for Settings.
+ */
+@Composable
+fun SettingsTheme(content: @Composable () -> Unit) {
+ val isDarkTheme = isSystemInDarkTheme()
+ val colorScheme = materialColorScheme(isDarkTheme)
+
+ MaterialTheme(colorScheme = colorScheme) {
+ content()
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BasePreference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BasePreference.kt
new file mode 100644
index 0000000..7476f4e
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BasePreference.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.settingslib.spa.widget.preference
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+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.material.icons.Icons
+import androidx.compose.material.icons.outlined.BatteryChargingFull
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.Dp
+import com.android.settingslib.spa.framework.toState
+import com.android.settingslib.spa.theme.SettingsDimension
+import com.android.settingslib.spa.theme.SettingsOpacity
+import com.android.settingslib.spa.theme.SettingsTheme
+
+@Composable
+internal fun BasePreference(
+ title: String,
+ summary: State<String>,
+ modifier: Modifier = Modifier,
+ icon: (@Composable () -> Unit)? = null,
+ enabled: State<Boolean> = true.toState(),
+ paddingStart: Dp = SettingsDimension.itemPaddingStart,
+ paddingEnd: Dp = SettingsDimension.itemPaddingEnd,
+ paddingVertical: Dp = SettingsDimension.itemPaddingVertical,
+ widget: @Composable () -> Unit = {},
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(end = paddingEnd),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ val alphaModifier =
+ Modifier.alpha(if (enabled.value) SettingsOpacity.Full else SettingsOpacity.Disabled)
+ if (icon != null) {
+ Box(
+ modifier = alphaModifier.size(SettingsDimension.itemIconContainerSize),
+ contentAlignment = Alignment.Center,
+ ) {
+ icon()
+ }
+ } else {
+ Spacer(modifier = Modifier.width(width = paddingStart))
+ }
+ TitleAndSummary(
+ title = title,
+ summary = summary,
+ modifier = alphaModifier
+ .weight(1f)
+ .padding(vertical = paddingVertical),
+ )
+ widget()
+ }
+}
+
+// Extracts a scope to avoid frequent recompose outside scope.
+@Composable
+private fun TitleAndSummary(title: String, summary: State<String>, modifier: Modifier) {
+ Column(modifier) {
+ Text(
+ text = title,
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.titleMedium,
+ )
+ if (summary.value.isNotEmpty()) {
+ Text(
+ text = summary.value,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun BasePreferencePreview() {
+ SettingsTheme {
+ BasePreference(
+ title = "Screen Saver",
+ summary = "Clock".toState(),
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun BasePreferenceIconPreview() {
+ SettingsTheme {
+ BasePreference(
+ title = "Screen Saver",
+ summary = "Clock".toState(),
+ icon = {
+ Icon(imageVector = Icons.Outlined.BatteryChargingFull, contentDescription = null)
+ },
+ )
+ }
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
new file mode 100644
index 0000000..ee20280
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/Preference.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.settingslib.spa.widget.preference
+
+import androidx.compose.foundation.clickable
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.ui.Modifier
+import com.android.settingslib.spa.framework.stateOf
+
+/**
+ * The widget model for [Preference] widget.
+ */
+interface PreferenceModel {
+ /**
+ * The title of this [Preference].
+ */
+ val title: String
+
+ /**
+ * The summary of this [Preference].
+ */
+ val summary: State<String>
+ get() = stateOf("")
+
+ /**
+ * Indicates whether this [Preference] is enabled.
+ *
+ * Disabled [Preference] will be displayed in disabled style.
+ */
+ val enabled: State<Boolean>
+ get() = stateOf(true)
+
+ /**
+ * The on click handler of this [Preference].
+ *
+ * This also indicates whether this [Preference] is clickable.
+ */
+ val onClick: (() -> Unit)?
+ get() = null
+}
+
+/**
+ * Preference widget.
+ *
+ * Data is provided through [PreferenceModel].
+ */
+@Composable
+fun Preference(model: PreferenceModel) {
+ val modifier = model.onClick?.let { onClick ->
+ Modifier.clickable(enabled = model.enabled.value) { onClick() }
+ } ?: Modifier
+ BasePreference(
+ title = model.title,
+ summary = model.summary,
+ modifier = modifier,
+ enabled = model.enabled,
+ )
+}
diff --git a/packages/SettingsLib/Spa/tests/Android.bp b/packages/SettingsLib/Spa/tests/Android.bp
new file mode 100644
index 0000000..037d8c4
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/Android.bp
@@ -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 {
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+ name: "SpaLibTests",
+ test_suites: ["device-tests"],
+
+ srcs: ["src/**/*.kt"],
+
+ static_libs: [
+ "SpaLib",
+ "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=all"],
+}
diff --git a/packages/SettingsLib/Spa/tests/AndroidManifest.xml b/packages/SettingsLib/Spa/tests/AndroidManifest.xml
new file mode 100644
index 0000000..c224caf
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.settingslib.spa.tests">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:label="Tests for SpaLib"
+ android:targetPackage="com.android.settingslib.spa.tests">
+ </instrumentation>
+</manifest>
diff --git a/packages/SettingsLib/Spa/tests/build.gradle b/packages/SettingsLib/Spa/tests/build.gradle
new file mode 100644
index 0000000..707017e
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/build.gradle
@@ -0,0 +1,67 @@
+/*
+ * Copyright 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.
+ */
+
+plugins {
+ id 'com.android.library'
+ id 'kotlin-android'
+}
+
+android {
+ namespace 'com.android.settingslib.spa.tests'
+ compileSdk 33
+
+ defaultConfig {
+ minSdk minSdk_version
+ targetSdk 33
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ sourceSets {
+ androidTest {
+ kotlin {
+ srcDir "src"
+ }
+ manifest.srcFile "AndroidManifest.xml"
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ freeCompilerArgs = ["-Xjvm-default=all"]
+ }
+ buildFeatures {
+ compose true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion compose_version
+ }
+ packagingOptions {
+ resources {
+ excludes += '/META-INF/{AL2.0,LGPL2.1}'
+ }
+ }
+}
+
+dependencies {
+ androidTestImplementation(project(":spa"))
+ androidTestImplementation 'androidx.test.ext:junit-ktx:1.1.3'
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
+ androidTestDebugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/PreferenceTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/PreferenceTest.kt
new file mode 100644
index 0000000..4097946
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/preference/PreferenceTest.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.settingslib.spa.widget.preference
+
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+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 com.android.settingslib.spa.framework.toState
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class PreferenceTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun title_displayed() {
+ composeTestRule.setContent {
+ Preference(object : PreferenceModel {
+ override val title = "Preference"
+ })
+ }
+
+ composeTestRule.onNodeWithText("Preference").assertIsDisplayed()
+ }
+
+ @Test
+ fun click_enabled_withEffect() {
+ composeTestRule.setContent {
+ var count by remember { mutableStateOf(0) }
+ Preference(object : PreferenceModel {
+ override val title = "Preference"
+ override val summary = derivedStateOf { count.toString() }
+ override val onClick: (() -> Unit) = { count++ }
+ })
+ }
+
+ composeTestRule.onNodeWithText("Preference").performClick()
+ composeTestRule.onNodeWithText("1").assertIsDisplayed()
+ }
+
+ @Test
+ fun click_disabled_noEffect() {
+ composeTestRule.setContent {
+ var count by remember { mutableStateOf(0) }
+ Preference(object : PreferenceModel {
+ override val title = "Preference"
+ override val summary = derivedStateOf { count.toString() }
+ override val enabled = false.toState()
+ override val onClick: (() -> Unit) = { count++ }
+ })
+ }
+
+ composeTestRule.onNodeWithText("Preference").performClick()
+ composeTestRule.onNodeWithText("0").assertIsDisplayed()
+ }
+}