Create SpaPrivileged Lib

If a functionality cannot be put into Spa Lib because one of:
- Using private API
- Depends on SettingsLib

Then the functionality can be put into the SpaPrivileged Lib.

Add AppInfo widget as example.

Bug: 235727273
Test: Manual with Test App
Change-Id: I5e711e0a9067314819c7f4ba86764f25d0060239
diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle
index 8c97eca..d380136 100644
--- a/packages/SettingsLib/Spa/build.gradle
+++ b/packages/SettingsLib/Spa/build.gradle
@@ -16,9 +16,9 @@
 
 buildscript {
     ext {
-        minSdk_version = 31
-        compose_version = '1.2.0-alpha04'
-        compose_material3_version = '1.0.0-alpha06'
+        spa_min_sdk = 31
+        jetpack_compose_version = '1.2.0-alpha04'
+        jetpack_compose_material3_version = '1.0.0-alpha06'
     }
 }
 plugins {
diff --git a/packages/SettingsLib/Spa/codelab/AndroidManifest.xml b/packages/SettingsLib/Spa/codelab/AndroidManifest.xml
index 9a89e5e..36b9313 100644
--- a/packages/SettingsLib/Spa/codelab/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/codelab/AndroidManifest.xml
@@ -13,14 +13,14 @@
   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">
+        android:theme="@style/Theme.SpaLib.DayNight">
         <activity
             android:name="com.android.settingslib.spa.codelab.MainActivity"
             android:exported="true">
diff --git a/packages/SettingsLib/Spa/codelab/build.gradle b/packages/SettingsLib/Spa/codelab/build.gradle
index 5251ddd..169ecf0 100644
--- a/packages/SettingsLib/Spa/codelab/build.gradle
+++ b/packages/SettingsLib/Spa/codelab/build.gradle
@@ -25,7 +25,7 @@
 
     defaultConfig {
         applicationId "com.android.settingslib.spa.codelab"
-        minSdk minSdk_version
+        minSdk spa_min_sdk
         targetSdk 33
         versionCode 1
         versionName "1.0"
@@ -52,7 +52,7 @@
         compose true
     }
     composeOptions {
-        kotlinCompilerExtensionVersion compose_version
+        kotlinCompilerExtensionVersion jetpack_compose_version
     }
     packagingOptions {
         resources {
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
index 60794c8..62f74d0 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle
+++ b/packages/SettingsLib/Spa/spa/build.gradle
@@ -24,7 +24,7 @@
     compileSdk 33
 
     defaultConfig {
-        minSdk minSdk_version
+        minSdk spa_min_sdk
         targetSdk 33
     }
 
@@ -49,7 +49,7 @@
         compose true
     }
     composeOptions {
-        kotlinCompilerExtensionVersion compose_version
+        kotlinCompilerExtensionVersion jetpack_compose_version
     }
     packagingOptions {
         resources {
@@ -59,11 +59,11 @@
 }
 
 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.compose.material3:material3:$jetpack_compose_material3_version"
+    api "androidx.compose.material:material-icons-extended:$jetpack_compose_version"
+    api "androidx.compose.runtime:runtime-livedata:$jetpack_compose_version"
+    api "androidx.compose.ui:ui-tooling-preview:$jetpack_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"
+    debugApi "androidx.compose.ui:ui-tooling:$jetpack_compose_version"
 }
diff --git a/packages/SettingsLib/Spa/spa/res/values-night/themes.xml b/packages/SettingsLib/Spa/spa/res/values-night/themes.xml
index 8b52b50..67dd2b0 100644
--- a/packages/SettingsLib/Spa/spa/res/values-night/themes.xml
+++ b/packages/SettingsLib/Spa/spa/res/values-night/themes.xml
@@ -13,8 +13,8 @@
   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" />
+    <style name="Theme.SpaLib.DayNight" />
 </resources>
diff --git a/packages/SettingsLib/Spa/spa/res/values/themes.xml b/packages/SettingsLib/Spa/spa/res/values/themes.xml
index 01f9ea5..e0e5fc2 100644
--- a/packages/SettingsLib/Spa/spa/res/values/themes.xml
+++ b/packages/SettingsLib/Spa/spa/res/values/themes.xml
@@ -13,15 +13,15 @@
   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">
+    <style name="Theme.SpaLib" 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">
+    <style name="Theme.SpaLib.DayNight">
         <item name="android:windowLightStatusBar">true</item>
     </style>
 </resources>
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/DrawablePainter.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/DrawablePainter.kt
new file mode 100644
index 0000000..ae325f8
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/DrawablePainter.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.compose
+
+import android.graphics.drawable.Animatable
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.Drawable
+import android.os.Handler
+import android.os.Looper
+import android.view.View
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.RememberObserver
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.asAndroidColorFilter
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.graphics.painter.ColorPainter
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.withSave
+import androidx.compose.ui.unit.LayoutDirection
+import kotlin.math.roundToInt
+
+/**
+ * *************************************************************************************************
+ * This file was forked from
+ * https://github.com/google/accompanist/blob/main/drawablepainter/src/main/java/com/google/accompanist/drawablepainter/DrawablePainter.kt
+ * and will be removed once it lands in AndroidX.
+ */
+
+private val MAIN_HANDLER by lazy(LazyThreadSafetyMode.NONE) {
+    Handler(Looper.getMainLooper())
+}
+
+/**
+ * A [Painter] which draws an Android [Drawable] and supports [Animatable] drawables. Instances
+ * should be remembered to be able to start and stop [Animatable] animations.
+ *
+ * Instances are usually retrieved from [rememberDrawablePainter].
+ */
+class DrawablePainter(
+    val drawable: Drawable
+) : Painter(), RememberObserver {
+    private var drawInvalidateTick by mutableStateOf(0)
+    private var drawableIntrinsicSize by mutableStateOf(drawable.intrinsicSize)
+
+    private val callback: Drawable.Callback by lazy {
+        object : Drawable.Callback {
+            override fun invalidateDrawable(d: Drawable) {
+                // Update the tick so that we get re-drawn
+                drawInvalidateTick++
+                // Update our intrinsic size too
+                drawableIntrinsicSize = drawable.intrinsicSize
+            }
+
+            override fun scheduleDrawable(d: Drawable, what: Runnable, time: Long) {
+                MAIN_HANDLER.postAtTime(what, time)
+            }
+
+            override fun unscheduleDrawable(d: Drawable, what: Runnable) {
+                MAIN_HANDLER.removeCallbacks(what)
+            }
+        }
+    }
+
+    init {
+        if (drawable.intrinsicWidth >= 0 && drawable.intrinsicHeight >= 0) {
+            // Update the drawable's bounds to match the intrinsic size
+            drawable.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight)
+        }
+    }
+
+    override fun onRemembered() {
+        drawable.callback = callback
+        drawable.setVisible(true, true)
+        if (drawable is Animatable) drawable.start()
+    }
+
+    override fun onAbandoned() = onForgotten()
+
+    override fun onForgotten() {
+        if (drawable is Animatable) drawable.stop()
+        drawable.setVisible(false, false)
+        drawable.callback = null
+    }
+
+    override fun applyAlpha(alpha: Float): Boolean {
+        drawable.alpha = (alpha * 255).roundToInt().coerceIn(0, 255)
+        return true
+    }
+
+    override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
+        drawable.colorFilter = colorFilter?.asAndroidColorFilter()
+        return true
+    }
+
+    override fun applyLayoutDirection(layoutDirection: LayoutDirection): Boolean =
+        drawable.setLayoutDirection(
+            when (layoutDirection) {
+                LayoutDirection.Ltr -> View.LAYOUT_DIRECTION_LTR
+                LayoutDirection.Rtl -> View.LAYOUT_DIRECTION_RTL
+            }
+        )
+
+    override val intrinsicSize: Size get() = drawableIntrinsicSize
+
+    override fun DrawScope.onDraw() {
+        drawIntoCanvas { canvas ->
+            // Reading this ensures that we invalidate when invalidateDrawable() is called
+            drawInvalidateTick
+
+            // Update the Drawable's bounds
+            drawable.setBounds(0, 0, size.width.roundToInt(), size.height.roundToInt())
+
+            canvas.withSave {
+                drawable.draw(canvas.nativeCanvas)
+            }
+        }
+    }
+}
+
+/**
+ * Remembers [Drawable] wrapped up as a [Painter]. This function attempts to un-wrap the
+ * drawable contents and use Compose primitives where possible.
+ *
+ * If the provided [drawable] is `null`, an empty no-op painter is returned.
+ *
+ * This function tries to dispatch lifecycle events to [drawable] as much as possible from
+ * within Compose.
+ *
+ * @sample com.google.accompanist.sample.drawablepainter.BasicSample
+ */
+@Composable
+fun rememberDrawablePainter(drawable: Drawable?): Painter = remember(drawable) {
+    when (drawable) {
+        null -> EmptyPainter
+        is BitmapDrawable -> BitmapPainter(drawable.bitmap.asImageBitmap())
+        is ColorDrawable -> ColorPainter(Color(drawable.color))
+        // Since the DrawablePainter will be remembered and it implements RememberObserver, it
+        // will receive the necessary events
+        else -> DrawablePainter(drawable.mutate())
+    }
+}
+
+private val Drawable.intrinsicSize: Size
+    get() = when {
+        // Only return a finite size if the drawable has an intrinsic size
+        intrinsicWidth >= 0 && intrinsicHeight >= 0 -> {
+            Size(width = intrinsicWidth.toFloat(), height = intrinsicHeight.toFloat())
+        }
+        else -> Size.Unspecified
+    }
+
+internal object EmptyPainter : Painter() {
+    override val intrinsicSize: Size get() = Size.Unspecified
+    override fun DrawScope.onDraw() {}
+}
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/RuntimeUtils.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/RuntimeUtils.kt
index 7c8608d..ba88546 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/RuntimeUtils.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/RuntimeUtils.kt
@@ -16,9 +16,17 @@
 
 package com.android.settingslib.spa.framework.compose
 
