Merge changes I5986ba47,I8ddeafe4,I385e2050,I1f8b2b4e,Iab75c65a, ... into tm-qpr-dev

* changes:
  [SB Refactor] Migrate mobile's `state.visible` to the new pipeline.
  [SB Refactor] Add better logging for the mobile icon ID.
  [SB Refactor] Add better logging for the network type icon.
  [SB Refactor] Add a table logger for the parent mobile classes.
  [Table logging] Support nullable ints and add more extension functions.
  [SB Refactor] Add logs for the carrier merged connection.
This commit is contained in:
Caitlin Shkuratov
2023-02-08 14:45:06 +00:00
committed by Android (Google) Code Review
32 changed files with 1270 additions and 157 deletions

View File

@@ -79,10 +79,10 @@ fun <T : Diffable<T>> Flow<T>.logDiffsForTable(
}
}
/**
* Each time the boolean flow is updated with a new value that's different from the previous value,
* logs the new value to the given [tableLogBuffer].
*/
// Here and below: Various Flow<SomeType> extension functions that are effectively equivalent to the
// above [logDiffsForTable] method.
/** See [logDiffsForTable(TableLogBuffer, String, T)]. */
fun Flow<Boolean>.logDiffsForTable(
tableLogBuffer: TableLogBuffer,
columnPrefix: String,
@@ -100,10 +100,8 @@ fun Flow<Boolean>.logDiffsForTable(
newVal
}
}
/**
* Each time the Int flow is updated with a new value that's different from the previous value, logs
* the new value to the given [tableLogBuffer].
*/
/** See [logDiffsForTable(TableLogBuffer, String, T)]. */
fun Flow<Int>.logDiffsForTable(
tableLogBuffer: TableLogBuffer,
columnPrefix: String,
@@ -122,10 +120,26 @@ fun Flow<Int>.logDiffsForTable(
}
}
/**
* Each time the String? flow is updated with a new value that's different from the previous value,
* logs the new value to the given [tableLogBuffer].
*/
/** See [logDiffsForTable(TableLogBuffer, String, T)]. */
fun Flow<Int?>.logDiffsForTable(
tableLogBuffer: TableLogBuffer,
columnPrefix: String,
columnName: String,
initialValue: Int?,
): Flow<Int?> {
val initialValueFun = {
tableLogBuffer.logChange(columnPrefix, columnName, initialValue)
initialValue
}
return this.pairwiseBy(initialValueFun) { prevVal, newVal: Int? ->
if (prevVal != newVal) {
tableLogBuffer.logChange(columnPrefix, columnName, newVal)
}
newVal
}
}
/** See [logDiffsForTable(TableLogBuffer, String, T)]. */
fun Flow<String?>.logDiffsForTable(
tableLogBuffer: TableLogBuffer,
columnPrefix: String,
@@ -143,3 +157,23 @@ fun Flow<String?>.logDiffsForTable(
newVal
}
}
/** See [logDiffsForTable(TableLogBuffer, String, T)]. */
fun <T> Flow<List<T>>.logDiffsForTable(
tableLogBuffer: TableLogBuffer,
columnPrefix: String,
columnName: String,
initialValue: List<T>,
): Flow<List<T>> {
val initialValueFun = {
tableLogBuffer.logChange(columnPrefix, columnName, initialValue.toString())
initialValue
}
return this.pairwiseBy(initialValueFun) { prevVal, newVal: List<T> ->
if (prevVal != newVal) {
// TODO(b/267761156): Can we log list changes without using toString?
tableLogBuffer.logChange(columnPrefix, columnName, newVal.toString())
}
newVal
}
}

View File

@@ -27,7 +27,7 @@ data class TableChange(
var columnName: String = "",
var type: DataType = DataType.EMPTY,
var bool: Boolean = false,
var int: Int = 0,
var int: Int? = null,
var str: String? = null,
) {
/** Resets to default values so that the object can be recycled. */
@@ -54,7 +54,7 @@ data class TableChange(
}
/** Sets this to store an int change. */
fun set(value: Int) {
fun set(value: Int?) {
type = DataType.INT
int = value
}

View File

@@ -138,7 +138,7 @@ class TableLogBuffer(
}
/** Logs a Int change. */
fun logChange(prefix: String, columnName: String, value: Int) {
fun logChange(prefix: String, columnName: String, value: Int?) {
logChange(systemClock.currentTimeMillis(), prefix, columnName, value)
}
@@ -155,7 +155,7 @@ class TableLogBuffer(
change.set(value)
}
private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Int) {
private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Int?) {
val change = obtain(timestamp, prefix, columnName)
change.set(value)
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2023 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.systemui.statusbar.pipeline.dagger
import javax.inject.Qualifier
/**
* Logs for mobile data that's **the same across all connections**.
*
* This buffer should only be used for the mobile parent classes like [MobileConnectionsRepository]
* and [MobileIconsInteractor]. It should *not* be used for classes that represent an individual
* connection, like [MobileConnectionRepository] or [MobileIconInteractor].
*/
@Qualifier
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class MobileSummaryLog

View File

@@ -118,5 +118,12 @@ abstract class StatusBarPipelineModule {
fun provideAirplaneTableLogBuffer(factory: TableLogBufferFactory): TableLogBuffer {
return factory.create("AirplaneTableLog", 30)
}
@Provides
@SysUISingleton
@MobileSummaryLog
fun provideMobileSummaryLogBuffer(factory: TableLogBufferFactory): TableLogBuffer {
return factory.create("MobileSummaryLog", 100)
}
}
}

View File

@@ -17,6 +17,8 @@
package com.android.systemui.statusbar.pipeline.mobile.data.model
import android.net.NetworkCapabilities
import com.android.systemui.log.table.Diffable
import com.android.systemui.log.table.TableRowLogger
/** Provides information about a mobile network connection */
data class MobileConnectivityModel(
@@ -24,4 +26,24 @@ data class MobileConnectivityModel(
val isConnected: Boolean = false,
/** Whether the mobile transport is validated [NetworkCapabilities.NET_CAPABILITY_VALIDATED] */
val isValidated: Boolean = false,
)
) : Diffable<MobileConnectivityModel> {
// TODO(b/267767715): Can we implement [logDiffs] and [logFull] generically for data classes?
override fun logDiffs(prevVal: MobileConnectivityModel, row: TableRowLogger) {
if (prevVal.isConnected != isConnected) {
row.logChange(COL_IS_CONNECTED, isConnected)
}
if (prevVal.isValidated != isValidated) {
row.logChange(COL_IS_VALIDATED, isValidated)
}
}
override fun logFull(row: TableRowLogger) {
row.logChange(COL_IS_CONNECTED, isConnected)
row.logChange(COL_IS_VALIDATED, isValidated)
}
companion object {
private const val COL_IS_CONNECTED = "isConnected"
private const val COL_IS_VALIDATED = "isValidated"
}
}

View File

@@ -91,9 +91,6 @@ class CarrierMergedConnectionRepository(
.map { it.toMobileConnectionModel() }
.stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectionModel())
// TODO(b/238425913): Add logging to this class.
// TODO(b/238425913): Make sure SignalStrength.getEmptyState is used when appropriate.
// Carrier merged is never roaming.
override val cdmaRoaming: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()

View File

@@ -117,11 +117,22 @@ class FullMobileConnectionRepository(
override val connectionInfo =
activeRepo
.flatMapLatest { it.connectionInfo }
.logDiffsForTable(
tableLogBuffer,
columnPrefix = "",
initialValue = activeRepo.value.connectionInfo.value,
)
.stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.connectionInfo.value)
override val dataEnabled =
activeRepo
.flatMapLatest { it.dataEnabled }
.logDiffsForTable(
tableLogBuffer,
columnPrefix = "",
columnName = "dataEnabled",
initialValue = activeRepo.value.dataEnabled.value,
)
.stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.dataEnabled.value)
override val numberOfLevels =
@@ -132,6 +143,11 @@ class FullMobileConnectionRepository(
override val networkName =
activeRepo
.flatMapLatest { it.networkName }
.logDiffsForTable(
tableLogBuffer,
columnPrefix = "",
initialValue = activeRepo.value.networkName.value,
)
.stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.networkName.value)
class Factory
@@ -168,7 +184,7 @@ class FullMobileConnectionRepository(
const val MOBILE_CONNECTION_BUFFER_SIZE = 100
/** Returns a log buffer name for a mobile connection with the given [subId]. */
fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]"
fun tableBufferLogName(subId: Int): String = "MobileConnectionLog[$subId]"
}
}
}

