Loading src/com/android/settings/network/SatelliteManagerUtil.kt→src/com/android/settings/network/SatelliteRepository.kt +137 −0 Original line number Diff line number Diff line Loading @@ -19,29 +19,36 @@ 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 /** * Utility class for interacting with the SatelliteManager API. * A repository class for interacting with the SatelliteManager API. */ object SatelliteManagerUtil { private const val TAG: String = "SatelliteManagerUtil" class SatelliteRepository( private val context: Context, ) { /** * 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<Boolean> { fun requestIsEnabled(executor: Executor): ListenableFuture<Boolean> { val satelliteManager: SatelliteManager? = context.getSystemService(SatelliteManager::class.java) if (satelliteManager == null) { Loading @@ -66,4 +73,65 @@ object SatelliteManagerUtil { "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<Boolean> { 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" } } src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt +9 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading src/com/android/settings/sim/receivers/SimSlotChangeReceiver.java +3 −3 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -58,8 +58,8 @@ public class SimSlotChangeReceiver extends BroadcastReceiver { if (shouldHandleSlotChange(context)) { Log.d(TAG, "Checking satellite enabled status"); Executor executor = Executors.newSingleThreadExecutor(); ListenableFuture<Boolean> satelliteEnabledFuture = SatelliteManagerUtil .requestIsEnabled(context, executor); ListenableFuture<Boolean> satelliteEnabledFuture = new SatelliteRepository(context) .requestIsEnabled(executor); satelliteEnabledFuture.addListener(() -> { boolean isSatelliteEnabled = false; try { Loading tests/robotests/src/com/android/settings/network/SatelliteManagerUtilTest.kt→tests/robotests/src/com/android/settings/network/SatelliteRepositoryTest.kt +57 −6 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -42,7 +44,7 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class SatelliteManagerUtilTest { class SatelliteRepositoryTest { @JvmField @Rule Loading @@ -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 Loading @@ -78,7 +85,7 @@ class SatelliteManagerUtilTest { } val result: ListenableFuture<Boolean> = requestIsEnabled(spyContext, mockExecutor) repository.requestIsEnabled(mockExecutor) assertTrue(result.get()) } Loading @@ -98,7 +105,7 @@ class SatelliteManagerUtilTest { } val result: ListenableFuture<Boolean> = requestIsEnabled(spyContext, mockExecutor) repository.requestIsEnabled(mockExecutor) assertFalse(result.get()) } Loading @@ -117,7 +124,7 @@ class SatelliteManagerUtilTest { null } val result = requestIsEnabled(spyContext, mockExecutor) val result = repository.requestIsEnabled(mockExecutor) assertFalse(result.get()) } Loading @@ -126,8 +133,52 @@ class SatelliteManagerUtilTest { fun requestIsEnabled_nullSatelliteManager() = runBlocking { `when`(spyContext.getSystemService(SatelliteManager::class.java)).thenReturn(null) val result: ListenableFuture<Boolean> = requestIsEnabled(spyContext, mockExecutor) val result: ListenableFuture<Boolean> = repository.requestIsEnabled(mockExecutor) assertFalse(result.get()) } @Test fun getIsModemEnabledFlow_isSatelliteEnabledState() = runBlocking { `when`( mockSatelliteManager.registerForModemStateChanged( any(), any() ) ).thenAnswer { invocation -> val callback = invocation.getArgument<SatelliteModemStateCallback>(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<SatelliteModemStateCallback>(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 Loading
src/com/android/settings/network/SatelliteManagerUtil.kt→src/com/android/settings/network/SatelliteRepository.kt +137 −0 Original line number Diff line number Diff line Loading @@ -19,29 +19,36 @@ 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 /** * Utility class for interacting with the SatelliteManager API. * A repository class for interacting with the SatelliteManager API. */ object SatelliteManagerUtil { private const val TAG: String = "SatelliteManagerUtil" class SatelliteRepository( private val context: Context, ) { /** * 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<Boolean> { fun requestIsEnabled(executor: Executor): ListenableFuture<Boolean> { val satelliteManager: SatelliteManager? = context.getSystemService(SatelliteManager::class.java) if (satelliteManager == null) { Loading @@ -66,4 +73,65 @@ object SatelliteManagerUtil { "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<Boolean> { 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" } }
src/com/android/settings/network/telephony/MobileNetworkSwitchController.kt +9 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading
src/com/android/settings/sim/receivers/SimSlotChangeReceiver.java +3 −3 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -58,8 +58,8 @@ public class SimSlotChangeReceiver extends BroadcastReceiver { if (shouldHandleSlotChange(context)) { Log.d(TAG, "Checking satellite enabled status"); Executor executor = Executors.newSingleThreadExecutor(); ListenableFuture<Boolean> satelliteEnabledFuture = SatelliteManagerUtil .requestIsEnabled(context, executor); ListenableFuture<Boolean> satelliteEnabledFuture = new SatelliteRepository(context) .requestIsEnabled(executor); satelliteEnabledFuture.addListener(() -> { boolean isSatelliteEnabled = false; try { Loading
tests/robotests/src/com/android/settings/network/SatelliteManagerUtilTest.kt→tests/robotests/src/com/android/settings/network/SatelliteRepositoryTest.kt +57 −6 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -42,7 +44,7 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class SatelliteManagerUtilTest { class SatelliteRepositoryTest { @JvmField @Rule Loading @@ -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 Loading @@ -78,7 +85,7 @@ class SatelliteManagerUtilTest { } val result: ListenableFuture<Boolean> = requestIsEnabled(spyContext, mockExecutor) repository.requestIsEnabled(mockExecutor) assertTrue(result.get()) } Loading @@ -98,7 +105,7 @@ class SatelliteManagerUtilTest { } val result: ListenableFuture<Boolean> = requestIsEnabled(spyContext, mockExecutor) repository.requestIsEnabled(mockExecutor) assertFalse(result.get()) } Loading @@ -117,7 +124,7 @@ class SatelliteManagerUtilTest { null } val result = requestIsEnabled(spyContext, mockExecutor) val result = repository.requestIsEnabled(mockExecutor) assertFalse(result.get()) } Loading @@ -126,8 +133,52 @@ class SatelliteManagerUtilTest { fun requestIsEnabled_nullSatelliteManager() = runBlocking { `when`(spyContext.getSystemService(SatelliteManager::class.java)).thenReturn(null) val result: ListenableFuture<Boolean> = requestIsEnabled(spyContext, mockExecutor) val result: ListenableFuture<Boolean> = repository.requestIsEnabled(mockExecutor) assertFalse(result.get()) } @Test fun getIsModemEnabledFlow_isSatelliteEnabledState() = runBlocking { `when`( mockSatelliteManager.registerForModemStateChanged( any(), any() ) ).thenAnswer { invocation -> val callback = invocation.getArgument<SatelliteModemStateCallback>(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<SatelliteModemStateCallback>(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