From 025a73e22ecf122f5f521490466f369bb01c5eaa Mon Sep 17 00:00:00 2001
From: Evan Laird <evanlaird@google.com>
Date: Fri, 24 May 2024 18:23:45 -0400
Subject: [PATCH] [Sat] Add satellite supported changed callback

Per the linked bug, there is a new API on `SatelliteManager` that
implements a callback for satellite support. This is in addition to the
existing query API that happens when the repository starts up.

This CL adds the listener after the initial query, and makes sure that
if satellite support shows up later it is caught. The state change
listener runs regardless of the initial supported state.

Test: DeviceBasedSatelliteRepositoryImplTest
Bug: 342278770
Flag: NONE bugfix
Change-Id: I4d930686f9ec98b0468f0efd6a1b0a4355b6fddc
---
 .../DeviceBasedSatelliteRepositoryImpl.kt     | 172 ++++++++++++------
 .../DeviceBasedSatelliteRepositoryImplTest.kt |  90 ++++++++-
 2 files changed, 207 insertions(+), 55 deletions(-)

diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
index 9d9cc2aa11de..7bb65dd57923 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -23,6 +23,7 @@ import android.telephony.satellite.NtnSignalStrengthCallback
 import android.telephony.satellite.SatelliteManager
 import android.telephony.satellite.SatelliteManager.SATELLITE_RESULT_SUCCESS
 import android.telephony.satellite.SatelliteModemStateCallback
+import android.telephony.satellite.SatelliteSupportedStateCallback
 import androidx.annotation.VisibleForTesting
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
@@ -34,6 +35,7 @@ import com.android.systemui.log.core.MessageInitializer
 import com.android.systemui.log.core.MessagePrinter
 import com.android.systemui.statusbar.pipeline.dagger.OemSatelliteInputLog
 import com.android.systemui.statusbar.pipeline.satellite.data.RealDeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported
 import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Supported
