Move Slice code to spa/slice folder & add tests.

Bug: 244122804
Test: unit-test & local build gallery
Change-Id: I61baac3e52e1ac3d7b674bc8366bee0074586115
diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
index a051ffe..71c52d9 100644
--- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
@@ -42,7 +42,7 @@
             android:exported="false">
         </provider>
 
-        <provider android:name="com.android.settingslib.spa.framework.SpaSliceProvider"
+        <provider android:name="com.android.settingslib.spa.slice.SpaSliceProvider"
             android:authorities="com.android.spa.gallery.slice.provider"
             android:exported="true" >
             <intent-filter>
@@ -52,7 +52,7 @@
         </provider>
 
         <receiver
-            android:name="com.android.settingslib.spa.framework.SpaSliceBroadcastReceiver"
+            android:name="com.android.settingslib.spa.slice.SpaSliceBroadcastReceiver"
             android:exported="false">
         </receiver>
 
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index 6d20c91..9daa4f7 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -17,7 +17,6 @@
 package com.android.settingslib.spa.gallery
 
 import android.content.Context
-import com.android.settingslib.spa.framework.SpaSliceBroadcastReceiver
 import com.android.settingslib.spa.framework.common.LocalLogger
 import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository
 import com.android.settingslib.spa.framework.common.SpaEnvironment
@@ -39,6 +38,7 @@
 import com.android.settingslib.spa.gallery.preference.TwoTargetSwitchPreferencePageProvider
 import com.android.settingslib.spa.gallery.ui.CategoryPageProvider
 import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider
+import com.android.settingslib.spa.slice.SpaSliceBroadcastReceiver
 
 /**
  * Enum to define all SPP name here.
@@ -81,9 +81,12 @@
         )
     }
 
+    override val logger = LocalLogger()
+
     override val browseActivityClass = GalleryMainActivity::class.java
     override val sliceBroadcastReceiverClass = SpaSliceBroadcastReceiver::class.java
+
+    // For debugging
     override val searchProviderAuthorities = "com.android.spa.gallery.search.provider"
     override val sliceProviderAuthorities = "com.android.spa.gallery.slice.provider"
-    override val logger = LocalLogger()
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
index 945add4..6d0b810 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/common/SpaEnvironment.kt
@@ -70,11 +70,14 @@
     // In Robolectric test, applicationContext is not available. Use context as fallback.
     val appContext: Context = context.applicationContext ?: context
 
+    open val logger: SpaLogger = object : SpaLogger {}
+
     open val browseActivityClass: Class<out Activity>? = null
     open val sliceBroadcastReceiverClass: Class<out BroadcastReceiver>? = null
+
+    // Specify provider authorities for debugging purpose.
     open val searchProviderAuthorities: String? = null
     open val sliceProviderAuthorities: String? = null
-    open val logger: SpaLogger = object : SpaLogger {}
 
     // TODO: add other environment setup here.
 }
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceBroadcastReceiver.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceBroadcastReceiver.kt
similarity index 95%
rename from packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceBroadcastReceiver.kt
rename to packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceBroadcastReceiver.kt
index 58131e6..39cb431 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceBroadcastReceiver.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceBroadcastReceiver.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spa.framework
+package com.android.settingslib.spa.slice
 
 import android.content.BroadcastReceiver
 import android.content.Context
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceProvider.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceProvider.kt
similarity index 98%
rename from packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceProvider.kt
rename to packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceProvider.kt
index faa04fd..b809c0f 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/SpaSliceProvider.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/slice/SpaSliceProvider.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.settingslib.spa.framework
+package com.android.settingslib.spa.slice
 
 import android.net.Uri
 import android.util.Log
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SpaEnvironmentForTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SpaEnvironmentForTest.kt
index b8731a3..4df37b1 100644
--- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SpaEnvironmentForTest.kt
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/common/SpaEnvironmentForTest.kt
@@ -17,7 +17,9 @@
 package com.android.settingslib.spa.framework.common
 
 import android.app.Activity
+import android.content.BroadcastReceiver
 import android.content.Context
+import android.content.Intent
 import android.os.Bundle
 import androidx.navigation.NavType
 import androidx.navigation.navArgument
@@ -57,6 +59,9 @@
 }
 
 class MockActivity : BrowseActivity()
+class MockSliceBroadcastReceiver : BroadcastReceiver() {
+    override fun onReceive(p0: Context?, p1: Intent?) {}
+}
 
 object SppHome : SettingsPageProvider {
     override val name = "SppHome"
@@ -104,7 +109,13 @@
     override fun buildEntry(arguments: Bundle?): List<SettingsEntry> {
         val owner = this.createSettingsPage()
         return listOf(
-            SettingsEntryBuilder.create(owner, "Layer2Entry1").build(),
+            SettingsEntryBuilder.create(owner, "Layer2Entry1")
+                .setSliceDataFn { _, _ ->
+                    return@setSliceDataFn object : EntrySliceData() {
+                        init { postValue(null) }
+                    }
+                }
+                .build(),
             SettingsEntryBuilder.create(owner, "Layer2Entry2").build(),
         )
     }
@@ -113,7 +124,9 @@
 class SpaEnvironmentForTest(
     context: Context,
     override val browseActivityClass: Class<out Activity>? = MockActivity::class.java,
-    override val logger: SpaLogger = SpaLoggerForTest()
+    override val sliceBroadcastReceiverClass: Class<out BroadcastReceiver>? =
+        MockSliceBroadcastReceiver::class.java,
+    override val logger: SpaLogger = object : SpaLogger {}
 ) : SpaEnvironment(context) {
 
     override val pageProviderRepository = lazy {
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt
new file mode 100644
index 0000000..c501fa5
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SettingsSliceDataRepositoryTest.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.slice
+
+import android.content.Context
+import android.net.Uri
+import androidx.lifecycle.Observer
+import androidx.slice.Slice
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.common.SpaEnvironmentForTest
+import com.android.settingslib.spa.framework.common.SppLayer2
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.common.getUniqueEntryId
+import com.android.settingslib.spa.testutils.InstantTaskExecutorRule
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsSliceDataRepositoryTest {
+    @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule()
+
+    private val context: Context = ApplicationProvider.getApplicationContext()
+    private val spaEnvironment = SpaEnvironmentForTest(context)
+    private val sliceDataRepository by spaEnvironment.sliceDataRepository
+
+    @Test
+    fun getOrBuildSliceDataTest() {
+        // Slice empty
+        assertThat(sliceDataRepository.getOrBuildSliceData(Uri.EMPTY)).isNull()
+
+        // Slice supported
+        val page = SppLayer2.createSettingsPage()
+        val entryId = getUniqueEntryId("Layer2Entry1", page)
+        val sliceUri = Uri.Builder().appendSliceParams(page.buildRoute(), entryId).build()
+        assertThat(sliceUri.getDestination()).isEqualTo("SppLayer2")
+        assertThat(sliceUri.getSliceId()).isEqualTo("${entryId}_Bundle[{}]")
+        val sliceData = sliceDataRepository.getOrBuildSliceData(sliceUri)
+        assertThat(sliceData).isNotNull()
+        assertThat(sliceDataRepository.getOrBuildSliceData(sliceUri)).isSameInstanceAs(sliceData)
+
+        // Slice unsupported
+        val entryId2 = getUniqueEntryId("Layer2Entry2", page)
+        val sliceUri2 = Uri.Builder().appendSliceParams(page.buildRoute(), entryId2).build()
+        assertThat(sliceUri2.getDestination()).isEqualTo("SppLayer2")
+        assertThat(sliceUri2.getSliceId()).isEqualTo("${entryId2}_Bundle[{}]")
+        assertThat(sliceDataRepository.getOrBuildSliceData(sliceUri2)).isNull()
+    }
+
+    @Test
+    fun getActiveSliceDataTest() {
+        val page = SppLayer2.createSettingsPage()
+        val entryId = getUniqueEntryId("Layer2Entry1", page)
+        val sliceUri = Uri.Builder().appendSliceParams(page.buildRoute(), entryId).build()
+
+        // build slice data first
+        val sliceData = sliceDataRepository.getOrBuildSliceData(sliceUri)
+
+        // slice data is inactive
+        assertThat(sliceData!!.isActive()).isFalse()
+        assertThat(sliceDataRepository.getActiveSliceData(sliceUri)).isNull()
+
+        // slice data is active
+        val observer = Observer<Slice?> { }
+        sliceData.observeForever(observer)
+        assertThat(sliceData.isActive()).isTrue()
+        assertThat(sliceDataRepository.getActiveSliceData(sliceUri)).isSameInstanceAs(sliceData)
+
+        // slice data is inactive again
+        sliceData.removeObserver(observer)
+        assertThat(sliceData.isActive()).isFalse()
+        assertThat(sliceDataRepository.getActiveSliceData(sliceUri)).isNull()
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt
new file mode 100644
index 0000000..50705d5
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/slice/SliceUtilTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.slice
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.core.os.bundleOf
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.common.SpaEnvironmentForTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SliceUtilTest {
+    private val context: Context = ApplicationProvider.getApplicationContext()
+    private val spaEnvironment = SpaEnvironmentForTest(context)
+
+    @Test
+    fun sliceUriTest() {
+        assertThat(Uri.EMPTY.getEntryId()).isNull()
+        assertThat(Uri.EMPTY.getDestination()).isNull()
+        assertThat(Uri.EMPTY.getRuntimeArguments().size()).isEqualTo(0)
+        assertThat(Uri.EMPTY.getSliceId()).isNull()
+
+        // valid slice uri
+        val dest = "myRoute"
+        val entryId = "myEntry"
+        val sliceUriWithoutParams = Uri.Builder().appendSliceParams(dest, entryId).build()
+        assertThat(sliceUriWithoutParams.getEntryId()).isEqualTo(entryId)
+        assertThat(sliceUriWithoutParams.getDestination()).isEqualTo(dest)
+        assertThat(sliceUriWithoutParams.getRuntimeArguments().size()).isEqualTo(0)
+        assertThat(sliceUriWithoutParams.getSliceId()).isEqualTo("${entryId}_Bundle[{}]")
+
+        val sliceUriWithParams =
+            Uri.Builder().appendSliceParams(dest, entryId, bundleOf("p1" to "v1")).build()
+        assertThat(sliceUriWithParams.getEntryId()).isEqualTo(entryId)
+        assertThat(sliceUriWithParams.getDestination()).isEqualTo(dest)
+        assertThat(sliceUriWithParams.getRuntimeArguments().size()).isEqualTo(1)
+        assertThat(sliceUriWithParams.getSliceId()).isEqualTo("${entryId}_Bundle[{p1=v1}]")
+    }
+
+    @Test
+    fun createBroadcastPendingIntentTest() {
+        SpaEnvironmentFactory.reset(spaEnvironment)
+
+        // Empty Slice Uri
+        assertThat(Uri.EMPTY.createBroadcastPendingIntent()).isNull()
+
+        // Valid Slice Uri
+        val dest = "myRoute"
+        val entryId = "myEntry"
+        val sliceUriWithoutParams = Uri.Builder().appendSliceParams(dest, entryId).build()
+        val pendingIntent = sliceUriWithoutParams.createBroadcastPendingIntent()
+        assertThat(pendingIntent).isNotNull()
+        assertThat(pendingIntent!!.isBroadcast).isTrue()
+        assertThat(pendingIntent.isImmutable).isFalse()
+    }
+
+    @Test
+    fun createBrowsePendingIntentTest() {
+        SpaEnvironmentFactory.reset(spaEnvironment)
+
+        // Empty Slice Uri
+        assertThat(Uri.EMPTY.createBrowsePendingIntent()).isNull()
+
+        // Empty Intent
+        assertThat(Intent().createBrowsePendingIntent()).isNull()
+
+        // Valid Slice Uri
+        val dest = "myRoute"
+        val entryId = "myEntry"
+        val sliceUri = Uri.Builder().appendSliceParams(dest, entryId).build()
+        val pendingIntent = sliceUri.createBrowsePendingIntent()
+        assertThat(pendingIntent).isNotNull()
+        assertThat(pendingIntent!!.isActivity).isTrue()
+        assertThat(pendingIntent.isImmutable).isTrue()
+
+        // Valid Intent
+        val intent = Intent().apply {
+            putExtra("spaActivityDestination", dest)
+            putExtra("highlightEntry", entryId)
+        }
+        val pendingIntent2 = intent.createBrowsePendingIntent()
+        assertThat(pendingIntent2).isNotNull()
+        assertThat(pendingIntent2!!.isActivity).isTrue()
+        assertThat(pendingIntent2.isImmutable).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/testutils/Android.bp b/packages/SettingsLib/Spa/testutils/Android.bp
index 0f618fa..48df569 100644
--- a/packages/SettingsLib/Spa/testutils/Android.bp
+++ b/packages/SettingsLib/Spa/testutils/Android.bp
@@ -24,6 +24,7 @@
     srcs: ["src/**/*.kt"],
 
     static_libs: [
+        "androidx.arch.core_core-runtime",
         "androidx.compose.ui_ui-test-junit4",
         "androidx.compose.ui_ui-test-manifest",
         "mockito",
diff --git a/packages/SettingsLib/Spa/testutils/build.gradle b/packages/SettingsLib/Spa/testutils/build.gradle
index 58b4d42..be8df43 100644
--- a/packages/SettingsLib/Spa/testutils/build.gradle
+++ b/packages/SettingsLib/Spa/testutils/build.gradle
@@ -47,6 +47,7 @@
 }
 
 dependencies {
+    api "androidx.arch.core:core-runtime:2.1.0"
     api "androidx.compose.ui:ui-test-junit4:$jetpack_compose_version"
     api "com.google.truth:truth:1.1.3"
     api "org.mockito:mockito-core:2.21.0"
diff --git a/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/InstantTaskExecutorRule.kt b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/InstantTaskExecutorRule.kt
new file mode 100644
index 0000000..43c18d4
--- /dev/null
+++ b/packages/SettingsLib/Spa/testutils/src/com/android/settingslib/spa/testutils/InstantTaskExecutorRule.kt
@@ -0,0 +1,55 @@
+/*
+ *  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.testutils
+
+import androidx.arch.core.executor.ArchTaskExecutor
+import androidx.arch.core.executor.TaskExecutor
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+/**
+ * Test rule that makes ArchTaskExecutor main thread assertions pass. There is one such assert
+ * in LifecycleRegistry.
+
+ * This is a copy of androidx/arch/core/executor/testing/InstantTaskExecutorRule which should be
+ * replaced once the dependency issue is solved.
+ */
+class InstantTaskExecutorRule : TestWatcher() {
+    override fun starting(description: Description) {
+        super.starting(description)
+        ArchTaskExecutor.getInstance().setDelegate(
+            object : TaskExecutor() {
+                override fun executeOnDiskIO(runnable: Runnable) {
+                    runnable.run()
+                }
+
+                override fun postToMainThread(runnable: Runnable) {
+                    runnable.run()
+                }
+
+                override fun isMainThread(): Boolean {
+                    return true
+                }
+            }
+        )
+    }
+
+    override fun finished(description: Description) {
+        super.finished(description)
+        ArchTaskExecutor.getInstance().setDelegate(null)
+    }
+}