View File

@@ -36,7 +36,6 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
@@ -62,7 +61,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.mapNotNull
@@ -87,7 +85,7 @@ class MobileConnectionRepositoryImpl(
private val mobileMappingsProxy: MobileMappingsProxy,
bgDispatcher: CoroutineDispatcher,
logger: ConnectivityPipelineLogger,
mobileLogger: TableLogBuffer,
override val tableLogBuffer: TableLogBuffer,
scope: CoroutineScope,
) : MobileConnectionRepository {
init {
@@ -101,8 +99,6 @@ class MobileConnectionRepositoryImpl(
private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
override val tableLogBuffer: TableLogBuffer = mobileLogger
/**
* This flow defines the single shared connection to system_server via TelephonyCallback. Any
* new callback should be added to this listener and funneled through callbackEvents via a data
@@ -243,11 +239,6 @@ class MobileConnectionRepositoryImpl(
val initial = MobileConnectionModel()
callbackEvents
.scan(initial, ::updateConnectionState)
.logDiffsForTable(
mobileLogger,
columnPrefix = "MobileConnection ($subId)",
initialValue = initial,
)
.stateIn(scope, SharingStarted.WhileSubscribed(), initial)
}
@@ -285,24 +276,12 @@ class MobileConnectionRepositoryImpl(
intent.toNetworkNameModel(networkNameSeparator) ?: defaultNetworkName
}
}
.distinctUntilChanged()
.logDiffsForTable(
mobileLogger,
columnPrefix = "",
initialValue = defaultNetworkName,
)
.stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName)
override val dataEnabled = run {
val initial = telephonyManager.isDataConnectionAllowed
callbackEvents
.mapNotNull { (it as? CallbackEvent.OnDataEnabledChanged)?.enabled }
.logDiffsForTable(
mobileLogger,
columnPrefix = "",
columnName = "dataEnabled",
initialValue = initial
)
.stateIn(scope, SharingStarted.WhileSubscribed(), initial)
}

View File

@@ -42,6 +42,9 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
@@ -82,6 +85,7 @@ constructor(
private val subscriptionManager: SubscriptionManager,
private val telephonyManager: TelephonyManager,
private val logger: ConnectivityPipelineLogger,
@MobileSummaryLog private val tableLogger: TableLogBuffer,
mobileMappingsProxy: MobileMappingsProxy,
broadcastDispatcher: BroadcastDispatcher,
private val context: Context,
@@ -114,6 +118,12 @@ constructor(
}
}
.distinctUntilChanged()
.logDiffsForTable(
tableLogger,
LOGGING_PREFIX,
columnName = "carrierMergedSubId",
initialValue = null,
)
.stateIn(scope, started = SharingStarted.WhileSubscribed(), null)
private val mobileSubscriptionsChangeEvent: Flow<Unit> = conflatedCallbackFlow {
@@ -139,8 +149,14 @@ constructor(
override val subscriptions: StateFlow<List<SubscriptionModel>> =
merge(mobileSubscriptionsChangeEvent, carrierMergedSubId)
.mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } }
.logInputChange(logger, "onSubscriptionsChanged")
.onEach { infos -> updateRepos(infos) }
.distinctUntilChanged()
.logDiffsForTable(
tableLogger,
LOGGING_PREFIX,
columnName = "subscriptions",
initialValue = listOf(),
)
.stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
/** StateFlow that keeps track of the current active mobile data subscription */
@@ -157,7 +173,12 @@ constructor(
awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
}
.distinctUntilChanged()
.logInputChange(logger, "onActiveDataSubscriptionIdChanged")
.logDiffsForTable(
tableLogger,
LOGGING_PREFIX,
columnName = "activeSubId",
initialValue = INVALID_SUBSCRIPTION_ID,
)
.stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)
private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
@@ -171,7 +192,12 @@ constructor(
intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
}
.distinctUntilChanged()
.logInputChange(logger, "ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED")
.logDiffsForTable(
tableLogger,
LOGGING_PREFIX,
columnName = "defaultSubId",
initialValue = SubscriptionManager.getDefaultDataSubscriptionId(),
)
.onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
.stateIn(
scope,
@@ -247,7 +273,11 @@ constructor(
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
.distinctUntilChanged()
.logInputChange(logger, "defaultMobileNetworkConnectivity")
.logDiffsForTable(
tableLogger,
columnPrefix = "$LOGGING_PREFIX.defaultConnection",
initialValue = MobileConnectivityModel(),
)
.stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())
/**
@@ -321,4 +351,8 @@ constructor(
subscriptionId = subscriptionId,
isOpportunistic = isOpportunistic,
)
companion object {
private const val LOGGING_PREFIX = "Repo"
}
}

View File

@@ -33,7 +33,9 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
interface MobileIconInteractor {
@@ -109,6 +111,9 @@ interface MobileIconInteractor {
/** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */
val numberOfLevels: StateFlow<Int>
/** See [MobileIconsInteractor.isForceHidden]. */
val isForceHidden: Flow<Boolean>
}
/** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
@@ -124,6 +129,7 @@ class MobileIconInteractorImpl(
defaultMobileIconGroup: StateFlow<MobileIconGroup>,
defaultDataSubId: StateFlow<Int>,
override val isDefaultConnectionFailed: StateFlow<Boolean>,
override val isForceHidden: Flow<Boolean>,
connectionRepository: MobileConnectionRepository,
) : MobileIconInteractor {
private val connectionInfo = connectionRepository.connectionInfo
@@ -181,6 +187,16 @@ class MobileIconInteractorImpl(
else -> mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup
}
}
.distinctUntilChanged()
.onEach {
// Doesn't use [logDiffsForTable] because [MobileIconGroup] can't implement the
// [Diffable] interface.
tableLogBuffer.logChange(
prefix = "",
columnName = "networkTypeIcon",
value = it.name
)
}
.stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value)
override val isEmergencyOnly: StateFlow<Boolean> =

View File

@@ -23,12 +23,17 @@ import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
import com.android.systemui.util.CarrierConfigTracker
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -42,8 +47,8 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
@@ -88,6 +93,10 @@ interface MobileIconsInteractor {
val isDefaultConnectionFailed: StateFlow<Boolean>
/** True once the user has been set up */
val isUserSetup: StateFlow<Boolean>
/** True if we're configured to force-hide the mobile icons and false otherwise. */
val isForceHidden: Flow<Boolean>
/**
* Vends out a [MobileIconInteractor] tracking the [MobileConnectionRepository] for the given
* subId. Will throw if the ID is invalid
@@ -104,6 +113,8 @@ constructor(
private val mobileConnectionsRepo: MobileConnectionsRepository,
private val carrierConfigTracker: CarrierConfigTracker,
private val logger: ConnectivityPipelineLogger,
@MobileSummaryLog private val tableLogger: TableLogBuffer,
connectivityRepository: ConnectivityRepository,
userSetupRepo: UserSetupRepository,
@Application private val scope: CoroutineScope,
) : MobileIconsInteractor {
@@ -173,7 +184,13 @@ constructor(
}
}
.distinctUntilChanged()
.onEach { logger.logFilteredSubscriptionsChanged(it) }
.logDiffsForTable(
tableLogger,
LOGGING_PREFIX,
columnName = "filteredSubscriptions",
initialValue = listOf(),
)
.stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
override val defaultDataSubId = mobileConnectionsRepo.defaultDataSubId
@@ -195,6 +212,12 @@ constructor(
delay(2000)
emit(false)
}
.logDiffsForTable(
tableLogger,
LOGGING_PREFIX,
columnName = "forcingValidation",
initialValue = false,
)
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
@@ -211,6 +234,12 @@ constructor(
networkConnectivity
}
}
.distinctUntilChanged()
.logDiffsForTable(
tableLogger,
columnPrefix = "$LOGGING_PREFIX.defaultConnection",
initialValue = mobileConnectionsRepo.defaultMobileNetworkConnectivity.value,
)
.stateIn(
scope,
SharingStarted.WhileSubscribed(),
@@ -259,10 +288,21 @@ constructor(
!connectivityModel.isValidated
}
}
.logDiffsForTable(
tableLogger,
LOGGING_PREFIX,
columnName = "isDefaultConnectionFailed",
initialValue = false,
)
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val isUserSetup: StateFlow<Boolean> = userSetupRepo.isUserSetupFlow
override val isForceHidden: Flow<Boolean> =
connectivityRepository.forceHiddenSlots
.map { it.contains(ConnectivitySlot.MOBILE) }
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
/** Vends out new [MobileIconInteractor] for a particular subId */
override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
MobileIconInteractorImpl(
@@ -275,6 +315,11 @@ constructor(
defaultMobileIconGroup,
defaultDataSubId,
isDefaultConnectionFailed,
isForceHidden,
mobileConnectionsRepo.getRepoForSubId(subId),
)
companion object {
private const val LOGGING_PREFIX = "Intr"
}
}

View File

@@ -91,10 +91,17 @@ object MobileIconBinder {
}
}
launch { viewModel.isVisible.collect { isVisible -> view.isVisible = isVisible } }
// Set the icon for the triangle
launch {
viewModel.iconId.distinctUntilChanged().collect { iconId ->
mobileDrawable.level = iconId
viewModel.icon.distinctUntilChanged().collect { icon ->
mobileDrawable.level =
SignalDrawable.getState(
icon.level,
icon.numberOfLevels,
icon.showExclamationMark,
)
}
}
@@ -148,8 +155,7 @@ object MobileIconBinder {
return object : ModernStatusBarViewBinding {
override fun getShouldIconBeVisible(): Boolean {
// If this view model exists, then the icon should be visible.
return true
return viewModel.isVisible.value
}
override fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) {

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2023 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.systemui.statusbar.pipeline.mobile.ui.model
import com.android.systemui.log.table.Diffable
import com.android.systemui.log.table.TableRowLogger
/** A model that will be consumed by [SignalDrawable] to show the mobile triangle icon. */
data class SignalIconModel(
val level: Int,
val numberOfLevels: Int,
val showExclamationMark: Boolean,
) : Diffable<SignalIconModel> {
// TODO(b/267767715): Can we implement [logDiffs] and [logFull] generically for data classes?
override fun logDiffs(prevVal: SignalIconModel, row: TableRowLogger) {
if (prevVal.level != level) {
row.logChange(COL_LEVEL, level)
}
if (prevVal.numberOfLevels != numberOfLevels) {
row.logChange(COL_NUM_LEVELS, numberOfLevels)
}
if (prevVal.showExclamationMark != showExclamationMark) {
row.logChange(COL_SHOW_EXCLAMATION, showExclamationMark)
}
}
override fun logFull(row: TableRowLogger) {
row.logChange(COL_LEVEL, level)
row.logChange(COL_NUM_LEVELS, numberOfLevels)
row.logChange(COL_SHOW_EXCLAMATION, showExclamationMark)
}
companion object {
/** Creates a [SignalIconModel] representing an empty and invalidated state. */
fun createEmptyState(numberOfLevels: Int) =
SignalIconModel(level = 0, numberOfLevels, showExclamationMark = true)
private const val COL_LEVEL = "level"
private const val COL_NUM_LEVELS = "numLevels"
private const val COL_SHOW_EXCLAMATION = "showExclamation"
}
}

View File

@@ -22,10 +22,11 @@ import com.android.settingslib.graph.SignalDrawable
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.model.SignalIconModel
import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -37,14 +38,14 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
/** Common interface for all of the location-based mobile icon view models. */
interface MobileIconViewModelCommon {
val subscriptionId: Int
/** An int consumable by [SignalDrawable] for display */
val iconId: Flow<Int>
/** True if this view should be visible at all. */
val isVisible: StateFlow<Boolean>
val icon: Flow<SignalIconModel>
val contentDescription: Flow<ContentDescription>
val roaming: Flow<Boolean>
/** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
@@ -73,7 +74,7 @@ class MobileIconViewModel
constructor(
override val subscriptionId: Int,
iconInteractor: MobileIconInteractor,
logger: ConnectivityPipelineLogger,
airplaneModeInteractor: AirplaneModeInteractor,
constants: ConnectivityConstants,
scope: CoroutineScope,
) : MobileIconViewModelCommon {
@@ -81,8 +82,28 @@ constructor(
private val showExclamationMark: Flow<Boolean> =
iconInteractor.isDefaultDataEnabled.mapLatest { !it }
override val iconId: Flow<Int> = run {
val initial = SignalDrawable.getEmptyState(iconInteractor.numberOfLevels.value)
override val isVisible: StateFlow<Boolean> =
if (!constants.hasDataCapabilities) {
flowOf(false)
} else {
combine(
airplaneModeInteractor.isAirplaneMode,
iconInteractor.isForceHidden,
) { isAirplaneMode, isForceHidden ->
!isAirplaneMode && !isForceHidden
}
}
.distinctUntilChanged()
.logDiffsForTable(
iconInteractor.tableLogBuffer,
columnPrefix = "",
columnName = "visible",
initialValue = false,
)
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val icon: Flow<SignalIconModel> = run {
val initial = SignalIconModel.createEmptyState(iconInteractor.numberOfLevels.value)
combine(
iconInteractor.level,
iconInteractor.numberOfLevels,
@@ -90,16 +111,15 @@ constructor(
iconInteractor.isInService,
) { level, numberOfLevels, showExclamationMark, isInService ->
if (!isInService) {
SignalDrawable.getEmptyState(numberOfLevels)
SignalIconModel.createEmptyState(numberOfLevels)
} else {
SignalDrawable.getState(level, numberOfLevels, showExclamationMark)
SignalIconModel(level, numberOfLevels, showExclamationMark)
}
}
.distinctUntilChanged()
.logDiffsForTable(
iconInteractor.tableLogBuffer,
columnPrefix = "",
columnName = "iconId",
columnPrefix = "icon",
initialValue = initial,
)
.stateIn(scope, SharingStarted.WhileSubscribed(), initial)
@@ -124,14 +144,22 @@ constructor(
private val showNetworkTypeIcon: Flow<Boolean> =
combine(
iconInteractor.isDataConnected,
iconInteractor.isDataEnabled,
iconInteractor.isDefaultConnectionFailed,
iconInteractor.alwaysShowDataRatIcon,
iconInteractor.isConnected,
) { dataConnected, dataEnabled, failedConnection, alwaysShow, connected ->
alwaysShow || (dataConnected && dataEnabled && !failedConnection && connected)
}
iconInteractor.isDataConnected,
iconInteractor.isDataEnabled,
iconInteractor.isDefaultConnectionFailed,
iconInteractor.alwaysShowDataRatIcon,
iconInteractor.isConnected,
) { dataConnected, dataEnabled, failedConnection, alwaysShow, connected ->
alwaysShow || (dataConnected && dataEnabled && !failedConnection && connected)
}
.distinctUntilChanged()
.logDiffsForTable(
iconInteractor.tableLogBuffer,
columnPrefix = "",
columnName = "showNetworkTypeIcon",
initialValue = false,
)
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val networkTypeIcon: Flow<Icon?> =
combine(
@@ -149,14 +177,6 @@ constructor(
}
}
.distinctUntilChanged()
.onEach {
// This is done as an onEach side effect since Icon is not Diffable (yet)
iconInteractor.tableLogBuffer.logChange(
prefix = "",
columnName = "networkTypeIcon",
value = it.toString(),
)
}
.stateIn(scope, SharingStarted.WhileSubscribed(), null)
override val roaming: StateFlow<Boolean> =

View File

@@ -17,9 +17,11 @@
package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
import androidx.annotation.VisibleForTesting
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView
import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
@@ -39,6 +41,7 @@ class MobileIconsViewModel
constructor(
val subscriptionIdsFlow: StateFlow<List<Int>>,
private val interactor: MobileIconsInteractor,
private val airplaneModeInteractor: AirplaneModeInteractor,
private val logger: ConnectivityPipelineLogger,
private val constants: ConnectivityConstants,
@Application private val scope: CoroutineScope,
@@ -56,7 +59,7 @@ constructor(
?: MobileIconViewModel(
subId,
interactor.createMobileConnectionInteractorForSubId(subId),
logger,
airplaneModeInteractor,
constants,
scope,
)
@@ -74,10 +77,12 @@ constructor(
subIdsToRemove.forEach { mobileIconSubIdCache.remove(it) }
}
@SysUISingleton
class Factory
@Inject
constructor(
private val interactor: MobileIconsInteractor,
private val airplaneModeInteractor: AirplaneModeInteractor,
private val logger: ConnectivityPipelineLogger,
private val constants: ConnectivityConstants,
@Application private val scope: CoroutineScope,
@@ -87,6 +92,7 @@ constructor(
return MobileIconsViewModel(
subscriptionIdsFlow,
interactor,
airplaneModeInteractor,
logger,
constants,
scope,

View File

@@ -204,15 +204,6 @@ constructor(
// TODO(b/238425913): We should split this class into mobile-specific and wifi-specific loggers.
fun logFilteredSubscriptionsChanged(subs: List<SubscriptionModel>) {
buffer.log(
SB_LOGGING_TAG,
LogLevel.INFO,
{ str1 = subs.toString() },
{ "Filtered subscriptions updated: $str1" },
)
}
fun logUiAdapterSubIdsUpdated(subs: List<Int>) {
buffer.log(
SB_LOGGING_TAG,

View File

@@ -275,6 +275,52 @@ class LogDiffsForTableTest : SysuiTestCase() {
job.cancel()
}
@Test
fun intNullable_logsNull() =
testScope.runTest {
systemClock.setCurrentTimeMillis(100L)
val flow = flow {
for (int in listOf(null, 6, null, 8)) {
systemClock.advanceTime(100L)
emit(int)
}
}
val flowWithLogging =
flow.logDiffsForTable(
tableLogBuffer,
COLUMN_PREFIX,
COLUMN_NAME,
initialValue = 1234,
)
val job = launch { flowWithLogging.collect() }
val logs = dumpLog()
assertThat(logs)
.contains(
TABLE_LOG_DATE_FORMAT.format(100L) + SEPARATOR + FULL_NAME + SEPARATOR + "1234"
)
assertThat(logs)
.contains(
TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "null"
)
assertThat(logs)
.contains(
TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "6"
)
assertThat(logs)
.contains(
TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "null"
)
assertThat(logs)
.contains(
TABLE_LOG_DATE_FORMAT.format(500L) + SEPARATOR + FULL_NAME + SEPARATOR + "8"
)
job.cancel()
}
@Test
fun int_logsUpdates() =
testScope.runTest {
@@ -1030,6 +1076,246 @@ class LogDiffsForTableTest : SysuiTestCase() {
job.cancel()
}
// ---- Flow<List<T>> tests ----
@Test
fun list_doesNotLogWhenNotCollected() {
val flow = flowOf(listOf(5), listOf(6), listOf(7))
flow.logDiffsForTable(
tableLogBuffer,
COLUMN_PREFIX,
COLUMN_NAME,
initialValue = listOf(1234),
)
val logs = dumpLog()
assertThat(logs).doesNotContain(COLUMN_PREFIX)
assertThat(logs).doesNotContain(COLUMN_NAME)
assertThat(logs).doesNotContain("1234")
}
@Test
fun list_logsInitialWhenCollected() =
testScope.runTest {
val flow = flowOf(listOf(5), listOf(6), listOf(7))
val flowWithLogging =
flow.logDiffsForTable(
tableLogBuffer,
COLUMN_PREFIX,
COLUMN_NAME,
initialValue = listOf(1234),
)
systemClock.setCurrentTimeMillis(3000L)
val job = launch { flowWithLogging.collect() }
val logs = dumpLog()
assertThat(logs)
.contains(
TABLE_LOG_DATE_FORMAT.format(3000L) +
SEPARATOR +
FULL_NAME +
SEPARATOR +
listOf(1234).toString()
)
job.cancel()
}
@Test
fun list_logsUpdates() =
testScope.runTest {
systemClock.setCurrentTimeMillis(100L)
val listItems =
listOf(listOf("val1", "val2"), listOf("val3"), listOf("val4", "val5", "val6"))
val flow = flow {
for (list in listItems) {
systemClock.advanceTime(100L)
emit(list)
}
}
val flowWithLogging =
flow.logDiffsForTable(
tableLogBuffer,
COLUMN_PREFIX,
COLUMN_NAME,
initialValue = listOf("val0", "val00"),
)
val job = launch { flowWithLogging.collect() }
val logs = dumpLog()
assertThat(logs)
.contains(
TABLE_LOG_DATE_FORMAT.format(100L) +
SEPARATOR +
FULL_NAME +
SEPARATOR +
listOf("val0", "val00").toString()
)
assertThat(logs)
.contains(
TABLE_LOG_DATE_FORMAT.format(200L) +
SEPARATOR +
FULL_NAME +
SEPARATOR +
listOf("val1", "val2").toString()
)
assertThat(logs)
.contains(
TABLE_LOG_DATE_FORMAT.format(300L) +
SEPARATOR +
FULL_NAME +
SEPARATOR +
listOf("val3").toString()
)
assertThat(logs)
.contains(
TABLE_LOG_DATE_FORMAT.format(400L) +
SEPARATOR +
FULL_NAME +
SEPARATOR +
listOf("val4", "val5", "val6").toString()
)
job.cancel()
}
@Test
fun list_doesNotLogIfSameValue() =
testScope.runTest {
systemClock.setCurrentTimeMillis(100L)
val listItems =
listOf(
listOf("val0", "val00"),
listOf("val1"),
listOf("val1"),
listOf("val1", "val2"),
)
val flow = flow {
for (bool in listItems) {
systemClock.advanceTime(100L)
emit(bool)
}
}
val flowWithLogging =
flow.logDiffsForTable(
tableLogBuffer,
COLUMN_PREFIX,
COLUMN_NAME,
initialValue = listOf("val0", "val00"),
)
val job = launch { flowWithLogging.collect() }
val logs = dumpLog()
val expected1 =
TABLE_LOG_DATE_FORMAT.format(100L) +
SEPARATOR +
FULL_NAME +
SEPARATOR +
listOf("val0", "val00").toString()
val expected3 =
TABLE_LOG_DATE_FORMAT.format(300L) +
SEPARATOR +
FULL_NAME +
SEPARATOR +
listOf("val1").toString()
val expected5 =
TABLE_LOG_DATE_FORMAT.format(500L) +
SEPARATOR +
FULL_NAME +
SEPARATOR +
listOf("val1", "val2").toString()
assertThat(logs).contains(expected1)
assertThat(logs).contains(expected3)
assertThat(logs).contains(expected5)
val unexpected2 =
TABLE_LOG_DATE_FORMAT.format(200L) +
SEPARATOR +
FULL_NAME +
SEPARATOR +
listOf("val0", "val00")
val unexpected4 =
TABLE_LOG_DATE_FORMAT.format(400L) +
SEPARATOR +
FULL_NAME +
SEPARATOR +
listOf("val1")
assertThat(logs).doesNotContain(unexpected2)
assertThat(logs).doesNotContain(unexpected4)
job.cancel()
}
@Test
fun list_worksForStateFlows() =
testScope.runTest {
val flow = MutableStateFlow(listOf(1111))
val flowWithLogging =
flow.logDiffsForTable(
tableLogBuffer,
COLUMN_PREFIX,
COLUMN_NAME,
initialValue = listOf(1111),
)
systemClock.setCurrentTimeMillis(50L)
val job = launch { flowWithLogging.collect() }
assertThat(dumpLog())
.contains(
TABLE_LOG_DATE_FORMAT.format(50L) +
SEPARATOR +
FULL_NAME +
SEPARATOR +
listOf(1111).toString()
)
systemClock.setCurrentTimeMillis(100L)
flow.emit(listOf(2222, 3333))
assertThat(dumpLog())
.contains(
TABLE_LOG_DATE_FORMAT.format(100L) +
SEPARATOR +
FULL_NAME +
SEPARATOR +
listOf(2222, 3333).toString()
)
systemClock.setCurrentTimeMillis(200L)
flow.emit(listOf(3333, 4444))
assertThat(dumpLog())
.contains(
TABLE_LOG_DATE_FORMAT.format(200L) +
SEPARATOR +
FULL_NAME +
SEPARATOR +
listOf(3333, 4444).toString()
)
// Doesn't log duplicates
systemClock.setCurrentTimeMillis(300L)
flow.emit(listOf(3333, 4444))
assertThat(dumpLog())
.doesNotContain(
TABLE_LOG_DATE_FORMAT.format(300L) +
SEPARATOR +
FULL_NAME +
SEPARATOR +
listOf(3333, 4444).toString()
)
job.cancel()
}
private fun dumpLog(): String {
val outputWriter = StringWriter()
tableLogBuffer.dump(PrintWriter(outputWriter), arrayOf())

View File

@@ -35,6 +35,17 @@ class TableChangeTest : SysuiTestCase() {
assertThat(underTest.getVal()).isEqualTo("fakeValue")
}
@Test
fun setString_null() {
val underTest = TableChange()
underTest.reset(timestamp = 100, columnPrefix = "", columnName = "fakeName")
underTest.set(null as String?)
assertThat(underTest.hasData()).isTrue()
assertThat(underTest.getVal()).isEqualTo("null")
}
@Test
fun setBoolean_isBoolean() {
val underTest = TableChange()
@@ -57,6 +68,17 @@ class TableChangeTest : SysuiTestCase() {
assertThat(underTest.getVal()).isEqualTo("8900")
}
@Test
fun setInt_null() {
val underTest = TableChange()
underTest.reset(timestamp = 100, columnPrefix = "", columnName = "fakeName")
underTest.set(null as Int?)
assertThat(underTest.hasData()).isTrue()
assertThat(underTest.getVal()).isEqualTo("null")
}
@Test
fun setThenReset_isEmpty() {
val underTest = TableChange()

View File

@@ -25,6 +25,7 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoModeController
import com.android.systemui.dump.DumpManager
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository
@@ -81,6 +82,7 @@ class MobileRepositorySwitcherTest : SysuiTestCase() {
@Mock private lateinit var subscriptionManager: SubscriptionManager
@Mock private lateinit var telephonyManager: TelephonyManager
@Mock private lateinit var logger: ConnectivityPipelineLogger
@Mock private lateinit var summaryLogger: TableLogBuffer
@Mock private lateinit var demoModeController: DemoModeController
@Mock private lateinit var dumpManager: DumpManager
@@ -114,6 +116,7 @@ class MobileRepositorySwitcherTest : SysuiTestCase() {
subscriptionManager,
telephonyManager,
logger,
summaryLogger,
mobileMappings,
fakeBroadcastDispatcher,
context,

View File

@@ -16,20 +16,32 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
import android.telephony.ServiceState
import android.telephony.SignalStrength
import android.telephony.TelephonyCallback
import android.telephony.TelephonyManager
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_EMERGENCY
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_OPERATOR
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_PRIMARY_LEVEL
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.getTelephonyCallbackForType
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -52,9 +64,10 @@ import org.mockito.Mockito.verify
class FullMobileConnectionRepositoryTest : SysuiTestCase() {
private lateinit var underTest: FullMobileConnectionRepository
private val systemClock = FakeSystemClock()
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
private val tableLogBuffer = mock<TableLogBuffer>()
private val tableLogBuffer = TableLogBuffer(maxSize = 100, name = "TestName", systemClock)
private val mobileFactory = mock<MobileConnectionRepositoryImpl.Factory>()
private val carrierMergedFactory = mock<CarrierMergedConnectionRepository.Factory>()
@@ -347,8 +360,214 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() {
.isSameInstanceAs(connection1Repeat.tableLogBuffer)
}
// TODO(b/238425913): Verify that the logging switches correctly (once the carrier merged repo
// implements logging).
@Test
fun connectionInfo_logging_notCarrierMerged_getsUpdates() =
testScope.runTest {
// SETUP: Use real repositories to verify the diffing still works. (See b/267501739.)
val telephonyManager = mock<TelephonyManager>()
createRealMobileRepo(telephonyManager)
createRealCarrierMergedRepo(FakeWifiRepository())
initializeRepo(startingIsCarrierMerged = false)
val job = underTest.connectionInfo.launchIn(this)
// WHEN we set up some mobile connection info
val serviceState = ServiceState()
serviceState.setOperatorName("longName", "OpTypical", "1")
serviceState.isEmergencyOnly = false
getTelephonyCallbackForType<TelephonyCallback.ServiceStateListener>(telephonyManager)
.onServiceStateChanged(serviceState)
// THEN it's logged to the buffer
assertThat(dumpBuffer()).contains("$COL_OPERATOR${BUFFER_SEPARATOR}OpTypical")
assertThat(dumpBuffer()).contains("$COL_EMERGENCY${BUFFER_SEPARATOR}false")
// WHEN we update mobile connection info
val serviceState2 = ServiceState()
serviceState2.setOperatorName("longName", "OpDiff", "1")
serviceState2.isEmergencyOnly = true
getTelephonyCallbackForType<TelephonyCallback.ServiceStateListener>(telephonyManager)
.onServiceStateChanged(serviceState2)
// THEN the updates are logged
assertThat(dumpBuffer()).contains("$COL_OPERATOR${BUFFER_SEPARATOR}OpDiff")
assertThat(dumpBuffer()).contains("$COL_EMERGENCY${BUFFER_SEPARATOR}true")
job.cancel()
}
@Test
fun connectionInfo_logging_carrierMerged_getsUpdates() =
testScope.runTest {
// SETUP: Use real repositories to verify the diffing still works. (See b/267501739.)
createRealMobileRepo(mock())
val wifiRepository = FakeWifiRepository()
createRealCarrierMergedRepo(wifiRepository)
initializeRepo(startingIsCarrierMerged = true)
val job = underTest.connectionInfo.launchIn(this)
// WHEN we set up carrier merged info
val networkId = 2
wifiRepository.setWifiNetwork(
WifiNetworkModel.CarrierMerged(
networkId,
SUB_ID,
level = 3,
)
)
// THEN the carrier merged info is logged
assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}3")
// WHEN we update the info
wifiRepository.setWifiNetwork(
WifiNetworkModel.CarrierMerged(
networkId,
SUB_ID,
level = 1,
)
)
// THEN the updates are logged
assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}1")
job.cancel()
}
@Test
fun connectionInfo_logging_updatesWhenCarrierMergedUpdates() =
testScope.runTest {
// SETUP: Use real repositories to verify the diffing still works. (See b/267501739.)
val telephonyManager = mock<TelephonyManager>()
createRealMobileRepo(telephonyManager)
val wifiRepository = FakeWifiRepository()
createRealCarrierMergedRepo(wifiRepository)
initializeRepo(startingIsCarrierMerged = false)
val job = underTest.connectionInfo.launchIn(this)
// WHEN we set up some mobile connection info
val signalStrength = mock<SignalStrength>()
whenever(signalStrength.level).thenReturn(1)
getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>(telephonyManager)
.onSignalStrengthsChanged(signalStrength)
// THEN it's logged to the buffer
assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}1")
// WHEN isCarrierMerged is set to true
val networkId = 2
wifiRepository.setWifiNetwork(
WifiNetworkModel.CarrierMerged(
networkId,
SUB_ID,
level = 3,
)
)
underTest.setIsCarrierMerged(true)
// THEN the carrier merged info is logged
assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}3")
// WHEN the carrier merge network is updated
wifiRepository.setWifiNetwork(
WifiNetworkModel.CarrierMerged(
networkId,
SUB_ID,
level = 4,
)
)
// THEN the new level is logged
assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}4")
// WHEN isCarrierMerged is set to false
underTest.setIsCarrierMerged(false)
// THEN the typical info is logged
// Note: Since our first logs also had the typical info, we need to search the log
// contents for after our carrier merged level log.
val fullBuffer = dumpBuffer()
val carrierMergedContentIndex = fullBuffer.indexOf("${BUFFER_SEPARATOR}4")
val bufferAfterCarrierMerged = fullBuffer.substring(carrierMergedContentIndex)
assertThat(bufferAfterCarrierMerged).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}1")
// WHEN the normal network is updated
val newMobileInfo =
MobileConnectionModel(
operatorAlphaShort = "Mobile Operator 2",
primaryLevel = 0,
)
mobileRepo.setConnectionInfo(newMobileInfo)
// THEN the new level is logged
assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}0")
job.cancel()
}
@Test
fun connectionInfo_logging_doesNotLogUpdatesForNotActiveRepo() =
testScope.runTest {
// SETUP: Use real repositories to verify the diffing still works. (See b/267501739.)
val telephonyManager = mock<TelephonyManager>()
createRealMobileRepo(telephonyManager)
val wifiRepository = FakeWifiRepository()
createRealCarrierMergedRepo(wifiRepository)
// WHEN isCarrierMerged = false
initializeRepo(startingIsCarrierMerged = false)
val job = underTest.connectionInfo.launchIn(this)
val signalStrength = mock<SignalStrength>()
whenever(signalStrength.level).thenReturn(1)
getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>(telephonyManager)
.onSignalStrengthsChanged(signalStrength)
// THEN updates to the carrier merged level aren't logged
val networkId = 2
wifiRepository.setWifiNetwork(
WifiNetworkModel.CarrierMerged(
networkId,
SUB_ID,
level = 4,
)
)
assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}4")
wifiRepository.setWifiNetwork(
WifiNetworkModel.CarrierMerged(
networkId,
SUB_ID,
level = 3,
)
)
assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}3")
// WHEN isCarrierMerged is set to true
underTest.setIsCarrierMerged(true)
// THEN updates to the normal level aren't logged
whenever(signalStrength.level).thenReturn(5)
getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>(telephonyManager)
.onSignalStrengthsChanged(signalStrength)
assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}5")
whenever(signalStrength.level).thenReturn(6)
getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>(telephonyManager)
.onSignalStrengthsChanged(signalStrength)
assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}6")
job.cancel()
}
private fun initializeRepo(startingIsCarrierMerged: Boolean) {
underTest =
@@ -364,9 +583,68 @@ class FullMobileConnectionRepositoryTest : SysuiTestCase() {
)
}
private fun createRealMobileRepo(
telephonyManager: TelephonyManager,
): MobileConnectionRepositoryImpl {
whenever(telephonyManager.subscriptionId).thenReturn(SUB_ID)
val realRepo =
MobileConnectionRepositoryImpl(
context,
SUB_ID,
defaultNetworkName = NetworkNameModel.Default("default"),
networkNameSeparator = SEP,
telephonyManager,
systemUiCarrierConfig = mock(),
fakeBroadcastDispatcher,
mobileMappingsProxy = mock(),
testDispatcher,
logger = mock(),
tableLogBuffer,
testScope.backgroundScope,
)
whenever(
mobileFactory.build(
eq(SUB_ID),
any(),
eq(DEFAULT_NAME),
eq(SEP),
)
)
.thenReturn(realRepo)
return realRepo
}
private fun createRealCarrierMergedRepo(
wifiRepository: FakeWifiRepository,
): CarrierMergedConnectionRepository {
wifiRepository.setIsWifiEnabled(true)
wifiRepository.setIsWifiDefault(true)
val realRepo =
CarrierMergedConnectionRepository(
SUB_ID,
tableLogBuffer,
defaultNetworkName = NetworkNameModel.Default("default"),
testScope.backgroundScope,
wifiRepository,
)
whenever(carrierMergedFactory.build(eq(SUB_ID), any(), eq(DEFAULT_NAME)))
.thenReturn(realRepo)
return realRepo
}
private fun dumpBuffer(): String {
val outputWriter = StringWriter()
tableLogBuffer.dump(PrintWriter(outputWriter), arrayOf())
return outputWriter.toString()
}
private companion object {
const val SUB_ID = 42
private val DEFAULT_NAME = NetworkNameModel.Default("default name")
private const val SEP = "-"
private const val BUFFER_SEPARATOR = "|"
}
}

View File

@@ -71,7 +71,6 @@ import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
@@ -86,7 +85,6 @@ import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@@ -670,16 +668,8 @@ class MobileConnectionRepositoryTest : SysuiTestCase() {
job.cancel()
}
private fun getTelephonyCallbacks(): List<TelephonyCallback> {
val callbackCaptor = argumentCaptor<TelephonyCallback>()
Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
return callbackCaptor.allValues
}
private inline fun <reified T> getTelephonyCallbackForType(): T {
val cbs = getTelephonyCallbacks().filterIsInstance<T>()
assertThat(cbs.size).isEqualTo(1)
return cbs[0]
return MobileTelephonyHelpers.getTelephonyCallbackForType(telephonyManager)
}
/** Convenience constructor for SignalStrength */

View File

@@ -84,6 +84,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
@Mock private lateinit var subscriptionManager: SubscriptionManager
@Mock private lateinit var telephonyManager: TelephonyManager
@Mock private lateinit var logger: ConnectivityPipelineLogger
@Mock private lateinit var summaryLogger: TableLogBuffer
@Mock private lateinit var logBufferFactory: TableLogBufferFactory
private val mobileMappings = FakeMobileMappingsProxy()
@@ -157,6 +158,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
subscriptionManager,
telephonyManager,
logger,
summaryLogger,
mobileMappings,
fakeBroadcastDispatcher,
context,
@@ -616,6 +618,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() {
subscriptionManager,
telephonyManager,
logger,
summaryLogger,
mobileMappings,
fakeBroadcastDispatcher,
context,

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2023 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.systemui.statusbar.pipeline.mobile.data.repository.prod
import android.telephony.TelephonyCallback
import android.telephony.TelephonyManager
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.google.common.truth.Truth.assertThat
import org.mockito.Mockito.verify
/** Helper methods for telephony-related callbacks for mobile tests. */
object MobileTelephonyHelpers {
fun getTelephonyCallbacks(mockTelephonyManager: TelephonyManager): List<TelephonyCallback> {
val callbackCaptor = argumentCaptor<TelephonyCallback>()
verify(mockTelephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
return callbackCaptor.allValues
}
inline fun <reified T> getTelephonyCallbackForType(mockTelephonyManager: TelephonyManager): T {
val cbs = getTelephonyCallbacks(mockTelephonyManager).filterIsInstance<T>()
assertThat(cbs.size).isEqualTo(1)
return cbs[0]
}
}

View File

@@ -71,6 +71,8 @@ class FakeMobileIconInteractor(
private val _numberOfLevels = MutableStateFlow(DEFAULT_NUM_LEVELS)
override val numberOfLevels = _numberOfLevels
override val isForceHidden = MutableStateFlow(false)
fun setIconGroup(group: SignalIcon.MobileIconGroup) {
_iconGroup.value = group
}

View File

@@ -73,6 +73,8 @@ class FakeMobileIconsInteractor(
private val _isUserSetup = MutableStateFlow(true)
override val isUserSetup = _isUserSetup
override val isForceHidden = MutableStateFlow(false)
/** Always returns a new fake interactor */
override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor {
return FakeMobileIconInteractor(tableLogBuffer)

View File

@@ -66,6 +66,7 @@ class MobileIconInteractorTest : SysuiTestCase() {
mobileIconsInteractor.defaultMobileIconGroup,
mobileIconsInteractor.defaultDataSubId,
mobileIconsInteractor.isDefaultConnectionFailed,
mobileIconsInteractor.isForceHidden,
connectionRepository,
)
}
@@ -550,6 +551,21 @@ class MobileIconInteractorTest : SysuiTestCase() {
job.cancel()
}
@Test
fun isForceHidden_matchesParent() =
runBlocking(IMMEDIATE) {
var latest: Boolean? = null
val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
mobileIconsInteractor.isForceHidden.value = true
assertThat(latest).isTrue()
mobileIconsInteractor.isForceHidden.value = false
assertThat(latest).isFalse()
job.cancel()
}
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate

View File

@@ -27,6 +27,8 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobile
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.util.CarrierConfigTracker
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
@@ -50,6 +52,7 @@ import org.mockito.MockitoAnnotations
@SmallTest
class MobileIconsInteractorTest : SysuiTestCase() {
private lateinit var underTest: MobileIconsInteractor
private lateinit var connectivityRepository: FakeConnectivityRepository
private lateinit var connectionsRepository: FakeMobileConnectionsRepository
private val userSetupRepository = FakeUserSetupRepository()
private val mobileMappingsProxy = FakeMobileMappingsProxy()
@@ -63,6 +66,8 @@ class MobileIconsInteractorTest : SysuiTestCase() {
fun setUp() {
MockitoAnnotations.initMocks(this)
connectivityRepository = FakeConnectivityRepository()
connectionsRepository = FakeMobileConnectionsRepository(mobileMappingsProxy, tableLogBuffer)
connectionsRepository.setMobileConnectionRepositoryMap(
mapOf(
@@ -79,6 +84,8 @@ class MobileIconsInteractorTest : SysuiTestCase() {
connectionsRepository,
carrierConfigTracker,
logger = mock(),
tableLogger = mock(),
connectivityRepository,
userSetupRepository,
testScope.backgroundScope,
)
@@ -609,6 +616,32 @@ class MobileIconsInteractorTest : SysuiTestCase() {
job.cancel()
}
@Test
fun isForceHidden_repoHasMobileHidden_true() =
testScope.runTest {
var latest: Boolean? = null
val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE))
assertThat(latest).isTrue()
job.cancel()
}
@Test
fun isForceHidden_repoDoesNotHaveMobileHidden_false() =
testScope.runTest {
var latest: Boolean? = null
val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
assertThat(latest).isFalse()
job.cancel()
}
companion object {
private val tableLogBuffer =
TableLogBuffer(8, "MobileIconsInteractorTest", FakeSystemClock())

View File

@@ -29,12 +29,14 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.QsMobileIconViewModel
import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -58,31 +60,37 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() {
@Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
@Mock private lateinit var tableLogBuffer: TableLogBuffer
@Mock private lateinit var logger: ConnectivityPipelineLogger
@Mock private lateinit var constants: ConnectivityConstants
private lateinit var interactor: FakeMobileIconInteractor
private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
private lateinit var airplaneModeInteractor: AirplaneModeInteractor
private lateinit var viewModelCommon: MobileIconViewModel
private lateinit var viewModel: LocationBasedMobileViewModel
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
// This line was necessary to make the onDarkChanged and setStaticDrawableColor tests pass.
// But, it maybe *shouldn't* be necessary.
whenever(constants.hasDataCapabilities).thenReturn(true)
testableLooper = TestableLooper.get(this)
val interactor = FakeMobileIconInteractor(tableLogBuffer)
val viewModelCommon =
MobileIconViewModel(
subscriptionId = 1,
interactor,
logger,
constants,
testScope.backgroundScope,
airplaneModeRepository = FakeAirplaneModeRepository()
airplaneModeInteractor =
AirplaneModeInteractor(
airplaneModeRepository,
FakeConnectivityRepository(),
)
viewModel = QsMobileIconViewModel(viewModelCommon, statusBarPipelineFlags)
interactor = FakeMobileIconInteractor(tableLogBuffer)
createViewModel()
}
// Note: The following tests are more like integration tests, since they stand up a full
// [WifiViewModel] and test the interactions between the view, view-binder, and view-model.
// [MobileIconViewModel] and test the interactions between the view, view-binder, and
// view-model.
@Test
fun setVisibleState_icon_iconShownDotHidden() {
@@ -130,7 +138,25 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() {
}
@Test
fun isIconVisible_alwaysTrue() {
fun isIconVisible_noData_outputsFalse() {
whenever(constants.hasDataCapabilities).thenReturn(false)
createViewModel()
val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel)
ViewUtils.attachView(view)
testableLooper.processAllMessages()
assertThat(view.isIconVisible).isFalse()
ViewUtils.detachView(view)
}
@Test
fun isIconVisible_hasData_outputsTrue() {
whenever(constants.hasDataCapabilities).thenReturn(true)
createViewModel()
val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel)
ViewUtils.attachView(view)
@@ -141,6 +167,34 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() {
ViewUtils.detachView(view)
}
@Test
fun isIconVisible_notAirplaneMode_outputsTrue() {
airplaneModeRepository.setIsAirplaneMode(false)
val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel)
ViewUtils.attachView(view)
testableLooper.processAllMessages()
assertThat(view.isIconVisible).isTrue()
ViewUtils.detachView(view)
}
@Test
fun isIconVisible_airplaneMode_outputsTrue() {
airplaneModeRepository.setIsAirplaneMode(true)
val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel)
ViewUtils.attachView(view)
testableLooper.processAllMessages()
assertThat(view.isIconVisible).isFalse()
ViewUtils.detachView(view)
}
@Test
fun onDarkChanged_iconHasNewColor() {
whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false)
@@ -184,6 +238,18 @@ class ModernStatusBarMobileViewTest : SysuiTestCase() {
private fun View.getDotView(): View {
return this.requireViewById(R.id.status_bar_dot)
}
private fun createViewModel() {
viewModelCommon =
MobileIconViewModel(
subscriptionId = 1,
interactor,
airplaneModeInteractor,
constants,
testScope.backgroundScope,
)
viewModel = QsMobileIconViewModel(viewModelCommon, statusBarPipelineFlags)
}
}
private const val SLOT_NAME = "TestSlotName"

View File

@@ -21,10 +21,13 @@ import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.SysuiTestCase
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.model.SignalIconModel
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModelTest.Companion.defaultSignal
import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
@@ -46,8 +49,8 @@ class LocationBasedMobileIconViewModelTest : SysuiTestCase() {
private lateinit var qsIcon: QsMobileIconViewModel
private lateinit var keyguardIcon: KeyguardMobileIconViewModel
private lateinit var interactor: FakeMobileIconInteractor
private lateinit var airplaneModeInteractor: AirplaneModeInteractor
@Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
@Mock private lateinit var logger: ConnectivityPipelineLogger
@Mock private lateinit var constants: ConnectivityConstants
@Mock private lateinit var tableLogBuffer: TableLogBuffer
@@ -57,6 +60,11 @@ class LocationBasedMobileIconViewModelTest : SysuiTestCase() {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
airplaneModeInteractor =
AirplaneModeInteractor(
FakeAirplaneModeRepository(),
FakeConnectivityRepository(),
)
interactor = FakeMobileIconInteractor(tableLogBuffer)
interactor.apply {
setLevel(1)
@@ -68,7 +76,13 @@ class LocationBasedMobileIconViewModelTest : SysuiTestCase() {
isDataConnected.value = true
}
commonImpl =
MobileIconViewModel(SUB_1_ID, interactor, logger, constants, testScope.backgroundScope)
MobileIconViewModel(
SUB_1_ID,
interactor,
airplaneModeInteractor,
constants,
testScope.backgroundScope,
)
homeIcon = HomeMobileIconViewModel(commonImpl, statusBarPipelineFlags)
qsIcon = QsMobileIconViewModel(commonImpl, statusBarPipelineFlags)
@@ -78,14 +92,14 @@ class LocationBasedMobileIconViewModelTest : SysuiTestCase() {
@Test
fun `location based view models receive same icon id when common impl updates`() =
testScope.runTest {
var latestHome: Int? = null
val homeJob = homeIcon.iconId.onEach { latestHome = it }.launchIn(this)
var latestHome: SignalIconModel? = null
val homeJob = homeIcon.icon.onEach { latestHome = it }.launchIn(this)
var latestQs: Int? = null
val qsJob = qsIcon.iconId.onEach { latestQs = it }.launchIn(this)
var latestQs: SignalIconModel? = null
val qsJob = qsIcon.icon.onEach { latestQs = it }.launchIn(this)
var latestKeyguard: Int? = null
val keyguardJob = keyguardIcon.iconId.onEach { latestKeyguard = it }.launchIn(this)
var latestKeyguard: SignalIconModel? = null
val keyguardJob = keyguardIcon.icon.onEach { latestKeyguard = it }.launchIn(this)
var expected = defaultSignal(level = 1)

View File

@@ -19,16 +19,18 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
import androidx.test.filters.SmallTest
import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH
import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH_NONE
import com.android.settingslib.graph.SignalDrawable
import com.android.settingslib.mobile.TelephonyIcons.THREE_G
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.model.SignalIconModel
import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -49,7 +51,8 @@ import org.mockito.MockitoAnnotations
class MobileIconViewModelTest : SysuiTestCase() {
private lateinit var underTest: MobileIconViewModel
private lateinit var interactor: FakeMobileIconInteractor
@Mock private lateinit var logger: ConnectivityPipelineLogger
private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
private lateinit var airplaneModeInteractor: AirplaneModeInteractor
@Mock private lateinit var constants: ConnectivityConstants
@Mock private lateinit var tableLogBuffer: TableLogBuffer
@@ -59,6 +62,15 @@ class MobileIconViewModelTest : SysuiTestCase() {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(constants.hasDataCapabilities).thenReturn(true)
airplaneModeRepository = FakeAirplaneModeRepository()
airplaneModeInteractor =
AirplaneModeInteractor(
airplaneModeRepository,
FakeConnectivityRepository(),
)
interactor = FakeMobileIconInteractor(tableLogBuffer)
interactor.apply {
setLevel(1)
@@ -69,15 +81,94 @@ class MobileIconViewModelTest : SysuiTestCase() {
setNumberOfLevels(4)
isDataConnected.value = true
}
underTest =
MobileIconViewModel(SUB_1_ID, interactor, logger, constants, testScope.backgroundScope)
createAndSetViewModel()
}
@Test
fun isVisible_notDataCapable_alwaysFalse() =
testScope.runTest {
// Create a new view model here so the constants are properly read
whenever(constants.hasDataCapabilities).thenReturn(false)
createAndSetViewModel()
var latest: Boolean? = null
val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
assertThat(latest).isFalse()
job.cancel()
}
@Test
fun isVisible_notAirplane_notForceHidden_true() =
testScope.runTest {
var latest: Boolean? = null
val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
airplaneModeRepository.setIsAirplaneMode(false)
interactor.isForceHidden.value = false
assertThat(latest).isTrue()
job.cancel()
}
@Test
fun isVisible_airplane_false() =
testScope.runTest {
var latest: Boolean? = null
val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
airplaneModeRepository.setIsAirplaneMode(true)
interactor.isForceHidden.value = false
assertThat(latest).isFalse()
job.cancel()
}
@Test
fun isVisible_forceHidden_false() =
testScope.runTest {
var latest: Boolean? = null
val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
airplaneModeRepository.setIsAirplaneMode(false)
interactor.isForceHidden.value = true
assertThat(latest).isFalse()
job.cancel()
}
@Test
fun isVisible_respondsToUpdates() =
testScope.runTest {
var latest: Boolean? = null
val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
airplaneModeRepository.setIsAirplaneMode(false)
interactor.isForceHidden.value = false
assertThat(latest).isTrue()
airplaneModeRepository.setIsAirplaneMode(true)
assertThat(latest).isFalse()
airplaneModeRepository.setIsAirplaneMode(false)
assertThat(latest).isTrue()
interactor.isForceHidden.value = true
assertThat(latest).isFalse()
job.cancel()
}
@Test
fun iconId_correctLevel_notCutout() =
testScope.runTest {
var latest: Int? = null
val job = underTest.iconId.onEach { latest = it }.launchIn(this)
var latest: SignalIconModel? = null
val job = underTest.icon.onEach { latest = it }.launchIn(this)
val expected = defaultSignal()
assertThat(latest).isEqualTo(expected)
@@ -90,8 +181,8 @@ class MobileIconViewModelTest : SysuiTestCase() {
testScope.runTest {
interactor.setIsDefaultDataEnabled(false)
var latest: Int? = null
val job = underTest.iconId.onEach { latest = it }.launchIn(this)
var latest: SignalIconModel? = null
val job = underTest.icon.onEach { latest = it }.launchIn(this)
val expected = defaultSignal(level = 1, connected = false)
assertThat(latest).isEqualTo(expected)
@@ -102,8 +193,8 @@ class MobileIconViewModelTest : SysuiTestCase() {
@Test
fun `icon - uses empty state - when not in service`() =
testScope.runTest {
var latest: Int? = null
val job = underTest.iconId.onEach { latest = it }.launchIn(this)
var latest: SignalIconModel? = null
val job = underTest.icon.onEach { latest = it }.launchIn(this)
interactor.isInService.value = false
@@ -364,14 +455,7 @@ class MobileIconViewModelTest : SysuiTestCase() {
testScope.runTest {
// Create a new view model here so the constants are properly read
whenever(constants.shouldShowActivityConfig).thenReturn(false)
underTest =
MobileIconViewModel(
SUB_1_ID,
interactor,
logger,
constants,
testScope.backgroundScope,
)
createAndSetViewModel()
var inVisible: Boolean? = null
val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
@@ -403,14 +487,7 @@ class MobileIconViewModelTest : SysuiTestCase() {
testScope.runTest {
// Create a new view model here so the constants are properly read
whenever(constants.shouldShowActivityConfig).thenReturn(true)
underTest =
MobileIconViewModel(
SUB_1_ID,
interactor,
logger,
constants,
testScope.backgroundScope,
)
createAndSetViewModel()
var inVisible: Boolean? = null
val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
@@ -459,6 +536,16 @@ class MobileIconViewModelTest : SysuiTestCase() {
containerJob.cancel()
}
private fun createAndSetViewModel() {
underTest = MobileIconViewModel(
SUB_1_ID,
interactor,
airplaneModeInteractor,
constants,
testScope.backgroundScope,
)
}
companion object {
private const val SUB_1_ID = 1
@@ -466,10 +553,11 @@ class MobileIconViewModelTest : SysuiTestCase() {
fun defaultSignal(
level: Int = 1,
connected: Boolean = true,
): Int {
return SignalDrawable.getState(level, /* numLevels */ 4, !connected)
): SignalIconModel {
return SignalIconModel(level, numberOfLevels = 4, showExclamationMark = !connected)
}
fun emptySignal(): Int = SignalDrawable.getEmptyState(4)
fun emptySignal(): SignalIconModel =
SignalIconModel(level = 0, numberOfLevels = 4, showExclamationMark = true)
}
}

View File

@@ -20,11 +20,14 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -46,6 +49,7 @@ class MobileIconsViewModelTest : SysuiTestCase() {
private lateinit var underTest: MobileIconsViewModel
private val interactor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
private lateinit var airplaneModeInteractor: AirplaneModeInteractor
@Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
@Mock private lateinit var logger: ConnectivityPipelineLogger
@Mock private lateinit var constants: ConnectivityConstants
@@ -57,6 +61,12 @@ class MobileIconsViewModelTest : SysuiTestCase() {
fun setUp() {
MockitoAnnotations.initMocks(this)
airplaneModeInteractor =
AirplaneModeInteractor(
FakeAirplaneModeRepository(),
FakeConnectivityRepository(),
)
val subscriptionIdsFlow =
interactor.filteredSubscriptions
.map { subs -> subs.map { it.subscriptionId } }
@@ -66,6 +76,7 @@ class MobileIconsViewModelTest : SysuiTestCase() {
MobileIconsViewModel(
subscriptionIdsFlow,
interactor,
airplaneModeInteractor,
logger,
constants,
testScope.backgroundScope,