Merge "Disable SIM On/Off operation when device is in Satellite Enabled Mode" into 24D1-dev

This commit is contained in:
Samuel Huang
2024-04-17 07:44:44 +00:00
committed by Android (Google) Code Review
13 changed files with 603 additions and 543 deletions

View File

@@ -18,9 +18,8 @@
xmlns:settings="http://schemas.android.com/apk/res-auto"
android:key="mobile_network_pref_screen">
<com.android.settings.widget.SettingsMainSwitchPreference
<com.android.settings.spa.preference.ComposePreference
android:key="use_sim_switch"
android:title="@string/mobile_network_use_sim_on"
settings:controller="com.android.settings.network.telephony.MobileNetworkSwitchController"/>
<PreferenceCategory

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,138 @@
/*
* 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

@@ -20,6 +20,7 @@ import android.app.Application
import android.telephony.SubscriptionManager
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.android.settings.network.telephony.getSelectableSubscriptionInfoList
import com.android.settings.network.telephony.subscriptionsChangedFlow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
@@ -41,10 +42,10 @@ class SubscriptionInfoListViewModel(application: Application) : AndroidViewModel
}.stateIn(scope, SharingStarted.Eagerly, initialValue = emptyList())
/**
* Getting the Selectable SubscriptionInfo List from the SubscriptionManager's
* Getting the Selectable SubscriptionInfo List from the SubscriptionRepository's
* getAvailableSubscriptionInfoList
*/
val selectableSubscriptionInfoListFlow = application.subscriptionsChangedFlow().map {
SubscriptionUtil.getSelectableSubscriptionInfoList(application)
application.getSelectableSubscriptionInfoList()
}.stateIn(scope, SharingStarted.Eagerly, initialValue = emptyList())
}

View File

