Add AppStoragePreference for Spa

Also add new SettingsSpaUnitTests.

Bug: 236346018
Test: Manual with App Info page
Test: atest SettingsSpaUnitTests
Test: Manual compare generated Settings AndroidManifest.xml
Change-Id: I9f6b2ca446fd3d196792a876a6e4049c5cf97a1d
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index a351fb4..f5da15f 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -3555,7 +3555,7 @@
 
         <provider
             android:name="androidx.core.content.FileProvider"
-            android:authorities="com.android.settings.files"
+            android:authorities="${applicationId}.files"
             android:grantUriPermissions="true"
             android:exported="false">
             <meta-data
@@ -3565,13 +3565,13 @@
 
         <provider
             android:name=".deviceinfo.legal.ModuleLicenseProvider"
-            android:authorities="com.android.settings.module_licenses"
+            android:authorities="${applicationId}.module_licenses"
             android:grantUriPermissions="true"
             android:exported="false"/>
 
         <provider
             android:name=".emergency.EmergencyActionContentProvider"
-            android:authorities="com.android.settings.emergency"
+            android:authorities="${applicationId}.emergency"
             android:permission="android.permission.CALL_PRIVILEGED"
             android:exported="true"/>
 
@@ -3685,7 +3685,7 @@
 
         <provider
             android:name=".search.SettingsSearchIndexablesProvider"
-            android:authorities="com.android.settings"
+            android:authorities="${applicationId}"
             android:multiprocess="false"
             android:grantUriPermissions="true"
             android:permission="android.permission.READ_SEARCH_INDEXABLES"
@@ -3697,7 +3697,7 @@
 
         <provider
             android:name=".dashboard.suggestions.SuggestionStateProvider"
-            android:authorities="com.android.settings.suggestions.status"
+            android:authorities="${applicationId}.suggestions.status"
             android:exported="true">
             <intent-filter>
                 <action android:name="com.android.settings.action.SUGGESTION_STATE_PROVIDER" />
@@ -3940,7 +3940,7 @@
 
         <provider
             android:name=".dashboard.SummaryProvider"
-            android:authorities="com.android.settings.dashboard.SummaryProvider">
+            android:authorities="${applicationId}.dashboard.SummaryProvider">
         </provider>
 
         <activity android:name=".backup.UserBackupSettingsActivity"
@@ -4327,7 +4327,7 @@
         </activity>
 
         <provider android:name=".slices.SettingsSliceProvider"
-                  android:authorities="com.android.settings.slices;android.settings.slices"
+                  android:authorities="${applicationId}.slices;android.settings.slices"
                   android:exported="true"
                   android:grantUriPermissions="true" />
 
@@ -4369,13 +4369,13 @@
 
         <provider
             android:name=".homepage.contextualcards.CardContentProvider"
-            android:authorities="com.android.settings.homepage.CardContentProvider"
+            android:authorities="${applicationId}.homepage.CardContentProvider"
             android:exported="true"
             android:permission="android.permission.WRITE_SETTINGS_HOMEPAGE_DATA" />
 
         <provider
             android:name=".homepage.contextualcards.SettingsContextualCardProvider"