+import android.content.Context
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.State
 import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+
+@Composable
+fun <T> rememberContext(constructor: (Context) -> T): T {
+    val context = LocalContext.current
+    return remember(context) { constructor(context) }
+}
 
 /**
  * Remember the [State] initialized with the [this].
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
index 6bdc294..9a34dbf 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt
@@ -25,8 +25,6 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.material3.Divider
-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
@@ -39,6 +37,7 @@
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsOpacity
 import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.ui.SettingsTitle
 
 @Composable
 internal fun BaseLayout(
@@ -94,11 +93,7 @@
 @Composable
 private fun Titles(title: String, subTitle: @Composable () -> Unit, modifier: Modifier) {
     Column(modifier) {
-        Text(
-            text = title,
-            color = MaterialTheme.colorScheme.onSurface,
-            style = MaterialTheme.typography.titleMedium,
-        )
+        SettingsTitle(title)
         subTitle()
     }
 }
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
index 3b99d36..4b2c8e4 100644
--- 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
@@ -19,8 +19,6 @@
 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.Modifier
@@ -29,6 +27,7 @@
 import com.android.settingslib.spa.framework.compose.toState
 import com.android.settingslib.spa.framework.theme.SettingsDimension
 import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.ui.SettingsBody
 
 @Composable
 internal fun BasePreference(
@@ -44,15 +43,7 @@
 ) {
     BaseLayout(
         title = title,
-        subTitle = {
-            if (summary.value.isNotEmpty()) {
-                Text(
-                    text = summary.value,
-                    color = MaterialTheme.colorScheme.onSurfaceVariant,
-                    style = MaterialTheme.typography.bodyMedium,
-                )
-            }
-        },
+        subTitle = { SettingsBody(summary) },
         modifier = modifier,
         icon = icon,
         enabled = enabled,
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
new file mode 100644
index 0000000..a414c89
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.ui
+
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+
+@Composable
+fun SettingsTitle(title: State<String>) {
+    SettingsTitle(title.value)
+}
+
+@Composable
+fun SettingsTitle(title: String) {
+    Text(
+        text = title,
+        color = MaterialTheme.colorScheme.onSurface,
+        style = MaterialTheme.typography.titleMedium,
+    )
+}
+
+@Composable
+fun SettingsBody(body: State<String>) {
+    SettingsBody(body.value)
+}
+
+@Composable
+fun SettingsBody(body: String) {
+    if (body.isNotEmpty()) {
+        Text(
+            text = body,
+            color = MaterialTheme.colorScheme.onSurfaceVariant,
+            style = MaterialTheme.typography.bodyMedium,
+        )
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/build.gradle b/packages/SettingsLib/Spa/tests/build.gradle
index 707017e..be5a5ec 100644
--- a/packages/SettingsLib/Spa/tests/build.gradle
+++ b/packages/SettingsLib/Spa/tests/build.gradle
@@ -24,7 +24,7 @@
     compileSdk 33
 
     defaultConfig {
-        minSdk minSdk_version
+        minSdk spa_min_sdk
         targetSdk 33
 
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -50,7 +50,7 @@
         compose true
     }
     composeOptions {
-        kotlinCompilerExtensionVersion compose_version
+        kotlinCompilerExtensionVersion jetpack_compose_version
     }
     packagingOptions {
         resources {
@@ -62,6 +62,6 @@
 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"
+    androidTestImplementation("androidx.compose.ui:ui-test-junit4:$jetpack_compose_version")
+    androidTestDebugImplementation "androidx.compose.ui:ui-test-manifest:$jetpack_compose_version"
 }
diff --git a/packages/SettingsLib/SpaPrivileged/Android.bp b/packages/SettingsLib/SpaPrivileged/Android.bp
new file mode 100644
index 0000000..48f7ff2
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/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_library {
+    name: "SpaPrivilegedLib",
+
+    srcs: ["src/**/*.kt"],
+
+    static_libs: [
+        "SpaLib",
+        "SettingsLib",
+        "androidx.compose.runtime_runtime",
+    ],
+    kotlincflags: ["-Xjvm-default=all"],
+    min_sdk_version: "31",
+}
diff --git a/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml b/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml
new file mode 100644
index 0000000..2efa107
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?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.spaprivileged" />
diff --git a/packages/SettingsLib/SpaPrivileged/OWNERS b/packages/SettingsLib/SpaPrivileged/OWNERS
new file mode 100644
index 0000000..9256ca5
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/OWNERS
@@ -0,0 +1 @@
+include platform/frameworks/base:/packages/SettingsLib/Spa/OWNERS
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRepository.kt
new file mode 100644
index 0000000..a6378ef
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/AppRepository.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.spaprivileged.framework.app
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.graphics.drawable.Drawable
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.produceState
+import com.android.settingslib.Utils
+import com.android.settingslib.spa.framework.compose.rememberContext
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+@Composable
+fun rememberAppRepository(): AppRepository = rememberContext(::AppRepositoryImpl)
+
+interface AppRepository {
+    @Composable
+    fun produceLabel(app: ApplicationInfo): State<String>
+
+    @Composable
+    fun produceIcon(app: ApplicationInfo): State<Drawable?>
+}
+
+private class AppRepositoryImpl(private val context: Context) : AppRepository {
+    private val packageManager = context.packageManager
+
+    @Composable
+    override fun produceLabel(app: ApplicationInfo) = produceState(initialValue = "", app) {
+        withContext(Dispatchers.Default) {
+            value = app.loadLabel(packageManager).toString()
+        }
+    }
+
+    @Composable
+    override fun produceIcon(app: ApplicationInfo) =
+        produceState<Drawable?>(initialValue = null, app) {
+            withContext(Dispatchers.Default) {
+                value = Utils.getBadgedIcon(context, app)
+            }
+        }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/PackageManagers.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/PackageManagers.kt
new file mode 100644
index 0000000..5a3e666
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/app/PackageManagers.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.spaprivileged.framework.app
+
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+
+object PackageManagers {
+    fun getPackageInfoAsUser(packageName: String, userId: Int): PackageInfo =
+        PackageManager.getPackageInfoAsUserCached(packageName, 0, userId)
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
new file mode 100644
index 0000000..5ae514c
--- /dev/null
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.spaprivileged.template.app
+
+import android.content.pm.ApplicationInfo
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.android.settingslib.spa.framework.compose.rememberDrawablePainter
+import com.android.settingslib.spa.widget.ui.SettingsBody
+import com.android.settingslib.spa.widget.ui.SettingsTitle
+import com.android.settingslib.spaprivileged.framework.app.PackageManagers
+import com.android.settingslib.spaprivileged.framework.app.rememberAppRepository
+
+@Composable
+fun AppInfo(packageName: String, userId: Int) {
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(16.dp),
+        horizontalAlignment = Alignment.CenterHorizontally) {
+        val packageInfo = remember { PackageManagers.getPackageInfoAsUser(packageName, userId) }
+        Box(modifier = Modifier.padding(8.dp)) {
+            AppIcon(app = packageInfo.applicationInfo, size = 48)
+        }
+        AppLabel(packageInfo.applicationInfo)
+        Spacer(modifier = Modifier.height(4.dp))
+        SettingsBody(packageInfo.versionName)
+    }
+}
+
+@Composable
+fun AppIcon(app: ApplicationInfo, size: Int) {
+    val appRepository = rememberAppRepository()
+    Image(
+        painter = rememberDrawablePainter(appRepository.produceIcon(app).value),
+        contentDescription = null,
+        modifier = Modifier.size(size.dp)
+    )
+}
+
+@Composable
+fun AppLabel(app: ApplicationInfo) {
+    val appRepository = rememberAppRepository()
+    SettingsTitle(appRepository.produceLabel(app))
+}