diff --git a/src/com/android/settings/network/SatelliteManagerUtil.kt b/src/com/android/settings/network/SatelliteManagerUtil.kt deleted file mode 100644 index 5dc1a84fa49..00000000000 --- a/src/com/android/settings/network/SatelliteManagerUtil.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2024 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.network - -import android.content.Context -import android.os.OutcomeReceiver -import android.telephony.satellite.SatelliteManager -import android.util.Log -import androidx.concurrent.futures.CallbackToFutureAdapter -import com.google.common.util.concurrent.Futures.immediateFuture -import com.google.common.util.concurrent.ListenableFuture -import java.util.concurrent.Executor - -/** - * Utility class for interacting with the SatelliteManager API. - */ -object SatelliteManagerUtil { - - private const val TAG: String = "SatelliteManagerUtil" - - /** - * Checks if the satellite modem is enabled. - * - * @param context The application context - * @param executor The executor to run the asynchronous operation on - * @return A ListenableFuture that will resolve to `true` if the satellite modem enabled, - * `false` otherwise. - */ - @JvmStatic - fun requestIsEnabled(context: Context, executor: Executor): ListenableFuture { - val satelliteManager: SatelliteManager? = - context.getSystemService(SatelliteManager::class.java) - if (satelliteManager == null) { - Log.w(TAG, "SatelliteManager is null") - return immediateFuture(false) - } - - return CallbackToFutureAdapter.getFuture { completer -> - satelliteManager.requestIsEnabled(executor, - object : OutcomeReceiver { - override fun onResult(result: Boolean) { - Log.i(TAG, "Satellite modem enabled status: $result") - completer.set(result) - } - - override fun onError(error: SatelliteManager.SatelliteException) { - super.onError(error) - Log.w(TAG, "Can't get satellite modem enabled status", error) - completer.set(false) - } - }) - "requestIsEnabled" - } - } -} diff --git a/src/com/android/settings/network/SatelliteRepository.kt b/src/com/android/settings/network/SatelliteRepository.kt new file mode 100644 index 00000000000..3dab7e6b44d --- /dev/null +++ b/src/com/android/settings/network/SatelliteRepository.kt @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2024 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.network + +import android.content.Context +import android.os.OutcomeReceiver +import android.telephony.satellite.SatelliteManager +import android.telephony.satellite.SatelliteModemStateCallback +import android.util.Log +import androidx.annotation.VisibleForTesting +import androidx.concurrent.futures.CallbackToFutureAdapter +import com.google.common.util.concurrent.Futures.immediateFuture +import com.google.common.util.concurrent.ListenableFuture +import java.util.concurrent.Executor +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flowOf + +/** + * A repository class for interacting with the SatelliteManager API. + */ +class SatelliteRepository( + private val context: Context, +) { + + /** + * Checks if the satellite modem is enabled. + * + * @param executor The executor to run the asynchronous operation on + * @return A ListenableFuture that will resolve to `true` if the satellite modem enabled, + * `false` otherwise. + */ + fun requestIsEnabled(executor: Executor): ListenableFuture { + val satelliteManager: SatelliteManager? = + context.getSystemService(SatelliteManager::class.java) + if (satelliteManager == null) { + Log.w(TAG, "SatelliteManager is null") + return immediateFuture(false) + } + + return CallbackToFutureAdapter.getFuture { completer -> + satelliteManager.requestIsEnabled(executor, + object : OutcomeReceiver { + override fun onResult(result: Boolean) { + Log.i(TAG, "Satellite modem enabled status: $result") + completer.set(result) + } + + override fun onError(error: SatelliteManager.SatelliteException) { + super.onError(error) + Log.w(TAG, "Can't get satellite modem enabled status", error) + completer.set(false) + } + }) + "requestIsEnabled" + } + } + + /** + * Provides a Flow that emits the enabled state of the satellite modem. Updates are triggered + * when the modem state changes. + * + * @param defaultDispatcher The CoroutineDispatcher to use (Defaults to `Dispatchers.Default`). + * @return A Flow emitting `true` when the modem is enabled and `false` otherwise. + */ + fun getIsModemEnabledFlow( + defaultDispatcher: CoroutineDispatcher = Dispatchers.Default, + ): Flow { + val satelliteManager: SatelliteManager? = + context.getSystemService(SatelliteManager::class.java) + if (satelliteManager == null) { + Log.w(TAG, "SatelliteManager is null") + return flowOf(false) + } + + return callbackFlow { + val callback = SatelliteModemStateCallback { state -> + val isEnabled = convertSatelliteModemStateToEnabledState(state) + Log.i(TAG, "Satellite modem state changed: state=$state, isEnabled=$isEnabled") + trySend(isEnabled) + } + + val result = satelliteManager.registerForModemStateChanged( + defaultDispatcher.asExecutor(), + callback + ) + Log.i(TAG, "Call registerForModemStateChanged: result=$result") + + awaitClose { satelliteManager.unregisterForModemStateChanged(callback) } + } + } + + /** + * Converts a [SatelliteManager.SatelliteModemState] to a boolean representing whether the modem + * is enabled. + * + * @param state The SatelliteModemState provided by the SatelliteManager. + * @return `true` if the modem is enabled, `false` otherwise. + */ + @VisibleForTesting + fun convertSatelliteModemStateToEnabledState( + @SatelliteManager.SatelliteModemState state: Int, + ): Boolean { + // Mapping table based on logic from b/315928920#comment24 + return when (state) { + SatelliteManager.SATELLITE_MODEM_STATE_IDLE, + SatelliteManager.SATELLITE_MODEM_STATE_LISTENING, + SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING, + SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_RETRYING, + SatelliteManager.SATELLITE_MODEM_STATE_NOT_CONNECTED, + SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED -> true + else -> false + } + } + + companion object { + private const val TAG: String = "SatelliteRepository" + } +} diff --git a/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt b/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt index dcac74fce6b..41cef508493 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt +++ b/src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt @@ -26,16 +26,19 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.settings.R +import com.android.settings.network.SatelliteRepository import com.android.settings.network.SubscriptionUtil import com.android.settings.spa.preference.ComposePreferenceController import com.android.settingslib.spa.widget.preference.MainSwitchPreference import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map class MobileNetworkSwitchController @JvmOverloads constructor( context: Context, preferenceKey: String, private val subscriptionRepository: SubscriptionRepository = SubscriptionRepository(context), + private val satelliteRepository: SatelliteRepository = SatelliteRepository(context) ) : ComposePreferenceController(context, preferenceKey) { private var subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID @@ -54,7 +57,12 @@ class MobileNetworkSwitchController @JvmOverloads constructor( subscriptionRepository.isSubscriptionEnabledFlow(subId) }.collectAsStateWithLifecycle(initialValue = null) val changeable by remember { - context.callStateFlow(subId).map { it == TelephonyManager.CALL_STATE_IDLE } + combine( + context.callStateFlow(subId).map { it == TelephonyManager.CALL_STATE_IDLE }, + satelliteRepository.getIsModemEnabledFlow() + ) { isCallStateIdle, isSatelliteModemEnabled -> + isCallStateIdle && !isSatelliteModemEnabled + } }.collectAsStateWithLifecycle(initialValue = true) MainSwitchPreference(model = object : SwitchPreferenceModel { override val title = stringResource(R.string.mobile_network_use_sim_on) diff --git a/src/com/android/settings/sim/receivers/SimSlotChangeReceiver.java b/src/com/android/settings/sim/receivers/SimSlotChangeReceiver.java index 9bba2177144..4920bb80e05 100644 --- a/src/com/android/settings/sim/receivers/SimSlotChangeReceiver.java +++ b/src/com/android/settings/sim/receivers/SimSlotChangeReceiver.java @@ -29,7 +29,7 @@ import android.util.Log; import androidx.annotation.Nullable; import com.android.settings.R; -import com.android.settings.network.SatelliteManagerUtil; +import com.android.settings.network.SatelliteRepository; import com.google.common.util.concurrent.ListenableFuture; @@ -58,8 +58,8 @@ public class SimSlotChangeReceiver extends BroadcastReceiver { if (shouldHandleSlotChange(context)) { Log.d(TAG, "Checking satellite enabled status"); Executor executor = Executors.newSingleThreadExecutor(); - ListenableFuture satelliteEnabledFuture = SatelliteManagerUtil - .requestIsEnabled(context, executor); + ListenableFuture satelliteEnabledFuture = new SatelliteRepository(context) + .requestIsEnabled(executor); satelliteEnabledFuture.addListener(() -> { boolean isSatelliteEnabled = false; try { diff --git a/tests/robotests/src/com/android/settings/network/SatelliteManagerUtilTest.kt b/tests/robotests/src/com/android/settings/network/SatelliteRepositoryTest.kt similarity index 65% rename from tests/robotests/src/com/android/settings/network/SatelliteManagerUtilTest.kt rename to tests/robotests/src/com/android/settings/network/SatelliteRepositoryTest.kt index 50d78973c16..c7d047af63e 100644 --- a/tests/robotests/src/com/android/settings/network/SatelliteManagerUtilTest.kt +++ b/tests/robotests/src/com/android/settings/network/SatelliteRepositoryTest.kt @@ -20,10 +20,12 @@ import android.content.Context import android.os.OutcomeReceiver import android.telephony.satellite.SatelliteManager import android.telephony.satellite.SatelliteManager.SatelliteException +import android.telephony.satellite.SatelliteModemStateCallback import androidx.test.core.app.ApplicationProvider -import com.android.settings.network.SatelliteManagerUtil.requestIsEnabled +import com.google.common.truth.Truth.assertThat import com.google.common.util.concurrent.ListenableFuture import java.util.concurrent.Executor +import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -42,7 +44,7 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) -class SatelliteManagerUtilTest { +class SatelliteRepositoryTest { @JvmField @Rule @@ -57,10 +59,15 @@ class SatelliteManagerUtilTest { @Mock private lateinit var mockExecutor: Executor + private lateinit var repository: SatelliteRepository + + @Before fun setUp() { `when`(this.spyContext.getSystemService(SatelliteManager::class.java)) .thenReturn(mockSatelliteManager) + + repository = SatelliteRepository(spyContext) } @Test @@ -78,7 +85,7 @@ class SatelliteManagerUtilTest { } val result: ListenableFuture = - requestIsEnabled(spyContext, mockExecutor) + repository.requestIsEnabled(mockExecutor) assertTrue(result.get()) } @@ -98,7 +105,7 @@ class SatelliteManagerUtilTest { } val result: ListenableFuture = - requestIsEnabled(spyContext, mockExecutor) + repository.requestIsEnabled(mockExecutor) assertFalse(result.get()) } @@ -117,7 +124,7 @@ class SatelliteManagerUtilTest { null } - val result = requestIsEnabled(spyContext, mockExecutor) + val result = repository.requestIsEnabled(mockExecutor) assertFalse(result.get()) } @@ -126,8 +133,52 @@ class SatelliteManagerUtilTest { fun requestIsEnabled_nullSatelliteManager() = runBlocking { `when`(spyContext.getSystemService(SatelliteManager::class.java)).thenReturn(null) - val result: ListenableFuture = requestIsEnabled(spyContext, mockExecutor) + val result: ListenableFuture = repository.requestIsEnabled(mockExecutor) assertFalse(result.get()) } + + @Test + fun getIsModemEnabledFlow_isSatelliteEnabledState() = runBlocking { + `when`( + mockSatelliteManager.registerForModemStateChanged( + any(), + any() + ) + ).thenAnswer { invocation -> + val callback = invocation.getArgument(1) + callback.onSatelliteModemStateChanged(SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED) + SatelliteManager.SATELLITE_RESULT_SUCCESS + } + + val flow = repository.getIsModemEnabledFlow() + + assertThat(flow.first()).isTrue() + } + + @Test + fun getIsModemEnabledFlow_isSatelliteDisabledState() = runBlocking { + `when`( + mockSatelliteManager.registerForModemStateChanged( + any(), + any() + ) + ).thenAnswer { invocation -> + val callback = invocation.getArgument(1) + callback.onSatelliteModemStateChanged(SatelliteManager.SATELLITE_MODEM_STATE_OFF) + SatelliteManager.SATELLITE_RESULT_SUCCESS + } + + val flow = repository.getIsModemEnabledFlow() + + assertThat(flow.first()).isFalse() + } + + @Test + fun getIsModemEnabledFlow_nullSatelliteManager() = runBlocking { + `when`(spyContext.getSystemService(SatelliteManager::class.java)).thenReturn(null) + + val flow = repository.getIsModemEnabledFlow() + assertThat(flow.first()).isFalse() + } } \ No newline at end of file