@@ -160,60 +162,6 @@ constructor(
     @get:VisibleForTesting
     val satelliteSupport: MutableStateFlow<SatelliteSupport> = MutableStateFlow(Unknown)
 
-    init {
-        satelliteManager = satelliteManagerOpt.getOrNull()
-
-        isSatelliteAllowedForCurrentLocation = MutableStateFlow(false)
-
-        if (satelliteManager != null) {
-            // First, check that satellite is supported on this device
-            scope.launch {
-                val waitTime = ensureMinUptime(systemClock, MIN_UPTIME)
-                if (waitTime > 0) {
-                    logBuffer.i({ long1 = waitTime }) {
-                        "Waiting $long1 ms before checking for satellite support"
-                    }
-                    delay(waitTime)
-                }
-
-                satelliteSupport.value = satelliteManager.checkSatelliteSupported()
-
-                logBuffer.i(
-                    { str1 = satelliteSupport.value.toString() },
-                    { "Checked for system support. support=$str1" },
-                )
-
-                // We only need to check location availability if this mode is supported
-                if (satelliteSupport.value is Supported) {
-                    isSatelliteAllowedForCurrentLocation.subscriptionCount
-                        .map { it > 0 }
-                        .distinctUntilChanged()
-                        .collectLatest { hasSubscribers ->
-                            if (hasSubscribers) {
-                                /*
-                                 * As there is no listener available for checking satellite allowed,
-                                 * we must poll. Defaulting to polling at most once every hour while
-                                 * active. Subsequent OOS events will restart the job, so a flaky
-                                 * connection might cause more frequent checks.
-                                 */
-                                while (true) {
-                                    logBuffer.i {
-                                        "requestIsCommunicationAllowedForCurrentLocation"
-                                    }
-                                    checkIsSatelliteAllowed()
-                                    delay(POLLING_INTERVAL_MS)
-                                }
-                            }
-                        }
-                }
-            }
-        } else {
-            logBuffer.i { "Satellite manager is null" }
-
-            satelliteSupport.value = NotSupported
-        }
-    }
-
     /**
      * Note that we are given an "unbound" [TelephonyManager] (meaning it was not created with a
      * specific `subscriptionId`). Therefore this is the radio power state of the
@@ -267,6 +215,122 @@ constructor(
             }
             .onStart { emit(Unit) }
 
+    init {
+        satelliteManager = satelliteManagerOpt.getOrNull()
+
+        isSatelliteAllowedForCurrentLocation = MutableStateFlow(false)
+
+        if (satelliteManager != null) {
+            // Outer scope launch allows us to delay until MIN_UPTIME
+            scope.launch {
+                // First, check that satellite is supported on this device
+                satelliteSupport.value = checkSatelliteSupportAfterMinUptime(satelliteManager)
+                logBuffer.i(
+                    { str1 = satelliteSupport.value.toString() },
+                    { "Checked for system support. support=$str1" },
+                )
+
+                // Second, launch a job to poll for service availability based on location
+                scope.launch { pollForAvailabilityBasedOnLocation() }
+
+                // Third, register a listener to let us know if there are changes to support
+                scope.launch { listenForChangesToSatelliteSupport(satelliteManager) }
+            }
+        } else {
+            logBuffer.i { "Satellite manager is null" }
+            satelliteSupport.value = NotSupported
+        }
+    }
+
+    private suspend fun checkSatelliteSupportAfterMinUptime(
+        sm: SatelliteManager
+    ): SatelliteSupport {
+        val waitTime = ensureMinUptime(systemClock, MIN_UPTIME)
+        if (waitTime > 0) {
+            logBuffer.i({ long1 = waitTime }) {
+                "Waiting $long1 ms before checking for satellite support"
+            }
+            delay(waitTime)
+        }
+
+        return sm.checkSatelliteSupported()
+    }
+
+    /*
+     * As there is no listener available for checking satellite allowed, we must poll the service.
+     * Defaulting to polling at most once every 20m while active. Subsequent OOS events will restart
+     * the job, so a flaky connection might cause more frequent checks.
+     */
+    private suspend fun pollForAvailabilityBasedOnLocation() {
+        satelliteSupport
+            .whenSupported(
+                supported = ::isSatelliteAllowedHasListener,
+                orElse = flowOf(false),
+                retrySignal = telephonyProcessCrashedEvent,
+            )
+            .collectLatest { hasSubscribers ->
+                if (hasSubscribers) {
+                    while (true) {
+                        logBuffer.i { "requestIsCommunicationAllowedForCurrentLocation" }
+                        checkIsSatelliteAllowed()
+                        delay(POLLING_INTERVAL_MS)
+                    }
+                }
+            }
+    }
+
+    /**
+     * Register a callback with [SatelliteManager] to let us know if there is a change in satellite
+     * support. This job restarts if there is a crash event detected.
+     *
+     * Note that the structure of this method looks similar to [whenSupported], but since we want
+     * this callback registered even when it is [NotSupported], we just mimic the structure here.
+     */
+    private suspend fun listenForChangesToSatelliteSupport(sm: SatelliteManager) {
+        telephonyProcessCrashedEvent.collectLatest {
+            satelliteIsSupportedCallback.collect { supported ->
+                if (supported) {
+                    satelliteSupport.value = Supported(sm)
+                } else {
+                    satelliteSupport.value = NotSupported
+                }
+            }
+        }
+    }
+
+    /**
+     * Callback version of [checkSatelliteSupported]. This flow should be retried on the same
+     * [telephonyProcessCrashedEvent] signal, but does not require a [SupportedSatelliteManager],
+     * since it specifically watches for satellite support.
+     */
+    private val satelliteIsSupportedCallback: Flow<Boolean> =
+        if (satelliteManager == null) {
+            flowOf(false)
+        } else {
+            conflatedCallbackFlow {
+                val callback = SatelliteSupportedStateCallback { supported ->
+                    logBuffer.i {
+                        "onSatelliteSupportedStateChanged: " +
+                            "${if (supported) "supported" else "not supported"}"
+                    }
+                    trySend(supported)
+                }
+                satelliteManager.registerForSupportedStateChanged(
+                    bgDispatcher.asExecutor(),
+                    callback
+                )
+                awaitClose { satelliteManager.unregisterForSupportedStateChanged(callback) }
+            }
+        }
+
+    /**
+     * Signal that we should start polling [checkIsSatelliteAllowed]. We only need to poll if there
+     * are active listeners to [isSatelliteAllowedForCurrentLocation]
+     */
+    @SuppressWarnings("unused")
+    private fun isSatelliteAllowedHasListener(sm: SupportedSatelliteManager): Flow<Boolean> =
+        isSatelliteAllowedForCurrentLocation.subscriptionCount.map { it > 0 }.distinctUntilChanged()
+
     override val connectionState =
         satelliteSupport
             .whenSupported(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
index 66516769c804..b65114cc967e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -34,6 +34,7 @@ import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNAVAI
 import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN
 import android.telephony.satellite.SatelliteManager.SatelliteException
 import android.telephony.satellite.SatelliteModemStateCallback
+import android.telephony.satellite.SatelliteSupportedStateCallback
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
@@ -326,7 +327,6 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() {
     @Test
     fun satelliteNotSupported_listenersAreNotRegistered() =
         testScope.runTest {
-            setupDefaultRepo()
             // GIVEN satellite is not supported
             setUpRepo(
                 uptime = MIN_UPTIME,
@@ -343,6 +343,94 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() {
             verify(satelliteManager, never()).registerForNtnSignalStrengthChanged(any(), any())
         }
 
+    @Test
+    fun satelliteSupported_registersCallbackForStateChanges() =
+        testScope.runTest {
+            // GIVEN a supported satellite manager.
+            setupDefaultRepo()
+            runCurrent()
+
+            // THEN the repo registers for state changes of satellite support
+            verify(satelliteManager, times(1)).registerForSupportedStateChanged(any(), any())
+        }
+
+    @Test
+    fun satelliteNotSupported_registersCallbackForStateChanges() =
+        testScope.runTest {
+            // GIVEN satellite is not supported
+            setUpRepo(
+                uptime = MIN_UPTIME,
+                satMan = satelliteManager,
+                satelliteSupported = false,
+            )
+
+            runCurrent()
+            // THEN the repo registers for state changes of satellite support
+            verify(satelliteManager, times(1)).registerForSupportedStateChanged(any(), any())
+        }
+
+    @Test
+    fun satelliteSupported_supportIsLost_unregistersListeners() =
+        testScope.runTest {
+            // GIVEN a supported satellite manager.
+            setupDefaultRepo()
+            runCurrent()
+
+            val callback =
+                withArgCaptor<SatelliteSupportedStateCallback> {
+                    verify(satelliteManager).registerForSupportedStateChanged(any(), capture())
+                }
+
+            // WHEN data is requested from the repo
+            val connectionState by collectLastValue(underTest.connectionState)
+            val signalStrength by collectLastValue(underTest.signalStrength)
+
+            // THEN the listeners are registered
+            verify(satelliteManager, times(1)).registerForModemStateChanged(any(), any())
+            verify(satelliteManager, times(1)).registerForNtnSignalStrengthChanged(any(), any())
+
+            // WHEN satellite support turns off
+            callback.onSatelliteSupportedStateChanged(false)
+            runCurrent()
+
+            // THEN listeners are unregistered
+            verify(satelliteManager, times(1)).unregisterForModemStateChanged(any())
+            verify(satelliteManager, times(1)).unregisterForNtnSignalStrengthChanged(any())
+        }
+
+    @Test
+    fun satelliteNotSupported_supportShowsUp_registersListeners() =
+        testScope.runTest {
+            // GIVEN satellite is not supported
+            setUpRepo(
+                uptime = MIN_UPTIME,
+                satMan = satelliteManager,
+                satelliteSupported = false,
+            )
+            runCurrent()
+
+            val callback =
+                withArgCaptor<SatelliteSupportedStateCallback> {
+                    verify(satelliteManager).registerForSupportedStateChanged(any(), capture())
+                }
+
+            // WHEN data is requested from the repo
+            val connectionState by collectLastValue(underTest.connectionState)
+            val signalStrength by collectLastValue(underTest.signalStrength)
+
+            // THEN the listeners are not yet registered
+            verify(satelliteManager, times(0)).registerForModemStateChanged(any(), any())
+            verify(satelliteManager, times(0)).registerForNtnSignalStrengthChanged(any(), any())
+
+            // WHEN satellite support turns on
+            callback.onSatelliteSupportedStateChanged(true)
+            runCurrent()
+
+            // THEN listeners are registered
+            verify(satelliteManager, times(1)).registerForModemStateChanged(any(), any())
+            verify(satelliteManager, times(1)).registerForNtnSignalStrengthChanged(any(), any())
+        }
+
     @Test
     fun repoDoesNotCheckForSupportUntilMinUptime() =
         testScope.runTest {
-- 
GitLab