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

Commit 7b7b46bc authored by Evan Laird's avatar Evan Laird
Browse files

[Sat] Carrier-based view model + signal

This CL does a couple of things that are all related to get the signal
showing:

1. Update the MobileIconViewModel to have two distinct "modes", based on
   whether or not the underlying subscription represents a "non
   terrestrial" network or not. This is achieved by wrapping up the
   existing "cellular" behavior in its own private subclass, and
   implementing the satellite behavior entirely additively.

2. Update SignalIconModel to be a sealed interface to make room for the
   resource-backed satellite icon in addition to the existing
   SignalDrawable icon.

3. Add `isNonTerrestrial` to the interactor so (based on a flag) it can
   expose the new field

4. Add the satellite icon calculation to the interactor

Test: MobileIconInteractorTest
Test: SignalIconModelParameterizedTest
Test: LocationBasedMobileIconViewModelTest
Test: MobileIconViewModelTest
Test: all tests in the mobile/ package
Bug: 311417356
Flag: ACONFIG com.android.internal.telephony.flags.carrier_enabled_satellite_flag DEVELOPMENT
Change-Id: I5c71e7b3dd757b17e71fcdc6b7742eba1775c244
parent 0baea084
Loading
Loading
Loading
Loading
+50 −16
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.pipeline.mobile.domain.interactor

import android.content.Context
import com.android.internal.telephony.flags.Flags
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.graph.SignalDrawable
import com.android.settingslib.mobile.MobileIconCarrierIdOverrides
@@ -32,14 +33,18 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIc
import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel.DefaultIcon
import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel.OverriddenIcon
import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

