package com.android.settings.network.telephony.carriersettingsoverride import android.app.Application import android.os.PersistableBundle import android.os.RemoteException import android.telephony.CarrierConfigManager import android.telephony.SubscriptionManager import android.util.ArrayMap import android.util.Log import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.application import androidx.lifecycle.viewModelScope import com.android.settings.R import com.android.settings.network.telephony.CarrierConfigRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor import kotlinx.coroutines.async import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull private const val TAG = "CarriSettOverridVM" class CarrierSettingsOverridesViewModel(application: Application) : AndroidViewModel(application) { private val carrierConfigRepo = CarrierConfigRepository(application) private val carrierConfigManager: CarrierConfigManager = application.getSystemService(CarrierConfigManager::class.java)!! private fun carrierConfigChanges(subId: Int): Flow = callbackFlow { val manager = carrierConfigManager val executor = Dispatchers.Default.asExecutor() val listener = CarrierConfigManager.CarrierConfigChangeListener { _, subscriptionId, _, _ -> if (subscriptionId == subId) { trySend(Unit) } } Log.d(TAG, "registering config listener") manager.registerCarrierConfigChangeListener(executor, listener) awaitClose { Log.d(TAG, "unregistering config listener") manager.unregisterCarrierConfigChangeListener(listener) } }.conflate() private val subId = MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID) private val carrierConfigUpdatePing: SharedFlow = subId .filter { it != SubscriptionManager.INVALID_SUBSCRIPTION_ID } .flatMapLatest { sid -> carrierConfigChanges(sid) } .shareIn( scope = viewModelScope, started = SharingStarted.Lazily, replay = 0 ) private val _overrideStates = mutableStateListOf() val overrideStates: List get() = _overrideStates /** * Whether the SIM is currently using an active override */ private val _isAnOverrideActive = MutableStateFlow(false) val isAnOverrideActive: StateFlow = _isAnOverrideActive private var isInitialized = false fun init(subId: Int) { if (isInitialized) return this.subId.update { oldSubId -> if (oldSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { subId } else { return } } viewModelScope.launch(Dispatchers.Default) { reloadFromCarrierConfig() isInitialized = true } } private val _unrecognizedOverrides = MutableStateFlow?>(null) val unrecognizedOverrides: StateFlow?> = _unrecognizedOverrides private fun setOverrideConfig(newOverrides: PersistableBundle?): Boolean { carrierConfigManager.overrideConfig(subId.value, newOverrides, CarrierConfigManager.CONFIG_OVERRIDE_TYPE_GRAPHENEOS) return true } private suspend fun reloadFromCarrierConfig() { val activeOverrides = carrierConfigManager.getConfigOverrides(subId.value, false) Log.d(TAG, "activeOverrides: $activeOverrides") _isAnOverrideActive.update { !activeOverrides.isEmpty } // Display any overrides from AOSP. We expect these to not be set, because // CarrierConfigLoader's overrideConfig is normally just a test API. val overridesFromAosp: PersistableBundle = carrierConfigManager.getConfigOverrides(subId.value, true) if (overridesFromAosp.isEmpty) { _unrecognizedOverrides.update { null } } else { // every override from AOSP is unrecognized val newUnrecognizedOverrides = ArrayMap() overridesFromAosp.keySet().forEach { key -> newUnrecognizedOverrides[key] = overridesFromAosp.get(key) } Log.d(TAG, "found newUnrecognizedOverrides: $newUnrecognizedOverrides") _unrecognizedOverrides.update { newUnrecognizedOverrides.ifEmpty { null } } } // Construct states val configList = allowedUserChangeableCarrierConfigOptions.map { flagKey -> val state = CarrierConfigState.createState( subId.value, carrierConfigRepo, flagKey, activeOverrides ?: PersistableBundle() ) if (state.isOverriddenBefore.value && !_isAnOverrideActive.value) { _isAnOverrideActive.update { true } } state } Log.d(TAG, "new configList is $configList") withContext(Dispatchers.Main) { if (_overrideStates.isEmpty()) { _overrideStates.addAll(configList) } else { // preserve the selections if just trying to reload with latest state _overrideStates.indices.forEach { index -> val newState = configList[index] _overrideStates[index] = _overrideStates[index].copy( indexOfValueBefore = newState.indexOfValueBefore, isOverriddenBefore = newState.isOverriddenBefore ) } } } } private val _isOverriding = MutableStateFlow(false) val isOverrideInProgress: StateFlow = _isOverriding sealed class MessageType { data class ErrorMessage(val msg: String, val title: String? = null) : MessageType() data object TurnOffToEdit : MessageType() } private val _message = MutableStateFlow(null) val message: Flow = combine(_message, _isAnOverrideActive) { errorMsg, active -> errorMsg ?: if (active) { MessageType.TurnOffToEdit } else { null } }.distinctUntilChanged() private val gate = Mutex() fun submitOverrides(clearOverrides: Boolean): Unit = viewModelScope.launch { if (!isInitialized) return@launch if (!gate.tryLock()) return@launch _isOverriding.update { true } try { val overrides: PersistableBundle? = if (clearOverrides) { null } else { val bundle = PersistableBundle() for (state in _overrideStates) { // null index means disabled. But the disabled option at the end could be // selected as well resulting in non-null configState. val configState = state.getConfigStateFromIndex() ?: continue configState.insertIntoBundle(state.key.keys, bundle) } if (bundle.isEmpty) { Log.d(TAG, "attempting to submit no overrides but not disabling") _message.update { MessageType.ErrorMessage( application.getString( R.string.carrier_settings_override_no_override_selected_message ), title = application.getString( R.string.carrier_settings_override_no_override_selected_title ), ) } return@launch } bundle } Log.d(TAG, "submitting overrides $overrides") try { runThenAwaitCarrierConfigUpdateIfTrue { if (setOverrideConfig(overrides)) { _message.update { null } true } else { _message.update { MessageType.ErrorMessage( application.getString( R.string.carrier_settings_override_error_unable_to_connect_to_config__s, "setExtOverrideConfigs false" ) ) } false } } if (clearOverrides && !_unrecognizedOverrides.value.isNullOrEmpty()) { Log.d(TAG, "clearing all AOSP overrides") runThenAwaitCarrierConfigUpdateIfTrue { carrierConfigManager.overrideConfig(subId.value, null, CarrierConfigManager.CONFIG_OVERRIDE_TYPE_AOSP_PERSISTENT) true } } } catch (e: RemoteException) { Log.e(TAG, "error while overriding config", e) _message.update { MessageType.ErrorMessage("RemoteException: ${e.message}") } } reloadFromCarrierConfig() // delay so user can't spam quickly delay(250L) } finally { gate.unlock() _isOverriding.update { false } } }.let { } private suspend inline fun runThenAwaitCarrierConfigUpdateIfTrue( timeoutMillis: Long = 2000L, crossinline block: suspend () -> Boolean ) { coroutineScope { // setOverrideConfig, etc. do the updates asynchronously, so the config is not // guaranteed to update immediately after RPC Binder calls. Wait until an update is // broadcast after making the override call. // // Start waiting here to avoid missing very a fast update. val waiter = async { carrierConfigUpdatePing.first() } try { if (block()) { withTimeoutOrNull(timeoutMillis) { waiter.await() } } } finally { waiter.cancel() } } } } private suspend fun Flow.firstWithTimeoutOrNull(timeMillis: Long = 1000): T? = withTimeoutOrNull(timeMillis) { filter { it != null }.first() }