Create LifecycleEffect to listen for lifecycle

Bug: 236346018
Test: Unit test
Change-Id: I264f7215914d92dc32120254b355ba75be225f42
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
index 73c1099..bf4ad75 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle
+++ b/packages/SettingsLib/Spa/spa/build.gradle
@@ -88,6 +88,7 @@
     implementation "com.airbnb.android:lottie-compose:5.2.0"
 
     androidTestImplementation project(":testutils")
+    androidTestImplementation 'androidx.lifecycle:lifecycle-runtime-testing'
     androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:2.28.1"
 }
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/LifecycleEffect.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/LifecycleEffect.kt
new file mode 100644
index 0000000..e91fa65
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/LifecycleEffect.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.framework.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleEventObserver
+
+@Composable
+fun LifecycleEffect(
+    onStart: () -> Unit = {},
+    onStop: () -> Unit = {},
+) {
+    val lifecycleOwner = LocalLifecycleOwner.current
+    DisposableEffect(lifecycleOwner) {
+        val observer = LifecycleEventObserver { _, event ->
+            if (event == Lifecycle.Event.ON_START) {
+                onStart()
+            } else if (event == Lifecycle.Event.ON_STOP) {
+                onStop()
+            }
+        }
+
+        lifecycleOwner.lifecycle.addObserver(observer)
+
+        onDispose {
+            lifecycleOwner.lifecycle.removeObserver(observer)
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt
index 271443e..73eae07 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt
@@ -18,55 +18,41 @@
 
 import android.os.Bundle
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.core.os.bundleOf
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleEventObserver
 import com.android.settingslib.spa.framework.common.LOG_DATA_DISPLAY_NAME
 import com.android.settingslib.spa.framework.common.LOG_DATA_SESSION_NAME
 import com.android.settingslib.spa.framework.common.LogCategory
 import com.android.settingslib.spa.framework.common.LogEvent
+import com.android.settingslib.spa.framework.common.SettingsPage
 import com.android.settingslib.spa.framework.common.SettingsPageProvider
 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
 import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.LifecycleEffect
 import com.android.settingslib.spa.framework.compose.LocalNavController
+import com.android.settingslib.spa.framework.compose.NavControllerWrapper
 
 @Composable
 internal fun SettingsPageProvider.PageEvent(arguments: Bundle? = null) {
     val page = remember(arguments) { createSettingsPage(arguments) }
-    val lifecycleOwner = LocalLifecycleOwner.current
     val navController = LocalNavController.current
-    DisposableEffect(lifecycleOwner) {
-        val observer = LifecycleEventObserver { _, event ->
-            val logPageEvent: (event: LogEvent) -> Unit = {
-                SpaEnvironmentFactory.instance.logger.event(
-                    id = page.id,
-                    event = it,
-                    category = LogCategory.FRAMEWORK,
-                    extraData = bundleOf(
-                        LOG_DATA_DISPLAY_NAME to page.displayName,
-                        LOG_DATA_SESSION_NAME to navController.sessionSourceName,
-                    ).apply {
-                        val normArguments = parameter.normalize(arguments)
-                        if (normArguments != null) putAll(normArguments)
-                    }
-                )
-            }
-            if (event == Lifecycle.Event.ON_START) {
-                logPageEvent(LogEvent.PAGE_ENTER)
-            } else if (event == Lifecycle.Event.ON_STOP) {
-                logPageEvent(LogEvent.PAGE_LEAVE)
-            }
-        }
-
-        // Add the observer to the lifecycle
-        lifecycleOwner.lifecycle.addObserver(observer)
-
-        // When the effect leaves the Composition, remove the observer
-        onDispose {
-            lifecycleOwner.lifecycle.removeObserver(observer)
-        }
-    }
+    LifecycleEffect(
+        onStart = { page.logPageEvent(LogEvent.PAGE_ENTER, navController) },
+        onStop = { page.logPageEvent(LogEvent.PAGE_LEAVE, navController) },
+    )
 }
+
+private fun SettingsPage.logPageEvent(event: LogEvent, navController: NavControllerWrapper) {
+    SpaEnvironmentFactory.instance.logger.event(
+        id = id,
+        event = event,
+        category = LogCategory.FRAMEWORK,
+        extraData = bundleOf(
+            LOG_DATA_DISPLAY_NAME to displayName,
+            LOG_DATA_SESSION_NAME to navController.sessionSourceName,
+        ).apply {
+            val normArguments = parameter.normalize(arguments)
+            if (normArguments != null) putAll(normArguments)
+        }
+    )
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/tests/Android.bp b/packages/SettingsLib/Spa/tests/Android.bp
index f9e64ae..b4c67cc 100644
--- a/packages/SettingsLib/Spa/tests/Android.bp
+++ b/packages/SettingsLib/Spa/tests/Android.bp
@@ -31,6 +31,7 @@
         "SpaLib",
         "SpaLibTestUtils",
         "androidx.compose.runtime_runtime",
+        "androidx.lifecycle_lifecycle-runtime-testing",
         "androidx.test.ext.junit",
         "androidx.test.runner",
         "mockito-target-minus-junit4",
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/LifecycleEffectTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/LifecycleEffectTest.kt
new file mode 100644
index 0000000..fe7baff
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/LifecycleEffectTest.kt
@@ -0,0 +1,62 @@
+/*
+ * 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 androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.testing.TestLifecycleOwner
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class LifecycleEffectTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun onStart_isCalled() {
+        var onStartIsCalled = false
+        composeTestRule.setContent {
+            LifecycleEffect(onStart = { onStartIsCalled = true })
+        }
+
+        assertThat(onStartIsCalled).isTrue()
+    }
+
+    @Test
+    fun onStop_isCalled() {
+        var onStopIsCalled = false
+        val testLifecycleOwner = TestLifecycleOwner()
+
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalLifecycleOwner provides testLifecycleOwner) {
+                LifecycleEffect(onStop = { onStopIsCalled = true })
+            }
+            LaunchedEffect(Unit) {
+                testLifecycleOwner.currentState = Lifecycle.State.CREATED
+            }
+        }
+
+        assertThat(onStopIsCalled).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
index b2ea4a0..a2fb101 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt
@@ -22,11 +22,9 @@
 import android.content.IntentFilter
 import android.os.UserHandle
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleEventObserver
+import com.android.settingslib.spa.framework.compose.LifecycleEffect
 
 /**
  * A [BroadcastReceiver] which registered when on start and unregistered when on stop.
@@ -39,28 +37,22 @@
     onReceive: (Intent) -> Unit,
 ) {
     val context = LocalContext.current
-    val lifecycleOwner = LocalLifecycleOwner.current
-    DisposableEffect(lifecycleOwner) {
-        val broadcastReceiver = object : BroadcastReceiver() {
+    val broadcastReceiver = remember {
+        object : BroadcastReceiver() {
             override fun onReceive(context: Context, intent: Intent) {
                 onReceive(intent)
             }
         }
-        val observer = LifecycleEventObserver { _, event ->
-            if (event == Lifecycle.Event.ON_START) {
-                context.registerReceiverAsUser(
-                    broadcastReceiver, userHandle, intentFilter, null, null
-                )
-                onStart()
-            } else if (event == Lifecycle.Event.ON_STOP) {
-                context.unregisterReceiver(broadcastReceiver)
-            }
-        }
-
-        lifecycleOwner.lifecycle.addObserver(observer)
-
-        onDispose {
-            lifecycleOwner.lifecycle.removeObserver(observer)
-        }
     }
+    LifecycleEffect(
+        onStart = {
+            context.registerReceiverAsUser(
+                broadcastReceiver, userHandle, intentFilter, null, null
+            )
+            onStart()
+        },
+        onStop = {
+            context.unregisterReceiver(broadcastReceiver)
+        },
+    )
 }