@@ -79,6 +84,9 @@ interface MobileIconInteractor {
    /** Whether or not to show the slice attribution */
    val showSliceAttribution: StateFlow<Boolean>

    /** True if this connection is satellite-based */
    val isNonTerrestrial: StateFlow<Boolean>

    /**
     * Provider name for this network connection. The name can be one of 3 values:
     * 1. The default network name, if one is configured
@@ -244,6 +252,13 @@ class MobileIconInteractorImpl(
    override val showSliceAttribution: StateFlow<Boolean> =
        connectionRepository.hasPrioritizedNetworkCapabilities

    override val isNonTerrestrial: StateFlow<Boolean> =
        if (Flags.carrierEnabledSatelliteFlag()) {
            connectionRepository.isNonTerrestrial
        } else {
            MutableStateFlow(false).asStateFlow()
        }

    override val isRoaming: StateFlow<Boolean> =
        combine(
                connectionRepository.carrierNetworkChangeActive,
@@ -313,27 +328,46 @@ class MobileIconInteractorImpl(
            }
            .stateIn(scope, SharingStarted.WhileSubscribed(), 0)

    override val signalLevelIcon: StateFlow<SignalIconModel> = run {
        val initial =
            SignalIconModel(
                level = shownLevel.value,
                numberOfLevels = numberOfLevels.value,
                showExclamationMark = showExclamationMark.value,
                carrierNetworkChange = carrierNetworkChangeActive.value,
            )
    private val cellularIcon: Flow<SignalIconModel.Cellular> =
        combine(
            shownLevel,
            numberOfLevels,
            showExclamationMark,
            carrierNetworkChangeActive,
        ) { shownLevel, numberOfLevels, showExclamationMark, carrierNetworkChange ->
                SignalIconModel(
            SignalIconModel.Cellular(
                shownLevel,
                numberOfLevels,
                showExclamationMark,
                carrierNetworkChange,
            )
        }

    private val satelliteIcon: Flow<SignalIconModel.Satellite> =
        shownLevel.map {
            SignalIconModel.Satellite(
                level = it,
                icon = SatelliteIconModel.fromSignalStrength(it)
                        ?: SatelliteIconModel.fromSignalStrength(0)!!
            )
        }

    override val signalLevelIcon: StateFlow<SignalIconModel> = run {
        val initial =
            SignalIconModel.Cellular(
                shownLevel.value,
                numberOfLevels.value,
                showExclamationMark.value,
                carrierNetworkChangeActive.value,
            )
        isNonTerrestrial
            .flatMapLatest { ntn ->
                if (ntn) {
                    satelliteIcon
                } else {
                    cellularIcon
                }
            }
            .distinctUntilChanged()
            .logDiffsForTable(
                tableLogBuffer,
+70 −27
Original line number Diff line number Diff line
@@ -17,18 +17,34 @@
package com.android.systemui.statusbar.pipeline.mobile.domain.model

import com.android.settingslib.graph.SignalDrawable
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.log.table.Diffable
import com.android.systemui.log.table.TableRowLogger

sealed interface SignalIconModel : Diffable<SignalIconModel> {
    val level: Int

    override fun logDiffs(prevVal: SignalIconModel, row: TableRowLogger) {
        logPartial(prevVal, row)
    }

    override fun logFull(row: TableRowLogger) = logFully(row)

    fun logFully(row: TableRowLogger)

    fun logPartial(prevVal: SignalIconModel, row: TableRowLogger)

    /** A model that will be consumed by [SignalDrawable] to show the mobile triangle icon. */
data class SignalIconModel(
    val level: Int,
    data class Cellular(
        override val level: Int,
        val numberOfLevels: Int,
        val showExclamationMark: Boolean,
        val carrierNetworkChange: Boolean,
) : Diffable<SignalIconModel> {
    // TODO(b/267767715): Can we implement [logDiffs] and [logFull] generically for data classes?
    override fun logDiffs(prevVal: SignalIconModel, row: TableRowLogger) {
    ) : SignalIconModel {
        override fun logPartial(prevVal: SignalIconModel, row: TableRowLogger) {
            if (prevVal !is Cellular) {
                logFull(row)
            } else {
                if (prevVal.level != level) {
                    row.logChange(COL_LEVEL, level)
                }
@@ -42,8 +58,10 @@ data class SignalIconModel(
                    row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChange)
                }
            }
        }

    override fun logFull(row: TableRowLogger) {
        override fun logFully(row: TableRowLogger) {
            row.logChange(COL_TYPE, "c")
            row.logChange(COL_LEVEL, level)
            row.logChange(COL_NUM_LEVELS, numberOfLevels)
            row.logChange(COL_SHOW_EXCLAMATION, showExclamationMark)
@@ -57,11 +75,36 @@ data class SignalIconModel(
            } else {
                SignalDrawable.getState(level, numberOfLevels, showExclamationMark)
            }
    }

    /**
     * For non-terrestrial networks, we can use a resource-backed icon instead of the
     * [SignalDrawable]-backed version above
     */
    data class Satellite(
        override val level: Int,
        val icon: Icon.Resource,
    ) : SignalIconModel {
        override fun logPartial(prevVal: SignalIconModel, row: TableRowLogger) {
            if (prevVal !is Satellite) {
                logFull(row)
            } else {
                if (prevVal.level != level) row.logChange(COL_LEVEL, level)
            }
        }

        override fun logFully(row: TableRowLogger) {
            row.logChange("numLevels", "HELLO")
            row.logChange(COL_TYPE, "s")
            row.logChange(COL_LEVEL, level)
        }
    }

    companion object {
        private const val COL_LEVEL = "level"
        private const val COL_NUM_LEVELS = "numLevels"
        private const val COL_SHOW_EXCLAMATION = "showExclamation"
        private const val COL_CARRIER_NETWORK_CHANGE = "carrierNetworkChange"
        private const val COL_TYPE = "type"
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -59,7 +59,7 @@ constructor(
                str1 = parentView.getIdForLogging()
                int1 = subId
                int2 = icon.level
                bool1 = icon.showExclamationMark
                bool1 = if (icon is SignalIconModel.Cellular) icon.showExclamationMark else false
            },
            {
                "Binder[subId=$int1, viewId=$str1] received new signal icon: " +
+8 −2
Original line number Diff line number Diff line
@@ -38,6 +38,7 @@ import com.android.systemui.plugins.DarkIconDispatcher
import com.android.systemui.res.R
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
@@ -70,7 +71,7 @@ object MobileIconBinder {
        val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type)
        val networkTypeContainer = view.requireViewById<FrameLayout>(R.id.mobile_type_container)
        val iconView = view.requireViewById<ImageView>(R.id.mobile_signal)
        val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) }
        val mobileDrawable = SignalDrawable(view.context)
        val roamingView = view.requireViewById<ImageView>(R.id.mobile_roaming)
        val roamingSpace = view.requireViewById<Space>(R.id.mobile_roaming_space)
        val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot)
@@ -138,7 +139,12 @@ object MobileIconBinder {
                                viewModel.subscriptionId,
                                icon,
                            )
                            if (icon is SignalIconModel.Cellular) {
                                iconView.setImageDrawable(mobileDrawable)
                                mobileDrawable.level = icon.toSignalDrawableState()
                            } else if (icon is SignalIconModel.Satellite) {
                                IconViewBinder.bind(icon.icon, iconView)
                            }
                        }
                    }

+97 −1
Original line number Diff line number Diff line
@@ -33,12 +33,15 @@ import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityMod
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn

/** Common interface for all of the location-based mobile icon view models. */
@@ -71,7 +74,6 @@ interface MobileIconViewModelCommon {
 * model gets the exact same information, as well as allows us to log that unified state only once
 * per icon.
 */
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
class MobileIconViewModel(
    override val subscriptionId: Int,
@@ -80,6 +82,100 @@ class MobileIconViewModel(
    constants: ConnectivityConstants,
    flags: FeatureFlagsClassic,
    scope: CoroutineScope,
) : MobileIconViewModelCommon {
    private val cellProvider by lazy {
        CellularIconViewModel(
            subscriptionId,
            iconInteractor,
            airplaneModeInteractor,
            constants,
            flags,
            scope,
        )
    }

    private val satelliteProvider by lazy {
        CarrierBasedSatelliteViewModelImpl(
            subscriptionId,
            iconInteractor,
        )
    }

    /**
     * Similar to repository switching, this allows us to split up the logic of satellite/cellular
     * states, since they are different by nature
     */
    private val vmProvider: Flow<MobileIconViewModelCommon> =
        iconInteractor.isNonTerrestrial
            .mapLatest { nonTerrestrial ->
                if (nonTerrestrial) {
                    satelliteProvider
                } else {
                    cellProvider
                }
            }
            .stateIn(scope, SharingStarted.WhileSubscribed(), cellProvider)

    override val isVisible: StateFlow<Boolean> =
        vmProvider
            .flatMapLatest { it.isVisible }
            .stateIn(scope, SharingStarted.WhileSubscribed(), false)

    override val icon: Flow<SignalIconModel> = vmProvider.flatMapLatest { it.icon }

    override val contentDescription: Flow<ContentDescription> =
        vmProvider.flatMapLatest { it.contentDescription }

    override val roaming: Flow<Boolean> = vmProvider.flatMapLatest { it.roaming }

    override val networkTypeIcon: Flow<Icon.Resource?> =
        vmProvider.flatMapLatest { it.networkTypeIcon }

    override val networkTypeBackground: StateFlow<Icon.Resource?> =
        vmProvider
            .flatMapLatest { it.networkTypeBackground }
            .stateIn(scope, SharingStarted.WhileSubscribed(), null)

    override val activityInVisible: Flow<Boolean> =
        vmProvider.flatMapLatest { it.activityInVisible }

    override val activityOutVisible: Flow<Boolean> =
        vmProvider.flatMapLatest { it.activityOutVisible }

    override val activityContainerVisible: Flow<Boolean> =
        vmProvider.flatMapLatest { it.activityContainerVisible }
}

/** Representation of this network when it is non-terrestrial (e.g., satellite) */
private class CarrierBasedSatelliteViewModelImpl(
    override val subscriptionId: Int,
    interactor: MobileIconInteractor,
) : MobileIconViewModelCommon {
    override val isVisible: StateFlow<Boolean> = MutableStateFlow(true)
    override val icon: Flow<SignalIconModel> = interactor.signalLevelIcon

    override val contentDescription: Flow<ContentDescription> =
        MutableStateFlow(ContentDescription.Loaded(""))

    /** These fields are not used for satellite icons currently */
    override val roaming: Flow<Boolean> = flowOf(false)
    override val networkTypeIcon: Flow<Icon.Resource?> = flowOf(null)
    override val networkTypeBackground: StateFlow<Icon.Resource?> = MutableStateFlow(null)
    override val activityInVisible: Flow<Boolean> = flowOf(false)
    override val activityOutVisible: Flow<Boolean> = flowOf(false)
    override val activityContainerVisible: Flow<Boolean> = flowOf(false)
}

/** Terrestrial (cellular) icon. */
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
private class CellularIconViewModel(
    override val subscriptionId: Int,
    iconInteractor: MobileIconInteractor,
    airplaneModeInteractor: AirplaneModeInteractor,
    constants: ConnectivityConstants,
    flags: FeatureFlagsClassic,
    scope: CoroutineScope,
) : MobileIconViewModelCommon {
    override val isVisible: StateFlow<Boolean> =
        if (!constants.hasDataCapabilities) {
Loading