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

This commit is contained in:
Samuel Huang
2024-04-16 09:54:41 +00:00
committed by Android (Google) Code Review
5 changed files with 206 additions and 79 deletions

View File

@@ -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<Boolean> {
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<Boolean, SatelliteManager.SatelliteException> {
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"
}
}
}

View File

@@ -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<Boolean> {
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<Boolean, SatelliteManager.SatelliteException> {
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<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"
}
}

View File

@@ -26,16 +26,19 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settings.R import com.android.settings.R
import com.android.settings.network.SatelliteRepository
import com.android.settings.network.SubscriptionUtil import com.android.settings.network.SubscriptionUtil
import com.android.settings.spa.preference.ComposePreferenceController import com.android.settings.spa.preference.ComposePreferenceController
import com.android.settingslib.spa.widget.preference.MainSwitchPreference import com.android.settingslib.spa.widget.preference.MainSwitchPreference
import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class MobileNetworkSwitchController @JvmOverloads constructor( class MobileNetworkSwitchController @JvmOverloads constructor(
context: Context, context: Context,
preferenceKey: String, preferenceKey: String,
private val subscriptionRepository: SubscriptionRepository = SubscriptionRepository(context), private val subscriptionRepository: SubscriptionRepository = SubscriptionRepository(context),
private val satelliteRepository: SatelliteRepository = SatelliteRepository(context)
) : ComposePreferenceController(context, preferenceKey) { ) : ComposePreferenceController(context, preferenceKey) {
private var subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID private var subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID
@@ -54,7 +57,12 @@ class MobileNetworkSwitchController @JvmOverloads constructor(
subscriptionRepository.isSubscriptionEnabledFlow(subId) subscriptionRepository.isSubscriptionEnabledFlow(subId)
}.collectAsStateWithLifecycle(initialValue = null) }.collectAsStateWithLifecycle(initialValue = null)
val changeable by remember { 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) }.collectAsStateWithLifecycle(initialValue = true)
MainSwitchPreference(model = object : SwitchPreferenceModel { MainSwitchPreference(model = object : SwitchPreferenceModel {
override val title = stringResource(R.string.mobile_network_use_sim_on) override val title = stringResource(R.string.mobile_network_use_sim_on)

View File

@@ -29,7 +29,7 @@ import android.util.Log;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.network.SatelliteManagerUtil; import com.android.settings.network.SatelliteRepository;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
@@ -58,8 +58,8 @@ public class SimSlotChangeReceiver extends BroadcastReceiver {
if (shouldHandleSlotChange(context)) { if (shouldHandleSlotChange(context)) {
Log.d(TAG, "Checking satellite enabled status"); Log.d(TAG, "Checking satellite enabled status");
Executor executor = Executors.newSingleThreadExecutor(); Executor executor = Executors.newSingleThreadExecutor();
ListenableFuture<Boolean> satelliteEnabledFuture = SatelliteManagerUtil ListenableFuture<Boolean> satelliteEnabledFuture = new SatelliteRepository(context)
.requestIsEnabled(context, executor); .requestIsEnabled(executor);
satelliteEnabledFuture.addListener(() -> { satelliteEnabledFuture.addListener(() -> {
boolean isSatelliteEnabled = false; boolean isSatelliteEnabled = false;
try { try {

View File

@@ -20,10 +20,12 @@ import android.content.Context
import android.os.OutcomeReceiver import android.os.OutcomeReceiver
import android.telephony.satellite.SatelliteManager import android.telephony.satellite.SatelliteManager
import android.telephony.satellite.SatelliteManager.SatelliteException import android.telephony.satellite.SatelliteManager.SatelliteException
import android.telephony.satellite.SatelliteModemStateCallback
import androidx.test.core.app.ApplicationProvider 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 com.google.common.util.concurrent.ListenableFuture
import java.util.concurrent.Executor import java.util.concurrent.Executor
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@@ -42,7 +44,7 @@ import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
class SatelliteManagerUtilTest { class SatelliteRepositoryTest {
@JvmField @JvmField
@Rule @Rule
@@ -57,10 +59,15 @@ class SatelliteManagerUtilTest {
@Mock @Mock
private lateinit var mockExecutor: Executor private lateinit var mockExecutor: Executor
private lateinit var repository: SatelliteRepository
@Before @Before
fun setUp() { fun setUp() {
`when`(this.spyContext.getSystemService(SatelliteManager::class.java)) `when`(this.spyContext.getSystemService(SatelliteManager::class.java))
.thenReturn(mockSatelliteManager) .thenReturn(mockSatelliteManager)
repository = SatelliteRepository(spyContext)
} }
@Test @Test
@@ -78,7 +85,7 @@ class SatelliteManagerUtilTest {
} }
val result: ListenableFuture<Boolean> = val result: ListenableFuture<Boolean> =
requestIsEnabled(spyContext, mockExecutor) repository.requestIsEnabled(mockExecutor)
assertTrue(result.get()) assertTrue(result.get())
} }
@@ -98,7 +105,7 @@ class SatelliteManagerUtilTest {
} }
val result: ListenableFuture<Boolean> = val result: ListenableFuture<Boolean> =
requestIsEnabled(spyContext, mockExecutor) repository.requestIsEnabled(mockExecutor)
assertFalse(result.get()) assertFalse(result.get())
} }
@@ -117,7 +124,7 @@ class SatelliteManagerUtilTest {
null null
} }
val result = requestIsEnabled(spyContext, mockExecutor) val result = repository.requestIsEnabled(mockExecutor)
assertFalse(result.get()) assertFalse(result.get())
} }
@@ -126,8 +133,52 @@ class SatelliteManagerUtilTest {
fun requestIsEnabled_nullSatelliteManager() = runBlocking { fun requestIsEnabled_nullSatelliteManager() = runBlocking {
`when`(spyContext.getSystemService(SatelliteManager::class.java)).thenReturn(null) `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()) 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()
}
} }