Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8782bd4f authored by Samuel Huang's avatar Samuel Huang Committed by Android (Google) Code Review
Browse files

Merge "Disable SIM On/Off operation when device is in Satellite Enabled Mode" into main

parents 7eaa2b16 cbf51542
Loading
Loading
Loading
Loading
+137 −0
Original line number Diff line number Diff line
@@ -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) {
@@ -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"
    }
}
+9 −1
Original line number Diff line number Diff line
@@ -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)
+3 −3
Original line number Diff line number Diff line
@@ -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<Boolean> satelliteEnabledFuture = SatelliteManagerUtil
                    .requestIsEnabled(context, executor);
            ListenableFuture<Boolean> satelliteEnabledFuture = new SatelliteRepository(context)
                    .requestIsEnabled(executor);
            satelliteEnabledFuture.addListener(() -> {
                boolean isSatelliteEnabled = false;
                try {
+57 −6
Original line number Diff line number Diff line
@@ -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<Boolean> =
            requestIsEnabled(spyContext, mockExecutor)
            repository.requestIsEnabled(mockExecutor)

        assertTrue(result.get())
    }
@@ -98,7 +105,7 @@ class SatelliteManagerUtilTest {
            }

        val result: ListenableFuture<Boolean> =
            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<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