@@ -50,12 +50,12 @@ import com.android.settings.network.helper.SelectableSubscriptions;
import com.android.settings.network.helper.SubscriptionAnnotation;
import com.android.settings.network.telephony.DeleteEuiccSubscriptionDialogActivity;
import com.android.settings.network.telephony.EuiccRacConnectivityDialogActivity;
import com.android.settings.network.telephony.SubscriptionRepositoryKt;
import com.android.settings.network.telephony.ToggleSubscriptionDialogActivity;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
@@ -505,40 +505,7 @@ public class SubscriptionUtil {
* @return list of user selectable subscriptions.
*/
public static List<SubscriptionInfo> getSelectableSubscriptionInfoList(Context context) {
SubscriptionManager subManager = context.getSystemService(SubscriptionManager.class);
List<SubscriptionInfo> availableList = subManager.getAvailableSubscriptionInfoList();
if (availableList == null) {
return null;
} else {
// Multiple subscriptions in a group should only have one representative.
// It should be the current active primary subscription if any, or any
// primary subscription.
List<SubscriptionInfo> selectableList = new ArrayList<>();
Map<ParcelUuid, SubscriptionInfo> groupMap = new HashMap<>();
for (SubscriptionInfo info : availableList) {
// Opportunistic subscriptions are considered invisible
// to users so they should never be returned.
if (!isSubscriptionVisible(subManager, context, info)) continue;
ParcelUuid groupUuid = info.getGroupUuid();
if (groupUuid == null) {
// Doesn't belong to any group. Add in the list.
selectableList.add(info);
} else if (!groupMap.containsKey(groupUuid)
|| (groupMap.get(groupUuid).getSimSlotIndex() == INVALID_SIM_SLOT_INDEX
&& info.getSimSlotIndex() != INVALID_SIM_SLOT_INDEX)) {
// If it belongs to a group that has never been recorded or it's the current
// active subscription, add it in the list.
selectableList.remove(groupMap.get(groupUuid));
selectableList.add(info);
groupMap.put(groupUuid, info);
}
}
Log.d(TAG, "getSelectableSubscriptionInfoList: " + selectableList);
return selectableList;
}
return SubscriptionRepositoryKt.getSelectableSubscriptionInfoList(context);
}
/**

View File

@@ -1,147 +0,0 @@
/*
* Copyright (C) 2019 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.telephony;
import static android.telephony.TelephonyManager.CALL_STATE_IDLE;
import static androidx.lifecycle.Lifecycle.Event.ON_PAUSE;
import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
import android.content.Context;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyManager;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.preference.PreferenceScreen;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.network.SubscriptionUtil;
import com.android.settings.network.SubscriptionsChangeListener;
import com.android.settings.widget.SettingsMainSwitchPreference;
/** This controls a switch to allow enabling/disabling a mobile network */
public class MobileNetworkSwitchController extends BasePreferenceController implements
SubscriptionsChangeListener.SubscriptionsChangeListenerClient, LifecycleObserver {
private static final String TAG = "MobileNetworkSwitchCtrl";
private SettingsMainSwitchPreference mSwitchBar;
private int mSubId;
private SubscriptionsChangeListener mChangeListener;
private SubscriptionManager mSubscriptionManager;
private TelephonyManager mTelephonyManager;
private CallStateTelephonyCallback mCallStateCallback;
public MobileNetworkSwitchController(Context context, String preferenceKey) {
super(context, preferenceKey);
mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
mChangeListener = new SubscriptionsChangeListener(context, this);
}
void init(int subId) {
mSubId = subId;
mTelephonyManager = mTelephonyManager.createForSubscriptionId(mSubId);
}
@OnLifecycleEvent(ON_RESUME)
public void onResume() {
mChangeListener.start();
if (mCallStateCallback == null) {
mCallStateCallback = new CallStateTelephonyCallback();
mTelephonyManager.registerTelephonyCallback(
mContext.getMainExecutor(), mCallStateCallback);
}
update();
}
@OnLifecycleEvent(ON_PAUSE)
public void onPause() {
if (mCallStateCallback != null) {
mTelephonyManager.unregisterTelephonyCallback(mCallStateCallback);
mCallStateCallback = null;
}
mChangeListener.stop();
}
@Override
public void displayPreference(PreferenceScreen screen) {
super.displayPreference(screen);
mSwitchBar = (SettingsMainSwitchPreference) screen.findPreference(mPreferenceKey);
mSwitchBar.setOnBeforeCheckedChangeListener((isChecked) -> {
// TODO b/135222940: re-evaluate whether to use
// mSubscriptionManager#isSubscriptionEnabled
if (mSubscriptionManager.isActiveSubscriptionId(mSubId) != isChecked) {
SubscriptionUtil.startToggleSubscriptionDialogActivity(mContext, mSubId, isChecked);
return true;
}
return false;
});
update();
}
private void update() {
if (mSwitchBar == null) {
return;
}
SubscriptionInfo subInfo = null;
for (SubscriptionInfo info : SubscriptionUtil.getAvailableSubscriptions(mContext)) {
if (info.getSubscriptionId() == mSubId) {
subInfo = info;
break;
}
}
// For eSIM, we always want the toggle. If telephony stack support disabling a pSIM
// directly, we show the toggle.
if (subInfo == null || (!subInfo.isEmbedded() && !SubscriptionUtil.showToggleForPhysicalSim(
mSubscriptionManager))) {
mSwitchBar.hide();
} else {
mSwitchBar.show();
mSwitchBar.setCheckedInternal(mSubscriptionManager.isActiveSubscriptionId(mSubId));
}
}
@Override
public int getAvailabilityStatus() {
return AVAILABLE_UNSEARCHABLE;
}
@Override
public void onAirplaneModeChanged(boolean airplaneModeEnabled) {
}
@Override
public void onSubscriptionsChanged() {
update();
}
private class CallStateTelephonyCallback extends TelephonyCallback implements
TelephonyCallback.CallStateListener {
@Override
public void onCallStateChanged(int state) {
mSwitchBar.setSwitchBarEnabled(state == CALL_STATE_IDLE);
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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.telephony
import android.content.Context
import android.telephony.SubscriptionManager
import android.telephony.TelephonyManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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
override fun getAvailabilityStatus() = AVAILABLE_UNSEARCHABLE
fun init(subId: Int) {
this.subId = subId
}
@Composable
override fun Content() {
val context = LocalContext.current
if (remember { !context.isVisible() }) return
val checked by remember {
subscriptionRepository.isSubscriptionEnabledFlow(subId)
}.collectAsStateWithLifecycle(initialValue = null)
val changeable by remember {
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)
override val changeable = { changeable }
override val checked = { checked }
override val onCheckedChange = { newChecked: Boolean ->
SubscriptionUtil.startToggleSubscriptionDialogActivity(mContext, subId, newChecked)
}
})
}
private fun Context.isVisible(): Boolean {
val subInfo = subscriptionRepository.getSelectableSubscriptionInfoList()
.firstOrNull { it.subscriptionId == subId }
?: return false
// For eSIM, we always want the toggle. If telephony stack support disabling a pSIM
// directly, we show the toggle.
return subInfo.isEmbedded || requireSubscriptionManager().canDisablePhysicalSubscription()
}
}

View File

@@ -20,30 +20,49 @@ import android.content.Context
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.util.Log
import androidx.lifecycle.LifecycleOwner
import com.android.settings.network.SubscriptionUtil
import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
private const val TAG = "SubscriptionRepository"
fun Context.isSubscriptionEnabledFlow(subId: Int) = subscriptionsChangedFlow().map {
val subscriptionManager = getSystemService(SubscriptionManager::class.java)
class SubscriptionRepository(private val context: Context) {
/**
* Return a list of subscriptions that are available and visible to the user.
*
* @return list of user selectable subscriptions.
*/
fun getSelectableSubscriptionInfoList(): List<SubscriptionInfo> =
context.getSelectableSubscriptionInfoList()
fun isSubscriptionEnabledFlow(subId: Int) = context.isSubscriptionEnabledFlow(subId)
}
val Context.subscriptionManager: SubscriptionManager?
get() = getSystemService(SubscriptionManager::class.java)
fun Context.requireSubscriptionManager(): SubscriptionManager = subscriptionManager!!
fun Context.isSubscriptionEnabledFlow(subId: Int) = subscriptionsChangedFlow().map {
subscriptionManager?.isSubscriptionEnabled(subId) ?: false
}.flowOn(Dispatchers.Default)
}.conflate().onEach { Log.d(TAG, "[$subId] isSubscriptionEnabledFlow: $it") }
.flowOn(Dispatchers.Default)
fun Context.phoneNumberFlow(subscriptionInfo: SubscriptionInfo) = subscriptionsChangedFlow().map {
SubscriptionUtil.getFormattedPhoneNumber(this, subscriptionInfo)
}.flowOn(Dispatchers.Default)
}.filterNot { it.isNullOrEmpty() }.flowOn(Dispatchers.Default)
fun Context.subscriptionsChangedFlow() = callbackFlow {
val subscriptionManager = getSystemService(SubscriptionManager::class.java)!!
val subscriptionManager = requireSubscriptionManager()
val listener = object : SubscriptionManager.OnSubscriptionsChangedListener() {
override fun onSubscriptionsChanged() {
@@ -58,3 +77,36 @@ fun Context.subscriptionsChangedFlow() = callbackFlow {
awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(listener) }
}.conflate().onEach { Log.d(TAG, "subscriptions changed") }.flowOn(Dispatchers.Default)
/**
* Return a list of subscriptions that are available and visible to the user.
*
* @return list of user selectable subscriptions.
*/
fun Context.getSelectableSubscriptionInfoList(): List<SubscriptionInfo> {
val subscriptionManager = requireSubscriptionManager()
val availableList = subscriptionManager.getAvailableSubscriptionInfoList() ?: return emptyList()
val visibleList = availableList.filter { subInfo ->
// Opportunistic subscriptions are considered invisible
// to users so they should never be returned.
SubscriptionUtil.isSubscriptionVisible(subscriptionManager, this, subInfo)
}
// Multiple subscriptions in a group should only have one representative.
// It should be the current active primary subscription if any, or any primary subscription.
val groupUuidToSelectedIdMap = visibleList
.groupBy { it.groupUuid }
.mapValues { (_, subInfos) ->
subInfos.filter { it.simSlotIndex != SubscriptionManager.INVALID_SIM_SLOT_INDEX }
.ifEmpty { subInfos }
.minOf { it.subscriptionId }
}
return visibleList
.filter { subInfo ->
val groupUuid = subInfo.groupUuid ?: return@filter true
groupUuidToSelectedIdMap[groupUuid] == subInfo.subscriptionId
}
.sortedBy { it.subscriptionId }
.also { Log.d(TAG, "getSelectableSubscriptionInfoList: $it") }
}

View File

@@ -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 {

View File

@@ -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()
}
}

View File

@@ -0,0 +1,169 @@
/*
* 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.telephony
import android.content.Context
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.isOff
import androidx.compose.ui.test.isOn
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settings.R
import com.android.settingslib.spa.testutils.waitUntilExists
import kotlinx.coroutines.flow.flowOf
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.doNothing
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.spy
import org.mockito.kotlin.stub
import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class MobileNetworkSwitchControllerTest {
@get:Rule
val composeTestRule = createComposeRule()
private val mockSubscriptionManager = mock<SubscriptionManager> {
on { isSubscriptionEnabled(SUB_ID) } doReturn true
}
private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
on { subscriptionManager } doReturn mockSubscriptionManager
doNothing().whenever(mock).startActivity(any())
}
private val mockSubscriptionRepository = mock<SubscriptionRepository> {
on { getSelectableSubscriptionInfoList() } doReturn listOf(SubInfo)
on { isSubscriptionEnabledFlow(SUB_ID) } doReturn flowOf(false)
}
private val controller = MobileNetworkSwitchController(
context = context,
preferenceKey = TEST_KEY,
subscriptionRepository = mockSubscriptionRepository,
).apply { init(SUB_ID) }
@Test
fun isVisible_pSimAndCanDisablePhysicalSubscription_returnTrue() {
val pSimSubInfo = SubscriptionInfo.Builder().apply {
setId(SUB_ID)
setEmbedded(false)
}.build()
mockSubscriptionManager.stub {
on { canDisablePhysicalSubscription() } doReturn true
}
mockSubscriptionRepository.stub {
on { getSelectableSubscriptionInfoList() } doReturn listOf(pSimSubInfo)
}
setContent()
composeTestRule.onNodeWithText(context.getString(R.string.mobile_network_use_sim_on))
.assertIsDisplayed()
}
@Test
fun isVisible_pSimAndCannotDisablePhysicalSubscription_returnFalse() {
val pSimSubInfo = SubscriptionInfo.Builder().apply {
setId(SUB_ID)
setEmbedded(false)
}.build()
mockSubscriptionManager.stub {
on { canDisablePhysicalSubscription() } doReturn false
}
mockSubscriptionRepository.stub {
on { getSelectableSubscriptionInfoList() } doReturn listOf(pSimSubInfo)
}
setContent()
composeTestRule.onNodeWithText(context.getString(R.string.mobile_network_use_sim_on))
.assertDoesNotExist()
}
@Test
fun isVisible_eSim_returnTrue() {
val eSimSubInfo = SubscriptionInfo.Builder().apply {
setId(SUB_ID)
setEmbedded(true)
}.build()
mockSubscriptionRepository.stub {
on { getSelectableSubscriptionInfoList() } doReturn listOf(eSimSubInfo)
}
setContent()
composeTestRule.onNodeWithText(context.getString(R.string.mobile_network_use_sim_on))
.assertIsDisplayed()
}
@Test
fun isChecked_subscriptionEnabled_switchIsOn() {
mockSubscriptionRepository.stub {
on { isSubscriptionEnabledFlow(SUB_ID) } doReturn flowOf(true)
}
setContent()
composeTestRule.waitUntilExists(
hasText(context.getString(R.string.mobile_network_use_sim_on)) and isOn()
)
}
@Test
fun isChecked_subscriptionNotEnabled_switchIsOff() {
mockSubscriptionRepository.stub {
on { isSubscriptionEnabledFlow(SUB_ID) } doReturn flowOf(false)
}
setContent()
composeTestRule.waitUntilExists(
hasText(context.getString(R.string.mobile_network_use_sim_on)) and isOff()
)
}
private fun setContent() {
composeTestRule.setContent {
CompositionLocalProvider(LocalContext provides context) {
controller.Content()
}
}
}
private companion object {
const val TEST_KEY = "test_key"
const val SUB_ID = 123
val SubInfo: SubscriptionInfo = SubscriptionInfo.Builder().apply {
setId(SUB_ID)
setEmbedded(true)
}.build()
}
}

View File

@@ -17,12 +17,14 @@
package com.android.settings.network.telephony
import android.content.Context
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
import com.android.settingslib.spa.testutils.toListWithTimeout
import com.google.common.truth.Truth.assertThat
import java.util.UUID
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
@@ -47,16 +49,16 @@ class SubscriptionRepositoryTest {
}
private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
on { getSystemService(SubscriptionManager::class.java) } doReturn mockSubscriptionManager
on { subscriptionManager } doReturn mockSubscriptionManager
}
@Test
fun isSubscriptionEnabledFlow() = runBlocking {
mockSubscriptionManager.stub {
on { isSubscriptionEnabled(SUB_ID) } doReturn true
on { isSubscriptionEnabled(SUB_ID_1) } doReturn true
}
val isEnabled = context.isSubscriptionEnabledFlow(SUB_ID).firstWithTimeoutOrNull()
val isEnabled = context.isSubscriptionEnabledFlow(SUB_ID_1).firstWithTimeoutOrNull()
assertThat(isEnabled).isTrue()
}
@@ -80,7 +82,87 @@ class SubscriptionRepositoryTest {
assertThat(listDeferred.await()).hasSize(2)
}
@Test
fun getSelectableSubscriptionInfoList_sortedBySubId() {
mockSubscriptionManager.stub {
on { getAvailableSubscriptionInfoList() } doReturn listOf(
SubscriptionInfo.Builder().apply {
setId(SUB_ID_2)
}.build(),
SubscriptionInfo.Builder().apply {
setId(SUB_ID_1)
}.build(),
)
}
val subInfos = context.getSelectableSubscriptionInfoList()
assertThat(subInfos.map { it.subscriptionId }).containsExactly(SUB_ID_1, SUB_ID_2).inOrder()
}
@Test
fun getSelectableSubscriptionInfoList_sameGroupAndOneHasSlot_returnTheOneWithSimSlotIndex() {
mockSubscriptionManager.stub {
on { getAvailableSubscriptionInfoList() } doReturn listOf(
SubscriptionInfo.Builder().apply {
setId(SUB_ID_1)
setGroupUuid(GROUP_UUID)
}.build(),
SubscriptionInfo.Builder().apply {
setId(SUB_ID_2)
setGroupUuid(GROUP_UUID)
setSimSlotIndex(SIM_SLOT_INDEX)
}.build(),
)
}
val subInfos = context.getSelectableSubscriptionInfoList()
assertThat(subInfos.map { it.subscriptionId }).containsExactly(SUB_ID_2)
}
@Test
fun getSelectableSubscriptionInfoList_sameGroupAndNonHasSlot_returnTheOneWithMinimumSubId() {
mockSubscriptionManager.stub {
on { getAvailableSubscriptionInfoList() } doReturn listOf(
SubscriptionInfo.Builder().apply {
setId(SUB_ID_2)
setGroupUuid(GROUP_UUID)
}.build(),
SubscriptionInfo.Builder().apply {
setId(SUB_ID_1)
setGroupUuid(GROUP_UUID)
}.build(),
)
}
val subInfos = context.getSelectableSubscriptionInfoList()
assertThat(subInfos.map { it.subscriptionId }).containsExactly(SUB_ID_1)
}
@Test
fun phoneNumberFlow() = runBlocking {
mockSubscriptionManager.stub {
on { getPhoneNumber(SUB_ID_1) } doReturn NUMBER_1
}
val subInfo = SubscriptionInfo.Builder().apply {
setId(SUB_ID_1)
setMcc(MCC)
}.build()
val phoneNumber = context.phoneNumberFlow(subInfo).firstWithTimeoutOrNull()
assertThat(phoneNumber).isEqualTo(NUMBER_1)
}
private companion object {
const val SUB_ID = 1
const val SUB_ID_1 = 1
const val SUB_ID_2 = 2
val GROUP_UUID = UUID.randomUUID().toString()
const val SIM_SLOT_INDEX = 1
const val NUMBER_1 = "000000001"
const val MCC = "310"
}
}

View File

@@ -1,269 +0,0 @@
/*
* Copyright (C) 2020 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.telephony;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Looper;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyManager;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.preference.PreferenceViewHolder;
import androidx.test.annotation.UiThreadTest;
import androidx.test.core.app.ApplicationProvider;
import com.android.settings.network.SubscriptionUtil;
import com.android.settings.widget.SettingsMainSwitchPreference;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.Arrays;
import java.util.concurrent.Executor;
public class MobileNetworkSwitchControllerTest {
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock
private SubscriptionManager mSubscriptionManager;
@Mock
private SubscriptionInfo mSubscription;
@Mock
private TelephonyManager mTelephonyManager;
private PreferenceScreen mScreen;
private PreferenceManager mPreferenceManager;
private SettingsMainSwitchPreference mSwitchBar;
private Context mContext;
private MobileNetworkSwitchController mController;
private int mSubId = 123;
@Before
public void setUp() {
if (Looper.myLooper() == null) {
Looper.prepare();
}
mContext = spy(ApplicationProvider.getApplicationContext());
when(mContext.getSystemService(SubscriptionManager.class)).thenReturn(mSubscriptionManager);
when(mSubscriptionManager.setSubscriptionEnabled(eq(mSubId), anyBoolean()))
.thenReturn(true);
when(mSubscription.isEmbedded()).thenReturn(true);
when(mSubscription.getSubscriptionId()).thenReturn(mSubId);
// Most tests want to have 2 available subscriptions so that the switch bar will show.
final SubscriptionInfo sub2 = mock(SubscriptionInfo.class);
when(sub2.getSubscriptionId()).thenReturn(456);
SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(mSubscription, sub2));
when(mContext.getSystemService(TelephonyManager.class)).thenReturn(mTelephonyManager);
when(mTelephonyManager.createForSubscriptionId(mSubId))
.thenReturn(mTelephonyManager);
final String key = "prefKey";
mController = new MobileNetworkSwitchController(mContext, key);
mController.init(mSubscription.getSubscriptionId());
mPreferenceManager = new PreferenceManager(mContext);
mScreen = mPreferenceManager.createPreferenceScreen(mContext);
mSwitchBar = new SettingsMainSwitchPreference(mContext);
mSwitchBar.setKey(key);
mSwitchBar.setTitle("123");
mScreen.addPreference(mSwitchBar);
final LayoutInflater inflater = LayoutInflater.from(mContext);
final View view = inflater.inflate(mSwitchBar.getLayoutResource(),
new LinearLayout(mContext), false);
final PreferenceViewHolder holder = PreferenceViewHolder.createInstanceForTests(view);
mSwitchBar.onBindViewHolder(holder);
}
@After
public void cleanUp() {
SubscriptionUtil.setAvailableSubscriptionsForTesting(null);
}
@Test
@UiThreadTest
public void isAvailable_pSIM_isNotAvailable() {
when(mSubscription.isEmbedded()).thenReturn(false);
mController.displayPreference(mScreen);
assertThat(mSwitchBar.isShowing()).isFalse();
when(mSubscriptionManager.canDisablePhysicalSubscription()).thenReturn(true);
mController.displayPreference(mScreen);
assertThat(mSwitchBar.isShowing()).isTrue();
}
@Test
@UiThreadTest
public void displayPreference_oneEnabledSubscription_switchBarNotHidden() {
doReturn(true).when(mSubscriptionManager).isActiveSubscriptionId(mSubId);
SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(mSubscription));
mController.displayPreference(mScreen);
assertThat(mSwitchBar.isShowing()).isTrue();
}
@Test
@UiThreadTest
public void displayPreference_oneDisabledSubscription_switchBarNotHidden() {
doReturn(false).when(mSubscriptionManager).isActiveSubscriptionId(mSubId);
SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(mSubscription));
mController.displayPreference(mScreen);
assertThat(mSwitchBar.isShowing()).isTrue();
}
@Test
@UiThreadTest
public void displayPreference_subscriptionEnabled_switchIsOn() {
when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(true);
mController.displayPreference(mScreen);
assertThat(mSwitchBar.isShowing()).isTrue();
assertThat(mSwitchBar.isChecked()).isTrue();
}
@Test
@UiThreadTest
public void displayPreference_subscriptionDisabled_switchIsOff() {
when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(false);
mController.displayPreference(mScreen);
assertThat(mSwitchBar.isShowing()).isTrue();
assertThat(mSwitchBar.isChecked()).isFalse();
}
@Test
@UiThreadTest
public void switchChangeListener_fromEnabledToDisabled_setSubscriptionEnabledCalledCorrectly() {
when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(true);
mController.displayPreference(mScreen);
assertThat(mSwitchBar.isShowing()).isTrue();
assertThat(mSwitchBar.isChecked()).isTrue();
final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
doNothing().when(mContext).startActivity(intentCaptor.capture());
// set switch off then should start a Activity.
mSwitchBar.setChecked(false);
when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(false);
// Simulate action of back from previous activity.
mController.displayPreference(mScreen);
Bundle extra = intentCaptor.getValue().getExtras();
verify(mContext, times(1)).startActivity(any());
assertThat(extra.getInt(ToggleSubscriptionDialogActivity.ARG_SUB_ID)).isEqualTo(mSubId);
assertThat(extra.getBoolean(ToggleSubscriptionDialogActivity.ARG_enable))
.isEqualTo(false);
assertThat(mSwitchBar.isChecked()).isFalse();
}
@Test
@UiThreadTest
public void switchChangeListener_fromEnabledToDisabled_setSubscriptionEnabledFailed() {
when(mSubscriptionManager.setSubscriptionEnabled(eq(mSubId), anyBoolean()))
.thenReturn(false);
when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(true);
mController.displayPreference(mScreen);
assertThat(mSwitchBar.isShowing()).isTrue();
assertThat(mSwitchBar.isChecked()).isTrue();
final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
doNothing().when(mContext).startActivity(intentCaptor.capture());
// set switch off then should start a Activity.
mSwitchBar.setChecked(false);
// Simulate action of back from previous activity.
mController.displayPreference(mScreen);
Bundle extra = intentCaptor.getValue().getExtras();
verify(mContext, times(1)).startActivity(any());
assertThat(extra.getInt(ToggleSubscriptionDialogActivity.ARG_SUB_ID)).isEqualTo(mSubId);
assertThat(extra.getBoolean(ToggleSubscriptionDialogActivity.ARG_enable))
.isEqualTo(false);
assertThat(mSwitchBar.isChecked()).isTrue();
}
@Test
@UiThreadTest
public void switchChangeListener_fromDisabledToEnabled_setSubscriptionEnabledCalledCorrectly() {
when(mSubscriptionManager.isActiveSubscriptionId(mSubId)).thenReturn(false);
mController.displayPreference(mScreen);
assertThat(mSwitchBar.isShowing()).isTrue();
assertThat(mSwitchBar.isChecked()).isFalse();
final ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
doNothing().when(mContext).startActivity(intentCaptor.capture());
mSwitchBar.setChecked(true);
Bundle extra = intentCaptor.getValue().getExtras();
verify(mContext, times(1)).startActivity(any());
assertThat(extra.getInt(ToggleSubscriptionDialogActivity.ARG_SUB_ID)).isEqualTo(mSubId);
assertThat(extra.getBoolean(ToggleSubscriptionDialogActivity.ARG_enable)).isEqualTo(true);
}
@Test
@UiThreadTest
public void onResumeAndonPause_registerAndUnregisterTelephonyCallback() {
mController.onResume();
verify(mTelephonyManager)
.registerTelephonyCallback(any(Executor.class), any(TelephonyCallback.class));
mController.onPause();
verify(mTelephonyManager)
.unregisterTelephonyCallback(any(TelephonyCallback.class));
}
@Test
@UiThreadTest
public void onPause_doNotRegisterAndUnregisterTelephonyCallback() {
mController.onPause();
verify(mTelephonyManager, times(0))
.unregisterTelephonyCallback(any(TelephonyCallback.class));
}
}