-            android:authorities="com.android.settings.homepage.contextualcards"
+            android:authorities="${applicationId}.homepage.contextualcards"
             android:permission="android.permission.WRITE_SETTINGS_HOMEPAGE_DATA"
             android:exported="true">
             <intent-filter>
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 4865e19..836806c 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,4 +1,9 @@
 {
+  "presubmit": [
+    {
+      "name": "SettingsSpaUnitTests"
+    }
+  ],
   "postsubmit": [
     {
       "name": "SettingsUnitTests",
diff --git a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java
index 39e8ea8..c1b49c2 100755
--- a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java
+++ b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java
@@ -630,6 +630,20 @@
                 .launch();
     }
 
+    /** Starts app info fragment from SPA pages. */
+    public static void startAppInfoFragment(
+            Class<?> destination, ApplicationInfo app, Context context, int sourceMetricsCategory) {
+        // start new fragment to display extended information
+        Bundle args = new Bundle();
+        args.putString(ARG_PACKAGE_NAME, app.packageName);
+        args.putInt(ARG_PACKAGE_UID, app.uid);
+        new SubSettingLauncher(context)
+                .setDestination(destination.getName())
+                .setArguments(args)
+                .setSourceMetricsCategory(sourceMetricsCategory)
+                .launch();
+    }
+
     private void onPackageRemoved() {
         getActivity().finishActivity(SUB_INFO_FRAGMENT);
         getActivity().finishAndRemoveTask();
diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
index 6acfac4..d71f889 100644
--- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
@@ -91,6 +91,7 @@
         AppButtons(packageInfoPresenter)
 
         AppPermissionPreference(app)
+        AppStoragePreference(app)
 
         Category(title = stringResource(R.string.advanced_apps)) {
             DisplayOverOtherAppsAppListProvider.InfoPageEntryItem(app)
diff --git a/src/com/android/settings/spa/app/appinfo/AppStoragePreference.kt b/src/com/android/settings/spa/app/appinfo/AppStoragePreference.kt
new file mode 100644
index 0000000..265f882
--- /dev/null
+++ b/src/com/android/settings/spa/app/appinfo/AppStoragePreference.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.settings.spa.app.appinfo
+
+import android.app.settings.SettingsEnums
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import com.android.settings.R
+import com.android.settings.applications.AppStorageSettings
+import com.android.settings.applications.appinfo.AppInfoDashboardFragment
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spaprivileged.model.app.hasFlag
+import com.android.settingslib.spaprivileged.template.app.getStorageSize
+
+@Composable
+fun AppStoragePreference(app: ApplicationInfo) {
+    if (!app.hasFlag(ApplicationInfo.FLAG_INSTALLED)) return
+    val context = LocalContext.current
+    Preference(
+        model = object : PreferenceModel {
+            override val title = stringResource(R.string.storage_settings_for_app)
+            override val summary = getSummary(context, app)
+            override val onClick = { startStorageSettingsActivity(context, app) }
+        },
+        singleLineSummary = true,
+    )
+}
+
+@Composable
+private fun getSummary(context: Context, app: ApplicationInfo): State<String> {
+    val sizeState = app.getStorageSize()
+    return remember {
+        derivedStateOf {
+            val size = sizeState.value
+            if (size.isBlank()) return@derivedStateOf context.getString(R.string.computing_size)
+            val storageType = context.getString(
+                when (app.hasFlag(ApplicationInfo.FLAG_EXTERNAL_STORAGE)) {
+                    true -> R.string.storage_type_external
+                    false -> R.string.storage_type_internal
+                }
+            )
+            context.getString(R.string.storage_summary_format, size, storageType)
+        }
+    }
+}
+
+private fun startStorageSettingsActivity(context: Context, app: ApplicationInfo) {
+    AppInfoDashboardFragment.startAppInfoFragment(
+        AppStorageSettings::class.java,
+        app,
+        context,
+        SettingsEnums.APPLICATIONS_INSTALLED_APP_DETAILS,
+    )
+}
diff --git a/tests/spa_unit/Android.bp b/tests/spa_unit/Android.bp
new file mode 100644
index 0000000..ed83ab2
--- /dev/null
+++ b/tests/spa_unit/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 {
+    default_applicable_licenses: ["packages_apps_Settings_license"],
+}
+
+android_test {
+    name: "SettingsSpaUnitTests",
+    certificate: "platform",
+    platform_apis: true,
+    test_suites: ["device-tests"],
+
+    srcs: [
+        "src/**/*.kt",
+    ],
+
+    static_libs: [
+        "Settings-core",
+        "androidx.compose.runtime_runtime",
+        "androidx.compose.ui_ui-test-junit4",
+        "androidx.compose.ui_ui-test-manifest",
+        "androidx.test.ext.junit",
+        "androidx.test.runner",
+        "mockito-target-minus-junit4",
+        "truth-prebuilt",
+    ],
+    kotlincflags: [
+        "-Xjvm-default=all",
+        "-opt-in=kotlin.RequiresOptIn",
+    ],
+
+    instrumentation_for: "Settings-core",
+}
diff --git a/tests/spa_unit/AndroidManifest.xml b/tests/spa_unit/AndroidManifest.xml
new file mode 100644
index 0000000..5cf8ffd
--- /dev/null
+++ b/tests/spa_unit/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?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"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="com.android.settings.tests.spa_unit">
+
+    <application>
+        <provider android:name="com.android.settings.slices.SettingsSliceProvider"
+                  android:authorities="${applicationId}.slices"
+                  tools:replace="android:authorities"/>
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:label="Tests for Settings SPA package"
+        android:targetPackage="com.android.settings.tests.spa_unit"/>
+</manifest>
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppStoragePreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppStoragePreferenceTest.kt
new file mode 100644
index 0000000..394442d
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppStoragePreferenceTest.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.settings.spa.app.appinfo
+
+import android.app.usage.StorageStats
+import android.app.usage.StorageStatsManager
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.onRoot
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settingslib.spaprivileged.framework.common.storageStatsManager
+import java.util.UUID
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Spy
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.Mockito.any
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.`when` as whenever
+
+@RunWith(AndroidJUnit4::class)
+class AppStoragePreferenceTest {
+    @JvmField
+    @Rule
+    val mockito: MockitoRule = MockitoJUnit.rule()
+
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Spy
+    private var context: Context = ApplicationProvider.getApplicationContext()
+
+    @Mock
+    private lateinit var storageStatsManager: StorageStatsManager
+
+    @Before
+    fun setUp() {
+        whenever(context.storageStatsManager).thenReturn(storageStatsManager)
+        whenever(
+            storageStatsManager.queryStatsForPackage(eq(STORAGE_UUID), eq(PACKAGE_NAME), any())
+        ).thenReturn(STATS)
+    }
+
+    @Test
+    fun uninstalledApp_notDisplayed() {
+        val uninstalledApp = ApplicationInfo()
+
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalContext provides context) {
+                AppStoragePreference(uninstalledApp)
+            }
+        }
+
+        composeTestRule.onRoot().assertIsNotDisplayed()
+    }
+
+    @Test
+    fun internalApp_displayed() {
+        val internalApp = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+            flags = ApplicationInfo.FLAG_INSTALLED
+            storageUuid = STORAGE_UUID
+        }
+
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalContext provides context) {
+                AppStoragePreference(internalApp)
+            }
+        }
+
+        composeTestRule.onNodeWithText(context.getString(R.string.storage_settings_for_app))
+            .assertIsDisplayed()
+        composeTestRule.onNodeWithText("123 B used in internal storage").assertIsDisplayed()
+    }
+
+    @Test
+    fun externalApp_displayed() {
+        val externalApp = ApplicationInfo().apply {
+            packageName = PACKAGE_NAME
+            flags = ApplicationInfo.FLAG_INSTALLED or ApplicationInfo.FLAG_EXTERNAL_STORAGE
+            storageUuid = STORAGE_UUID
+        }
+
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalContext provides context) {
+                AppStoragePreference(externalApp)
+            }
+        }
+
+        composeTestRule.onNodeWithText(context.getString(R.string.storage_settings_for_app))
+            .assertIsDisplayed()
+        composeTestRule.onNodeWithText("123 B used in external storage").assertIsDisplayed()
+    }
+
+    companion object {
+        private const val PACKAGE_NAME = "package name"
+        private val STORAGE_UUID = UUID.randomUUID()
+
+        private val STATS = StorageStats().apply {
+            codeBytes = 100
+            dataBytes = 20
+            cacheBytes = 3
+        }
+    }
+}