diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 66813f90bac4e..f3339c06ce8f5 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -354,6 +354,10 @@ object Flags { // TODO(b/280426085): Tracking Bug @JvmField val NEW_BLUETOOTH_REPOSITORY = releasedFlag(612, "new_bluetooth_repository") + // TODO(b/292533677): Tracking Bug + val WIFI_TRACKER_LIB_FOR_WIFI_ICON = + unreleasedFlag(613, "wifi_tracker_lib_for_wifi_icon") + // 700 - dialer/calls // TODO(b/254512734): Tracking Bug val ONGOING_CALL_STATUS_BAR_CHIP = releasedFlag(700, "ongoing_call_status_bar_chip") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java index c9ce12a6859ea..ff40f70a84fbe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImpl.java @@ -16,13 +16,7 @@ package com.android.systemui.statusbar.connectivity; -import android.content.Context; import android.content.Intent; -import android.net.ConnectivityManager; -import android.net.wifi.WifiManager; -import android.os.Handler; -import android.os.SimpleClock; -import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -35,25 +29,18 @@ import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleRegistry; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.settings.UserTracker; import com.android.wifitrackerlib.MergedCarrierEntry; import com.android.wifitrackerlib.WifiEntry; import com.android.wifitrackerlib.WifiPickerTracker; import java.io.PrintWriter; -import java.time.Clock; -import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.Executor; -import javax.inject.Inject; - /** */ public class AccessPointControllerImpl implements AccessPointController, WifiPickerTracker.WifiPickerTrackerCallback, @@ -272,77 +259,4 @@ public class AccessPointControllerImpl implements AccessPointController, } } }; - - /** - * Factory for creating {@link WifiPickerTracker}. - * - * Uses the same time intervals as the Settings page for Wifi. - */ - @SysUISingleton - public static class WifiPickerTrackerFactory { - - // Max age of tracked WifiEntries - private static final long MAX_SCAN_AGE_MILLIS = 15_000; - // Interval between initiating WifiPickerTracker scans - private static final long SCAN_INTERVAL_MILLIS = 10_000; - - private final Context mContext; - private final @Nullable WifiManager mWifiManager; - private final ConnectivityManager mConnectivityManager; - private final Handler mMainHandler; - private final Handler mWorkerHandler; - private final Clock mClock = new SimpleClock(ZoneOffset.UTC) { - @Override - public long millis() { - return SystemClock.elapsedRealtime(); - } - }; - - @Inject - public WifiPickerTrackerFactory( - Context context, - @Nullable WifiManager wifiManager, - ConnectivityManager connectivityManager, - @Main Handler mainHandler, - @Background Handler workerHandler - ) { - mContext = context; - mWifiManager = wifiManager; - mConnectivityManager = connectivityManager; - mMainHandler = mainHandler; - mWorkerHandler = workerHandler; - } - - private boolean isSupported() { - return mWifiManager != null; - } - - /** - * Create a {@link WifiPickerTracker} - * - * @param lifecycle - * @param listener - * @return a new {@link WifiPickerTracker} or {@code null} if {@link WifiManager} is null. - */ - public @Nullable WifiPickerTracker create( - Lifecycle lifecycle, - WifiPickerTracker.WifiPickerTrackerCallback listener - ) { - if (mWifiManager == null) { - return null; - } - return new WifiPickerTracker( - lifecycle, - mContext, - mWifiManager, - mConnectivityManager, - mMainHandler, - mWorkerHandler, - mClock, - MAX_SCAN_AGE_MILLIS, - SCAN_INTERVAL_MILLIS, - listener - ); - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiPickerTrackerFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiPickerTrackerFactory.kt new file mode 100644 index 0000000000000..ddbfcef513c32 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiPickerTrackerFactory.kt @@ -0,0 +1,92 @@ +/* + * 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.connectivity + +import android.content.Context +import android.net.ConnectivityManager +import android.net.wifi.WifiManager +import android.os.Handler +import android.os.SimpleClock +import androidx.lifecycle.Lifecycle +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.util.time.SystemClock +import com.android.wifitrackerlib.WifiPickerTracker +import com.android.wifitrackerlib.WifiPickerTracker.WifiPickerTrackerCallback +import java.time.Clock +import java.time.ZoneOffset +import javax.inject.Inject + +/** + * Factory for creating [WifiPickerTracker] for SysUI. + * + * Uses the same time intervals as the Settings page for Wifi. + */ +@SysUISingleton +class WifiPickerTrackerFactory +@Inject +constructor( + private val context: Context, + private val wifiManager: WifiManager?, + private val connectivityManager: ConnectivityManager, + private val systemClock: SystemClock, + @Main private val mainHandler: Handler, + @Background private val workerHandler: Handler, +) { + private val clock: Clock = + object : SimpleClock(ZoneOffset.UTC) { + override fun millis(): Long { + return systemClock.elapsedRealtime() + } + } + val isSupported: Boolean + get() = wifiManager != null + + /** + * Creates a [WifiPickerTracker] instance. + * + * @return a new [WifiPickerTracker] or null if [WifiManager] is null. + */ + fun create( + lifecycle: Lifecycle, + listener: WifiPickerTrackerCallback, + ): WifiPickerTracker? { + return if (wifiManager == null) { + null + } else + WifiPickerTracker( + lifecycle, + context, + wifiManager, + connectivityManager, + mainHandler, + workerHandler, + clock, + MAX_SCAN_AGE_MILLIS, + SCAN_INTERVAL_MILLIS, + listener, + ) + } + + companion object { + /** Max age of tracked WifiEntries. */ + private const val MAX_SCAN_AGE_MILLIS: Long = 15000 + /** Interval between initiating WifiPickerTracker scans. */ + private const val SCAN_INTERVAL_MILLIS: Long = 10000 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 0e99c67a0d801..e1fd37f558aba 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -19,6 +19,8 @@ package com.android.systemui.statusbar.pipeline.dagger import android.net.wifi.WifiManager import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory import com.android.systemui.log.table.TableLogBuffer @@ -48,9 +50,12 @@ import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStat import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModelImpl import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryDagger import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryViaTrackerLibDagger import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.DisabledWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl +import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryViaTrackerLib import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl import dagger.Binds @@ -114,14 +119,19 @@ abstract class StatusBarPipelineModule { impl: CollapsedStatusBarViewBinderImpl ): CollapsedStatusBarViewBinder + @Binds + @IntoMap + @ClassKey(WifiRepositoryDagger::class) + abstract fun bindWifiRepositoryDagger(impl: WifiRepositoryDagger): CoreStartable + companion object { @Provides @SysUISingleton - fun provideRealWifiRepository( + fun provideWifiRepositoryDagger( wifiManager: WifiManager?, disabledWifiRepository: DisabledWifiRepository, wifiRepositoryImplFactory: WifiRepositoryImpl.Factory, - ): RealWifiRepository { + ): WifiRepositoryDagger { // If we have a null [WifiManager], then the wifi repository should be permanently // disabled. return if (wifiManager == null) { @@ -131,6 +141,36 @@ abstract class StatusBarPipelineModule { } } + @Provides + @SysUISingleton + fun provideWifiRepositoryViaTrackerLibDagger( + wifiManager: WifiManager?, + disabledWifiRepository: DisabledWifiRepository, + wifiRepositoryFromTrackerLibFactory: WifiRepositoryViaTrackerLib.Factory, + ): WifiRepositoryViaTrackerLibDagger { + // If we have a null [WifiManager], then the wifi repository should be permanently + // disabled. + return if (wifiManager == null) { + disabledWifiRepository + } else { + wifiRepositoryFromTrackerLibFactory.create(wifiManager) + } + } + + @Provides + @SysUISingleton + fun provideRealWifiRepository( + wifiRepository: WifiRepositoryDagger, + wifiRepositoryFromTrackerLib: WifiRepositoryViaTrackerLibDagger, + flags: FeatureFlags, + ): RealWifiRepository { + return if (flags.isEnabled(Flags.WIFI_TRACKER_LIB_FOR_WIFI_ICON)) { + wifiRepositoryFromTrackerLib + } else { + wifiRepository + } + } + @Provides @SysUISingleton @Named(FIRST_MOBILE_SUB_SHOWING_NETWORK_TYPE_ICON) @@ -149,6 +189,14 @@ abstract class StatusBarPipelineModule { return factory.create("WifiInputLog", 50) } + @Provides + @SysUISingleton + @WifiTrackerLibInputLog + fun provideWifiTrackerLibInputLogBuffer(factory: LogBufferFactory): LogBuffer { + // WifiTrackerLib is pretty noisy, so give it more room than WifiInputLog. + return factory.create("WifiTrackerLibInputLog", 200) + } + @Provides @SysUISingleton @WifiTableLog @@ -156,6 +204,13 @@ abstract class StatusBarPipelineModule { return factory.create("WifiTableLog", 100) } + @Provides + @SysUISingleton + @WifiTrackerLibTableLog + fun provideWifiTrackerLibTableLogBuffer(factory: TableLogBufferFactory): TableLogBuffer { + return factory.create("WifiTrackerLibTableLog", 100) + } + @Provides @SysUISingleton @AirplaneTableLog diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTrackerLibInputLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTrackerLibInputLog.kt new file mode 100644 index 0000000000000..b84b01e669550 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTrackerLibInputLog.kt @@ -0,0 +1,25 @@ +/* + * 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 + +/** Wifi logs for inputs into [WifiRepositoryViaTrackerLib]. */ +@Qualifier +@MustBeDocumented +@kotlin.annotation.Retention(AnnotationRetention.RUNTIME) +annotation class WifiTrackerLibInputLog diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTrackerLibTableLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTrackerLibTableLog.kt new file mode 100644 index 0000000000000..7ca70308361d6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/WifiTrackerLibTableLog.kt @@ -0,0 +1,25 @@ +/* + * 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 + +/** Wifi logs from [WifiRepositoryViaTrackerLib] in table format. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class WifiTrackerLibTableLog diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt index f800cf496ca99..b11b4727c3c3b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.wifi.data.repository +import com.android.systemui.CoreStartable import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import kotlinx.coroutines.flow.StateFlow @@ -42,6 +43,13 @@ interface WifiRepository { val currentNetwork = wifiNetwork.value return currentNetwork is WifiNetworkModel.Active && currentNetwork.hasValidSsid() } + + companion object { + /** Column name to use for [isWifiEnabled] for table logging. */ + const val COL_NAME_IS_ENABLED = "isEnabled" + /** Column name to use for [isWifiDefault] for table logging. */ + const val COL_NAME_IS_DEFAULT = "isDefault" + } } /** @@ -53,3 +61,8 @@ interface WifiRepository { * repository. */ interface RealWifiRepository : WifiRepository + +/** Used only by Dagger to bind [WifiRepositoryImpl]. */ +interface WifiRepositoryDagger : RealWifiRepository, CoreStartable +/** Used only by Dagger to bind [WifiRepositoryViaTrackerLib]. */ +interface WifiRepositoryViaTrackerLibDagger : RealWifiRepository diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt index 86a668a2e8424..9ed7c6a83cee1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt @@ -18,7 +18,8 @@ package com.android.systemui.statusbar.pipeline.wifi.data.repository.prod import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel -import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryDagger +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryViaTrackerLibDagger import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -32,7 +33,10 @@ import kotlinx.coroutines.flow.asStateFlow * wifi information. */ @SysUISingleton -class DisabledWifiRepository @Inject constructor() : RealWifiRepository { +class DisabledWifiRepository @Inject constructor() : + WifiRepositoryDagger, WifiRepositoryViaTrackerLibDagger { + override fun start() {} + override val isWifiEnabled: StateFlow = MutableStateFlow(false).asStateFlow() override val isWifiDefault: StateFlow = MutableStateFlow(false).asStateFlow() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt index 7f35dfbe27006..995de6d2fc615 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt @@ -43,8 +43,10 @@ import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityMod import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl.Companion.getMainOrUnderlyingWifiInfo -import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_DEFAULT +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_ENABLED +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryDagger import com.android.systemui.statusbar.pipeline.wifi.shared.WifiInputLogger import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import java.util.concurrent.Executor @@ -64,6 +66,7 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** Real implementation of [WifiRepository]. */ @@ -81,9 +84,21 @@ constructor( @WifiTableLog wifiTableLogBuffer: TableLogBuffer, @Main mainExecutor: Executor, @Background private val bgDispatcher: CoroutineDispatcher, - @Application scope: CoroutineScope, + @Application private val scope: CoroutineScope, private val wifiManager: WifiManager, -) : RealWifiRepository { +) : WifiRepositoryDagger { + + override fun start() { + // There are two possible [WifiRepository] implementations: This class (old) and + // [WifiRepositoryFromTrackerLib] (new). While we migrate to the new class, we want this old + // class to still be running in the background so that we can collect logs and compare + // discrepancies. This #start method collects on the flows to ensure that the logs are + // collected. + scope.launch { isWifiEnabled.collect {} } + scope.launch { isWifiDefault.collect {} } + scope.launch { wifiNetwork.collect {} } + scope.launch { wifiActivity.collect {} } + } private val wifiStateChangeEvents: Flow = broadcastDispatcher @@ -104,7 +119,7 @@ constructor( .logDiffsForTable( wifiTableLogBuffer, columnPrefix = "", - columnName = "isEnabled", + columnName = COL_NAME_IS_ENABLED, initialValue = false, ) .stateIn( @@ -125,7 +140,7 @@ constructor( .logDiffsForTable( wifiTableLogBuffer, columnPrefix = "", - columnName = "isDefault", + columnName = COL_NAME_IS_DEFAULT, initialValue = false, ) .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) @@ -234,6 +249,8 @@ constructor( // NetworkCallback inside [wifiNetwork] for our wifi network information. val WIFI_NETWORK_DEFAULT = WifiNetworkModel.Inactive + const val WIFI_STATE_DEFAULT = WifiManager.WIFI_STATE_DISABLED + private fun createWifiNetworkModel( wifiInfo: WifiInfo, network: Network, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLib.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLib.kt new file mode 100644 index 0000000000000..127136789ba6d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLib.kt @@ -0,0 +1,295 @@ +/* + * 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.wifi.data.repository.prod + +import android.net.wifi.WifiManager +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.log.table.logDiffsForTable +import com.android.systemui.statusbar.connectivity.WifiPickerTrackerFactory +import com.android.systemui.statusbar.pipeline.dagger.WifiTrackerLibInputLog +import com.android.systemui.statusbar.pipeline.dagger.WifiTrackerLibTableLog +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_DEFAULT +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository.Companion.COL_NAME_IS_ENABLED +import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryViaTrackerLibDagger +import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT +import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl.Companion.WIFI_STATE_DEFAULT +import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel +import com.android.wifitrackerlib.MergedCarrierEntry +import com.android.wifitrackerlib.WifiEntry +import com.android.wifitrackerlib.WifiPickerTracker +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** + * An implementation of [WifiRepository] that uses [com.android.wifitrackerlib] as the source of + * truth for wifi information. + * + * Serves as a possible replacement for [WifiRepositoryImpl]. See b/292534484. + */ +@SysUISingleton +class WifiRepositoryViaTrackerLib +@Inject +constructor( + @Application private val scope: CoroutineScope, + @Main private val mainExecutor: Executor, + private val wifiPickerTrackerFactory: WifiPickerTrackerFactory, + private val wifiManager: WifiManager, + @WifiTrackerLibInputLog private val inputLogger: LogBuffer, + @WifiTrackerLibTableLog private val wifiTrackerLibTableLogBuffer: TableLogBuffer, +) : WifiRepositoryViaTrackerLibDagger, LifecycleOwner { + + override val lifecycle = + LifecycleRegistry(this).also { + mainExecutor.execute { it.currentState = Lifecycle.State.CREATED } + } + + private var wifiPickerTracker: WifiPickerTracker? = null + + private val wifiPickerTrackerInfo: StateFlow = run { + var current = + WifiPickerTrackerInfo( + state = WIFI_STATE_DEFAULT, + isDefault = false, + network = WIFI_NETWORK_DEFAULT, + ) + callbackFlow { + val callback = + object : WifiPickerTracker.WifiPickerTrackerCallback { + override fun onWifiEntriesChanged() { + val connectedEntry = wifiPickerTracker?.connectedWifiEntry + logOnWifiEntriesChanged(connectedEntry) + + // [WifiPickerTracker.connectedWifiEntry] will return the same instance + // but with updated internals. For example, when its validation status + // changes from false to true, the same instance is re-used but with the + // validated field updated. + // + // Because it's the same instance, the flow won't re-emit the value + // (even though the internals have changed). So, we need to transform it + // into our internal model immediately. [toWifiNetworkModel] always + // returns a new instance, so the flow is guaranteed to emit. + send( + newNetwork = connectedEntry?.toWifiNetworkModel() + ?: WIFI_NETWORK_DEFAULT, + newIsDefault = connectedEntry?.isDefaultNetwork ?: false, + ) + } + + override fun onWifiStateChanged() { + val state = wifiPickerTracker?.wifiState + logOnWifiStateChanged(state) + send(newState = state ?: WIFI_STATE_DEFAULT) + } + + override fun onNumSavedNetworksChanged() {} + + override fun onNumSavedSubscriptionsChanged() {} + + private fun send( + newState: Int = current.state, + newIsDefault: Boolean = current.isDefault, + newNetwork: WifiNetworkModel = current.network, + ) { + val new = WifiPickerTrackerInfo(newState, newIsDefault, newNetwork) + current = new + trySend(new) + } + } + + // TODO(b/292591403): [WifiPickerTrackerFactory] currently scans to see all + // available wifi networks every 10s. Because SysUI only needs to display the + // **connected** network, we don't need scans to be running. We should disable these + // scans (ideal) or at least run them very infrequently. + wifiPickerTracker = wifiPickerTrackerFactory.create(lifecycle, callback) + // The lifecycle must be STARTED in order for the callback to receive events. + mainExecutor.execute { lifecycle.currentState = Lifecycle.State.STARTED } + awaitClose { + mainExecutor.execute { lifecycle.currentState = Lifecycle.State.CREATED } + } + } + // TODO(b/292534484): Update to Eagerly once scans are disabled. (Here and other flows) + .stateIn(scope, SharingStarted.WhileSubscribed(), current) + } + + override val isWifiEnabled: StateFlow = + wifiPickerTrackerInfo + .map { it.state == WifiManager.WIFI_STATE_ENABLED } + .distinctUntilChanged() + .logDiffsForTable( + wifiTrackerLibTableLogBuffer, + columnPrefix = "", + columnName = COL_NAME_IS_ENABLED, + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val wifiNetwork: StateFlow = + wifiPickerTrackerInfo + .map { it.network } + .distinctUntilChanged() + .logDiffsForTable( + wifiTrackerLibTableLogBuffer, + columnPrefix = "", + initialValue = WIFI_NETWORK_DEFAULT, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), WIFI_NETWORK_DEFAULT) + + /** Converts WifiTrackerLib's [WifiEntry] into our internal model. */ + private fun WifiEntry.toWifiNetworkModel(): WifiNetworkModel { + if (!this.isPrimaryNetwork) { + return WIFI_NETWORK_DEFAULT + } + return if (this is MergedCarrierEntry) { + WifiNetworkModel.CarrierMerged( + networkId = NETWORK_ID, + // TODO(b/292534484): Fetch the real subscription ID from [MergedCarrierEntry]. + subscriptionId = TEMP_SUB_ID, + level = this.level, + // WifiManager APIs to calculate the signal level start from 0, so + // maxSignalLevel + 1 represents the total level buckets count. + numberOfLevels = wifiManager.maxSignalLevel + 1, + ) + } else { + WifiNetworkModel.Active( + networkId = NETWORK_ID, + isValidated = this.hasInternetAccess(), + level = this.level, + ssid = this.ssid, + // TODO(b/292534484): Fetch the real values from [WifiEntry] (#getTitle might be + // appropriate). + isPasspointAccessPoint = false, + isOnlineSignUpForPasspointAccessPoint = false, + passpointProviderFriendlyName = null, + ) + } + } + + override val isWifiDefault: StateFlow = + wifiPickerTrackerInfo + .map { it.isDefault } + .distinctUntilChanged() + .logDiffsForTable( + wifiTrackerLibTableLogBuffer, + columnPrefix = "", + columnName = COL_NAME_IS_DEFAULT, + initialValue = false, + ) + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + // TODO(b/292534484): Re-use WifiRepositoryImpl code to implement wifi activity since + // WifiTrackerLib doesn't expose activity details. + override val wifiActivity: StateFlow = + MutableStateFlow(DataActivityModel(false, false)) + + private fun logOnWifiEntriesChanged(connectedEntry: WifiEntry?) { + inputLogger.log( + TAG, + LogLevel.DEBUG, + { str1 = connectedEntry.toString() }, + { "onWifiEntriesChanged. ConnectedEntry=$str1" }, + ) + } + + private fun logOnWifiStateChanged(state: Int?) { + inputLogger.log( + TAG, + LogLevel.DEBUG, + { int1 = state ?: -1 }, + { "onWifiStateChanged. State=${if (int1 == -1) null else int1}" }, + ) + } + + /** + * Data class storing all the information fetched from [WifiPickerTracker]. + * + * Used so that we only register a single callback on [WifiPickerTracker]. + */ + data class WifiPickerTrackerInfo( + /** The current wifi state. See [WifiManager.getWifiState]. */ + val state: Int, + /** True if wifi is currently the default connection and false otherwise. */ + val isDefault: Boolean, + /** The currently primary wifi network. */ + val network: WifiNetworkModel, + ) + + @SysUISingleton + class Factory + @Inject + constructor( + @Application private val scope: CoroutineScope, + @Main private val mainExecutor: Executor, + private val wifiPickerTrackerFactory: WifiPickerTrackerFactory, + @WifiTrackerLibInputLog private val inputLogger: LogBuffer, + @WifiTrackerLibTableLog private val wifiTrackerLibTableLogBuffer: TableLogBuffer, + ) { + fun create(wifiManager: WifiManager): WifiRepositoryViaTrackerLib { + return WifiRepositoryViaTrackerLib( + scope, + mainExecutor, + wifiPickerTrackerFactory, + wifiManager, + inputLogger, + wifiTrackerLibTableLogBuffer, + ) + } + } + + companion object { + private const val TAG = "WifiTrackerLibInputLog" + + /** + * [WifiNetworkModel.Active.networkId] is only used at the repository layer. It's used by + * [WifiRepositoryImpl], which tracks the ID in order to correctly apply the framework + * callbacks within the repository. + * + * Since this class does not need to manually apply framework callbacks and since the + * network ID is not used beyond the repository, it's safe to use an invalid ID in this + * repository. + * + * The [WifiNetworkModel.Active.networkId] field should be deleted once we've fully migrated + * to [WifiRepositoryViaTrackerLib]. + */ + private const val NETWORK_ID = -1 + + /** + * A temporary subscription ID until WifiTrackerLib exposes a method to fetch the + * subscription ID. + * + * Use -2 because [SubscriptionManager.INVALID_SUBSCRIPTION_ID] is -1. + */ + private const val TEMP_SUB_ID = -2 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java index e1a7b6d59f413..c2a8e701653a7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java @@ -29,6 +29,7 @@ import com.android.systemui.statusbar.connectivity.AccessPointController; import com.android.systemui.statusbar.connectivity.AccessPointControllerImpl; import com.android.systemui.statusbar.connectivity.NetworkController; import com.android.systemui.statusbar.connectivity.NetworkControllerImpl; +import com.android.systemui.statusbar.connectivity.WifiPickerTrackerFactory; import com.android.systemui.statusbar.phone.ConfigurationControllerImpl; import com.android.systemui.statusbar.policy.BluetoothController; import com.android.systemui.statusbar.policy.BluetoothControllerImpl; @@ -165,7 +166,7 @@ public interface StatusBarPolicyModule { UserManager userManager, UserTracker userTracker, @Main Executor mainExecutor, - AccessPointControllerImpl.WifiPickerTrackerFactory wifiPickerTrackerFactory + WifiPickerTrackerFactory wifiPickerTrackerFactory ) { AccessPointControllerImpl controller = new AccessPointControllerImpl( userManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt index c57b64db2c5ce..5b919d9a6a821 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/AccessPointControllerImplTest.kt @@ -23,6 +23,7 @@ import android.testing.TestableLooper.RunWithLooper import androidx.lifecycle.Lifecycle import com.android.systemui.SysuiTestCase import com.android.systemui.settings.UserTracker +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.wifitrackerlib.WifiEntry import com.android.wifitrackerlib.WifiPickerTracker @@ -31,7 +32,6 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyList import org.mockito.Captor import org.mockito.Mock @@ -52,7 +52,7 @@ class AccessPointControllerImplTest : SysuiTestCase() { private lateinit var userTracker: UserTracker @Mock private lateinit var wifiPickerTrackerFactory: - AccessPointControllerImpl.WifiPickerTrackerFactory + WifiPickerTrackerFactory @Mock private lateinit var wifiPickerTracker: WifiPickerTracker @Mock diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt index 3cf5f5249f1aa..fef042be65a82 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt @@ -36,6 +36,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel @@ -48,23 +49,25 @@ import com.android.systemui.util.concurrency.FakeExecutor 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.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.concurrent.Executor import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations +/** + * Note: Any new tests added here may also need to be added to [WifiRepositoryViaTrackerLibTest]. + */ @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -81,7 +84,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { private lateinit var executor: Executor private lateinit var connectivityRepository: ConnectivityRepository - private val dispatcher = UnconfinedTestDispatcher() + private val dispatcher = StandardTestDispatcher() private val testScope = TestScope(dispatcher) @Before @@ -109,6 +112,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { whenever(wifiManager.isWifiEnabled).thenReturn(true) underTest = createRepo() + testScope.runCurrent() assertThat(underTest.isWifiEnabled.value).isTrue() } @@ -116,53 +120,43 @@ class WifiRepositoryImplTest : SysuiTestCase() { @Test fun isWifiEnabled_networkCapabilitiesChanged_valueUpdated() = testScope.runTest { - // We need to call launch on the flows so that they start updating - val networkJob = underTest.wifiNetwork.launchIn(this) - val enabledJob = underTest.isWifiEnabled.launchIn(this) + collectLastValue(underTest.wifiNetwork) + val latest by collectLastValue(underTest.isWifiEnabled) whenever(wifiManager.isWifiEnabled).thenReturn(true) getNetworkCallback() .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)) - assertThat(underTest.isWifiEnabled.value).isTrue() + assertThat(latest).isTrue() whenever(wifiManager.isWifiEnabled).thenReturn(false) getNetworkCallback() .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)) - assertThat(underTest.isWifiEnabled.value).isFalse() - - networkJob.cancel() - enabledJob.cancel() + assertThat(latest).isFalse() } @Test fun isWifiEnabled_networkLost_valueUpdated() = testScope.runTest { - // We need to call launch on the flows so that they start updating - val networkJob = underTest.wifiNetwork.launchIn(this) - val enabledJob = underTest.isWifiEnabled.launchIn(this) + collectLastValue(underTest.wifiNetwork) + val latest by collectLastValue(underTest.isWifiEnabled) whenever(wifiManager.isWifiEnabled).thenReturn(true) getNetworkCallback().onLost(NETWORK) - assertThat(underTest.isWifiEnabled.value).isTrue() + assertThat(latest).isTrue() whenever(wifiManager.isWifiEnabled).thenReturn(false) getNetworkCallback().onLost(NETWORK) - assertThat(underTest.isWifiEnabled.value).isFalse() - - networkJob.cancel() - enabledJob.cancel() + assertThat(latest).isFalse() } @Test fun isWifiEnabled_intentsReceived_valueUpdated() = testScope.runTest { - underTest = createRepo() - - val job = underTest.isWifiEnabled.launchIn(this) + val latest by collectLastValue(underTest.isWifiEnabled) whenever(wifiManager.isWifiEnabled).thenReturn(true) fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( @@ -170,7 +164,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { Intent(WifiManager.WIFI_STATE_CHANGED_ACTION), ) - assertThat(underTest.isWifiEnabled.value).isTrue() + assertThat(latest).isTrue() whenever(wifiManager.isWifiEnabled).thenReturn(false) fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( @@ -178,76 +172,61 @@ class WifiRepositoryImplTest : SysuiTestCase() { Intent(WifiManager.WIFI_STATE_CHANGED_ACTION), ) - assertThat(underTest.isWifiEnabled.value).isFalse() - - job.cancel() + assertThat(latest).isFalse() } @Test fun isWifiEnabled_bothIntentAndNetworkUpdates_valueAlwaysUpdated() = testScope.runTest { - underTest = createRepo() - - val networkJob = underTest.wifiNetwork.launchIn(this) - val enabledJob = underTest.isWifiEnabled.launchIn(this) + collectLastValue(underTest.wifiNetwork) + val latest by collectLastValue(underTest.isWifiEnabled) whenever(wifiManager.isWifiEnabled).thenReturn(false) fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( context, Intent(WifiManager.WIFI_STATE_CHANGED_ACTION), ) - assertThat(underTest.isWifiEnabled.value).isFalse() + assertThat(latest).isFalse() whenever(wifiManager.isWifiEnabled).thenReturn(true) getNetworkCallback().onLost(NETWORK) - assertThat(underTest.isWifiEnabled.value).isTrue() + assertThat(latest).isTrue() whenever(wifiManager.isWifiEnabled).thenReturn(false) getNetworkCallback() .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)) - assertThat(underTest.isWifiEnabled.value).isFalse() + assertThat(latest).isFalse() whenever(wifiManager.isWifiEnabled).thenReturn(true) fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( context, Intent(WifiManager.WIFI_STATE_CHANGED_ACTION), ) - assertThat(underTest.isWifiEnabled.value).isTrue() - - networkJob.cancel() - enabledJob.cancel() + assertThat(latest).isTrue() } @Test fun isWifiDefault_initiallyGetsDefault() = - testScope.runTest { - val job = underTest.isWifiDefault.launchIn(this) - - assertThat(underTest.isWifiDefault.value).isFalse() - - job.cancel() - } + testScope.runTest { assertThat(underTest.isWifiDefault.value).isFalse() } @Test fun isWifiDefault_wifiNetwork_isTrue() = testScope.runTest { - val job = underTest.isWifiDefault.launchIn(this) + val latest by collectLastValue(underTest.isWifiDefault) val wifiInfo = mock().apply { whenever(this.ssid).thenReturn(SSID) } getDefaultNetworkCallback() .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) - assertThat(underTest.isWifiDefault.value).isTrue() - - job.cancel() + assertThat(latest).isTrue() } /** Regression test for b/266628069. */ @Test fun isWifiDefault_transportInfoIsNotWifi_andNoWifiTransport_false() = testScope.runTest { - val job = underTest.isWifiDefault.launchIn(this) + val latest by collectLastValue(underTest.isWifiDefault) val transportInfo = VpnTransportInfo( @@ -264,16 +243,14 @@ class WifiRepositoryImplTest : SysuiTestCase() { getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, networkCapabilities) - assertThat(underTest.isWifiDefault.value).isFalse() - - job.cancel() + assertThat(latest).isFalse() } /** Regression test for b/266628069. */ @Test fun isWifiDefault_transportInfoIsNotWifi_butHasWifiTransport_true() = testScope.runTest { - val job = underTest.isWifiDefault.launchIn(this) + val latest by collectLastValue(underTest.isWifiDefault) val transportInfo = VpnTransportInfo( @@ -290,15 +267,13 @@ class WifiRepositoryImplTest : SysuiTestCase() { getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, networkCapabilities) - assertThat(underTest.isWifiDefault.value).isTrue() - - job.cancel() + assertThat(latest).isTrue() } @Test fun isWifiDefault_carrierMergedViaCellular_isTrue() = testScope.runTest { - val job = underTest.isWifiDefault.launchIn(this) + val latest by collectLastValue(underTest.isWifiDefault) val carrierMergedInfo = mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } @@ -312,15 +287,13 @@ class WifiRepositoryImplTest : SysuiTestCase() { getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) - assertThat(underTest.isWifiDefault.value).isTrue() - - job.cancel() + assertThat(latest).isTrue() } @Test fun isWifiDefault_carrierMergedViaCellular_withVcnTransport_isTrue() = testScope.runTest { - val job = underTest.isWifiDefault.launchIn(this) + val latest by collectLastValue(underTest.isWifiDefault) val capabilities = mock().apply { @@ -331,15 +304,13 @@ class WifiRepositoryImplTest : SysuiTestCase() { getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) - assertThat(underTest.isWifiDefault.value).isTrue() - - job.cancel() + assertThat(latest).isTrue() } @Test fun isWifiDefault_carrierMergedViaWifi_isTrue() = testScope.runTest { - val job = underTest.isWifiDefault.launchIn(this) + val latest by collectLastValue(underTest.isWifiDefault) val carrierMergedInfo = mock().apply { whenever(this.isCarrierMerged).thenReturn(true) } @@ -353,15 +324,13 @@ class WifiRepositoryImplTest : SysuiTestCase() { getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) - assertThat(underTest.isWifiDefault.value).isTrue() - - job.cancel() + assertThat(latest).isTrue() } @Test fun isWifiDefault_carrierMergedViaWifi_withVcnTransport_isTrue() = testScope.runTest { - val job = underTest.isWifiDefault.launchIn(this) + val latest by collectLastValue(underTest.isWifiDefault) val capabilities = mock().apply { @@ -372,15 +341,13 @@ class WifiRepositoryImplTest : SysuiTestCase() { getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) - assertThat(underTest.isWifiDefault.value).isTrue() - - job.cancel() + assertThat(latest).isTrue() } @Test fun isWifiDefault_cellularAndWifiTransports_usesCellular_isTrue() = testScope.runTest { - val job = underTest.isWifiDefault.launchIn(this) + val latest by collectLastValue(underTest.isWifiDefault) val capabilities = mock().apply { @@ -391,15 +358,13 @@ class WifiRepositoryImplTest : SysuiTestCase() { getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) - assertThat(underTest.isWifiDefault.value).isTrue() - - job.cancel() + assertThat(latest).isTrue() } @Test fun isWifiDefault_cellularNotVcnNetwork_isFalse() = testScope.runTest { - val job = underTest.isWifiDefault.launchIn(this) + val latest by collectLastValue(underTest.isWifiDefault) val capabilities = mock().apply { @@ -409,15 +374,13 @@ class WifiRepositoryImplTest : SysuiTestCase() { getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) - assertThat(underTest.isWifiDefault.value).isFalse() - - job.cancel() + assertThat(latest).isFalse() } @Test fun isWifiDefault_isCarrierMergedViaUnderlyingWifi_isTrue() = testScope.runTest { - val job = underTest.isWifiDefault.launchIn(this) + val latest by collectLastValue(underTest.isWifiDefault) val underlyingNetwork = mock() val carrierMergedInfo = @@ -444,15 +407,13 @@ class WifiRepositoryImplTest : SysuiTestCase() { getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities) // THEN the wifi network is carrier merged, so wifi is default - assertThat(underTest.isWifiDefault.value).isTrue() - - job.cancel() + assertThat(latest).isTrue() } @Test fun isWifiDefault_isCarrierMergedViaUnderlyingCellular_isTrue() = testScope.runTest { - val job = underTest.isWifiDefault.launchIn(this) + val latest by collectLastValue(underTest.isWifiDefault) val underlyingCarrierMergedNetwork = mock() val carrierMergedInfo = @@ -478,46 +439,38 @@ class WifiRepositoryImplTest : SysuiTestCase() { getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities) // THEN the wifi network is carrier merged, so wifi is default - assertThat(underTest.isWifiDefault.value).isTrue() - - job.cancel() + assertThat(latest).isTrue() } @Test fun isWifiDefault_wifiNetworkLost_isFalse() = testScope.runTest { - val job = underTest.isWifiDefault.launchIn(this) + val latest by collectLastValue(underTest.isWifiDefault) // First, add a network getDefaultNetworkCallback() .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)) - assertThat(underTest.isWifiDefault.value).isTrue() + assertThat(latest).isTrue() // WHEN the network is lost getDefaultNetworkCallback().onLost(NETWORK) // THEN we update to false - assertThat(underTest.isWifiDefault.value).isFalse() - - job.cancel() + assertThat(latest).isFalse() } @Test fun wifiNetwork_initiallyGetsDefault() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) assertThat(latest).isEqualTo(WIFI_NETWORK_DEFAULT) - - job.cancel() } @Test fun wifiNetwork_primaryWifiNetworkAdded_flowHasNetwork() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) val wifiInfo = mock().apply { @@ -533,15 +486,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { val latestActive = latest as WifiNetworkModel.Active assertThat(latestActive.networkId).isEqualTo(NETWORK_ID) assertThat(latestActive.ssid).isEqualTo(SSID) - - job.cancel() } @Test fun wifiNetwork_isCarrierMerged_flowHasCarrierMerged() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) val wifiInfo = mock().apply { @@ -553,15 +503,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue() - - job.cancel() } @Test fun wifiNetwork_isCarrierMergedViaUnderlyingWifi_flowHasCarrierMerged() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) val underlyingNetwork = mock() val carrierMergedInfo = @@ -590,15 +537,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { // THEN the wifi network is carrier merged assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue() - - job.cancel() } @Test fun wifiNetwork_isCarrierMergedViaUnderlyingCellular_flowHasCarrierMerged() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) val underlyingCarrierMergedNetwork = mock() val carrierMergedInfo = @@ -628,15 +572,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { // THEN the wifi network is carrier merged assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue() - - job.cancel() } @Test fun wifiNetwork_carrierMergedButInvalidSubId_flowHasInvalid() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) val wifiInfo = mock().apply { @@ -652,15 +593,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { ) assertThat(latest).isInstanceOf(WifiNetworkModel.Invalid::class.java) - - job.cancel() } @Test fun wifiNetwork_isCarrierMerged_getsCorrectValues() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) val rssi = -57 val wifiInfo = @@ -687,15 +625,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { assertThat(latestCarrierMerged.level).isEqualTo(2) // numberOfLevels = maxSignalLevel + 1 assertThat(latestCarrierMerged.numberOfLevels).isEqualTo(6) - - job.cancel() } @Test fun wifiNetwork_notValidated_networkNotValidated() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) getNetworkCallback() .onCapabilitiesChanged( @@ -704,15 +639,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { ) assertThat((latest as WifiNetworkModel.Active).isValidated).isFalse() - - job.cancel() } @Test fun wifiNetwork_validated_networkValidated() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) getNetworkCallback() .onCapabilitiesChanged( @@ -721,15 +653,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { ) assertThat((latest as WifiNetworkModel.Active).isValidated).isTrue() - - job.cancel() } @Test fun wifiNetwork_nonPrimaryWifiNetworkAdded_flowHasNoNetwork() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) val wifiInfo = mock().apply { @@ -741,16 +670,13 @@ class WifiRepositoryImplTest : SysuiTestCase() { .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) assertThat(latest is WifiNetworkModel.Inactive).isTrue() - - job.cancel() } /** Regression test for b/266628069. */ @Test fun wifiNetwork_transportInfoIsNotWifi_flowHasNoNetwork() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) val transportInfo = VpnTransportInfo( @@ -761,15 +687,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(transportInfo)) assertThat(latest is WifiNetworkModel.Inactive).isTrue() - - job.cancel() } @Test fun wifiNetwork_cellularVcnNetworkAdded_flowHasNetwork() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) val capabilities = mock().apply { @@ -783,15 +706,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { val latestActive = latest as WifiNetworkModel.Active assertThat(latestActive.networkId).isEqualTo(NETWORK_ID) assertThat(latestActive.ssid).isEqualTo(SSID) - - job.cancel() } @Test fun wifiNetwork_nonPrimaryCellularVcnNetworkAdded_flowHasNoNetwork() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) val wifiInfo = mock().apply { @@ -807,15 +727,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) assertThat(latest is WifiNetworkModel.Inactive).isTrue() - - job.cancel() } @Test fun wifiNetwork_cellularNotVcnNetworkAdded_flowHasNoNetwork() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) val capabilities = mock().apply { @@ -826,15 +743,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { getNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities) assertThat(latest is WifiNetworkModel.Inactive).isTrue() - - job.cancel() } @Test fun wifiNetwork_cellularAndWifiTransports_usesCellular() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) val capabilities = mock().apply { @@ -849,15 +763,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { val latestActive = latest as WifiNetworkModel.Active assertThat(latestActive.networkId).isEqualTo(NETWORK_ID) assertThat(latestActive.ssid).isEqualTo(SSID) - - job.cancel() } @Test fun wifiNetwork_newPrimaryWifiNetwork_flowHasNewNetwork() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) // Start with the original network getNetworkCallback() @@ -882,15 +793,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { val latestActive = latest as WifiNetworkModel.Active assertThat(latestActive.networkId).isEqualTo(newNetworkId) assertThat(latestActive.ssid).isEqualTo(newSsid) - - job.cancel() } @Test fun wifiNetwork_newNonPrimaryWifiNetwork_flowHasOldNetwork() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) // Start with the original network getNetworkCallback() @@ -915,15 +823,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { val latestActive = latest as WifiNetworkModel.Active assertThat(latestActive.networkId).isEqualTo(NETWORK_ID) assertThat(latestActive.ssid).isEqualTo(SSID) - - job.cancel() } @Test fun wifiNetwork_newNetworkCapabilities_flowHasNewData() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) val wifiInfo = mock().apply { @@ -958,30 +863,24 @@ class WifiRepositoryImplTest : SysuiTestCase() { assertThat(latestActive.networkId).isEqualTo(NETWORK_ID) assertThat(latestActive.ssid).isEqualTo(newSsid) assertThat(latestActive.isValidated).isFalse() - - job.cancel() } @Test fun wifiNetwork_noCurrentNetwork_networkLost_flowHasNoNetwork() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) // WHEN we receive #onLost without any #onCapabilitiesChanged beforehand getNetworkCallback().onLost(NETWORK) // THEN there's no crash and we still have no network assertThat(latest is WifiNetworkModel.Inactive).isTrue() - - job.cancel() } @Test fun wifiNetwork_currentActiveNetworkLost_flowHasNoNetwork() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) getNetworkCallback() .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)) @@ -992,16 +891,13 @@ class WifiRepositoryImplTest : SysuiTestCase() { // THEN we update to no network assertThat(latest is WifiNetworkModel.Inactive).isTrue() - - job.cancel() } /** Possible regression test for b/278618530. */ @Test fun wifiNetwork_currentCarrierMergedNetworkLost_flowHasNoNetwork() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) val wifiInfo = mock().apply { @@ -1019,15 +915,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { // THEN we update to no network assertThat(latest is WifiNetworkModel.Inactive).isTrue() - - job.cancel() } @Test fun wifiNetwork_unknownNetworkLost_flowHasPreviousNetwork() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) getNetworkCallback() .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)) @@ -1042,15 +935,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { val latestActive = latest as WifiNetworkModel.Active assertThat(latestActive.networkId).isEqualTo(NETWORK_ID) assertThat(latestActive.ssid).isEqualTo(SSID) - - job.cancel() } @Test fun wifiNetwork_notCurrentNetworkLost_flowHasCurrentNetwork() = testScope.runTest { - var latest: WifiNetworkModel? = null - val job = underTest.wifiNetwork.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiNetwork) getNetworkCallback() .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)) @@ -1067,16 +957,13 @@ class WifiRepositoryImplTest : SysuiTestCase() { // THEN we still have the new network assertThat((latest as WifiNetworkModel.Active).networkId).isEqualTo(newNetworkId) - - job.cancel() } /** Regression test for b/244173280. */ @Test fun wifiNetwork_multipleSubscribers_newSubscribersGetCurrentValue() = testScope.runTest { - var latest1: WifiNetworkModel? = null - val job1 = underTest.wifiNetwork.onEach { latest1 = it }.launchIn(this) + val latest1 by collectLastValue(underTest.wifiNetwork) getNetworkCallback() .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)) @@ -1087,23 +974,19 @@ class WifiRepositoryImplTest : SysuiTestCase() { assertThat(latest1Active.ssid).isEqualTo(SSID) // WHEN we add a second subscriber after having already emitted a value - var latest2: WifiNetworkModel? = null - val job2 = underTest.wifiNetwork.onEach { latest2 = it }.launchIn(this) + val latest2 by collectLastValue(underTest.wifiNetwork) // THEN the second subscribe receives the already-emitted value assertThat(latest2 is WifiNetworkModel.Active).isTrue() val latest2Active = latest2 as WifiNetworkModel.Active assertThat(latest2Active.networkId).isEqualTo(NETWORK_ID) assertThat(latest2Active.ssid).isEqualTo(SSID) - - job1.cancel() - job2.cancel() } @Test fun isWifiConnectedWithValidSsid_inactiveNetwork_false() = testScope.runTest { - val job = underTest.wifiNetwork.launchIn(this) + collectLastValue(underTest.wifiNetwork) val wifiInfo = mock().apply { @@ -1116,14 +999,12 @@ class WifiRepositoryImplTest : SysuiTestCase() { .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() - - job.cancel() } @Test fun isWifiConnectedWithValidSsid_carrierMergedNetwork_false() = testScope.runTest { - val job = underTest.wifiNetwork.launchIn(this) + collectLastValue(underTest.wifiNetwork) val wifiInfo = mock().apply { @@ -1133,16 +1014,15 @@ class WifiRepositoryImplTest : SysuiTestCase() { getNetworkCallback() .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + testScope.runCurrent() assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() - - job.cancel() } @Test fun isWifiConnectedWithValidSsid_invalidNetwork_false() = testScope.runTest { - val job = underTest.wifiNetwork.launchIn(this) + collectLastValue(underTest.wifiNetwork) val wifiInfo = mock().apply { @@ -1156,16 +1036,15 @@ class WifiRepositoryImplTest : SysuiTestCase() { NETWORK, createWifiNetworkCapabilities(wifiInfo), ) + testScope.runCurrent() assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() - - job.cancel() } @Test fun isWifiConnectedWithValidSsid_activeNetwork_nullSsid_false() = testScope.runTest { - val job = underTest.wifiNetwork.launchIn(this) + collectLastValue(underTest.wifiNetwork) val wifiInfo = mock().apply { @@ -1175,16 +1054,15 @@ class WifiRepositoryImplTest : SysuiTestCase() { getNetworkCallback() .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + testScope.runCurrent() assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() - - job.cancel() } @Test fun isWifiConnectedWithValidSsid_activeNetwork_unknownSsid_false() = testScope.runTest { - val job = underTest.wifiNetwork.launchIn(this) + collectLastValue(underTest.wifiNetwork) val wifiInfo = mock().apply { @@ -1194,16 +1072,15 @@ class WifiRepositoryImplTest : SysuiTestCase() { getNetworkCallback() .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + testScope.runCurrent() assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() - - job.cancel() } @Test fun isWifiConnectedWithValidSsid_activeNetwork_validSsid_true() = testScope.runTest { - val job = underTest.wifiNetwork.launchIn(this) + collectLastValue(underTest.wifiNetwork) val wifiInfo = mock().apply { @@ -1213,16 +1090,15 @@ class WifiRepositoryImplTest : SysuiTestCase() { getNetworkCallback() .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + testScope.runCurrent() assertThat(underTest.isWifiConnectedWithValidSsid()).isTrue() - - job.cancel() } @Test fun isWifiConnectedWithValidSsid_activeToInactive_trueToFalse() = testScope.runTest { - val job = underTest.wifiNetwork.launchIn(this) + collectLastValue(underTest.wifiNetwork) // Start with active val wifiInfo = @@ -1232,71 +1108,60 @@ class WifiRepositoryImplTest : SysuiTestCase() { } getNetworkCallback() .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo)) + testScope.runCurrent() + assertThat(underTest.isWifiConnectedWithValidSsid()).isTrue() // WHEN the network is lost getNetworkCallback().onLost(NETWORK) + testScope.runCurrent() // THEN the isWifiConnected updates assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() - - job.cancel() } @Test fun wifiActivity_callbackGivesNone_activityFlowHasNone() = testScope.runTest { - var latest: DataActivityModel? = null - val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiActivity) getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_NONE) assertThat(latest) .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) - - job.cancel() } @Test fun wifiActivity_callbackGivesIn_activityFlowHasIn() = testScope.runTest { - var latest: DataActivityModel? = null - val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiActivity) getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_IN) assertThat(latest) .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = false)) - - job.cancel() } @Test fun wifiActivity_callbackGivesOut_activityFlowHasOut() = testScope.runTest { - var latest: DataActivityModel? = null - val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiActivity) getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_OUT) assertThat(latest) .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = true)) - - job.cancel() } @Test fun wifiActivity_callbackGivesInout_activityFlowHasInAndOut() = testScope.runTest { - var latest: DataActivityModel? = null - val job = underTest.wifiActivity.onEach { latest = it }.launchIn(this) + val latest by collectLastValue(underTest.wifiActivity) getTrafficStateCallback().onStateChanged(TrafficStateCallback.DATA_ACTIVITY_INOUT) assertThat(latest) .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = true)) - - job.cancel() } private fun createRepo(): WifiRepositoryImpl { @@ -1314,18 +1179,21 @@ class WifiRepositoryImplTest : SysuiTestCase() { } private fun getTrafficStateCallback(): TrafficStateCallback { + testScope.runCurrent() val callbackCaptor = argumentCaptor() verify(wifiManager).registerTrafficStateCallback(any(), callbackCaptor.capture()) return callbackCaptor.value!! } private fun getNetworkCallback(): ConnectivityManager.NetworkCallback { + testScope.runCurrent() val callbackCaptor = argumentCaptor() verify(connectivityManager).registerNetworkCallback(any(), callbackCaptor.capture()) return callbackCaptor.value!! } private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback { + testScope.runCurrent() val callbackCaptor = argumentCaptor() verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture()) return callbackCaptor.value!! diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLibTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLibTest.kt new file mode 100644 index 0000000000000..7002cbb6ab211 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLibTest.kt @@ -0,0 +1,665 @@ +/* + * 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.wifi.data.repository.prod + +import android.net.wifi.WifiManager +import android.net.wifi.WifiManager.UNKNOWN_SSID +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.connectivity.WifiPickerTrackerFactory +import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT +import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.android.wifitrackerlib.MergedCarrierEntry +import com.android.wifitrackerlib.WifiEntry +import com.android.wifitrackerlib.WifiPickerTracker +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +/** + * Note: Most of these tests are duplicates of [WifiRepositoryImplTest] tests. + * + * Any new tests added here may also need to be added to [WifiRepositoryImplTest]. + */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class WifiRepositoryViaTrackerLibTest : SysuiTestCase() { + + private lateinit var underTest: WifiRepositoryViaTrackerLib + + private val executor = FakeExecutor(FakeSystemClock()) + private val logger = LogBuffer("name", maxSize = 100, logcatEchoTracker = mock()) + private val tableLogger = mock() + private val wifiManager = + mock().apply { whenever(this.maxSignalLevel).thenReturn(10) } + private val wifiPickerTrackerFactory = mock() + private val wifiPickerTracker = mock() + + private val callbackCaptor = argumentCaptor() + + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + + @Before + fun setUp() { + whenever(wifiPickerTrackerFactory.create(any(), capture(callbackCaptor))) + .thenReturn(wifiPickerTracker) + underTest = createRepo() + } + + @Test + fun isWifiEnabled_enabled_true() = + testScope.runTest { + val latest by collectLastValue(underTest.isWifiEnabled) + + whenever(wifiPickerTracker.wifiState).thenReturn(WifiManager.WIFI_STATE_ENABLED) + getCallback().onWifiStateChanged() + + assertThat(latest).isTrue() + } + + @Test + fun isWifiEnabled_enabling_false() = + testScope.runTest { + val latest by collectLastValue(underTest.isWifiEnabled) + + whenever(wifiPickerTracker.wifiState).thenReturn(WifiManager.WIFI_STATE_ENABLING) + getCallback().onWifiStateChanged() + + assertThat(latest).isFalse() + } + + @Test + fun isWifiEnabled_disabling_true() = + testScope.runTest { + val latest by collectLastValue(underTest.isWifiEnabled) + + whenever(wifiPickerTracker.wifiState).thenReturn(WifiManager.WIFI_STATE_DISABLING) + getCallback().onWifiStateChanged() + + assertThat(latest).isFalse() + } + + @Test + fun isWifiEnabled_disabled_false() = + testScope.runTest { + val latest by collectLastValue(underTest.isWifiEnabled) + + whenever(wifiPickerTracker.wifiState).thenReturn(WifiManager.WIFI_STATE_DISABLED) + getCallback().onWifiStateChanged() + + assertThat(latest).isFalse() + } + + @Test + fun isWifiEnabled_respondsToUpdates() = + testScope.runTest { + val latest by collectLastValue(underTest.isWifiEnabled) + executor.runAllReady() + + whenever(wifiPickerTracker.wifiState).thenReturn(WifiManager.WIFI_STATE_ENABLED) + getCallback().onWifiStateChanged() + + assertThat(latest).isTrue() + + whenever(wifiPickerTracker.wifiState).thenReturn(WifiManager.WIFI_STATE_DISABLED) + getCallback().onWifiStateChanged() + + assertThat(latest).isFalse() + } + + @Test + fun isWifiDefault_initiallyGetsDefault() = + testScope.runTest { assertThat(underTest.isWifiDefault.value).isFalse() } + + @Test + fun isWifiDefault_wifiNetwork_isTrue() = + testScope.runTest { + val latest by collectLastValue(underTest.isWifiDefault) + + val wifiEntry = + mock().apply { whenever(this.isDefaultNetwork).thenReturn(true) } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + + assertThat(latest).isTrue() + } + + @Test + fun isWifiDefault_carrierMerged_isTrue() = + testScope.runTest { + val latest by collectLastValue(underTest.isWifiDefault) + + val wifiEntry = + mock().apply { + whenever(this.isDefaultNetwork).thenReturn(true) + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + + assertThat(latest).isTrue() + } + + @Test + fun isWifiDefault_wifiNetworkNotDefault_isFalse() = + testScope.runTest { + val latest by collectLastValue(underTest.isWifiDefault) + + val wifiEntry = + mock().apply { whenever(this.isDefaultNetwork).thenReturn(false) } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + + assertThat(latest).isFalse() + } + + @Test + fun isWifiDefault_carrierMergedNotDefault_isFalse() = + testScope.runTest { + val latest by collectLastValue(underTest.isWifiDefault) + + val wifiEntry = + mock().apply { + whenever(this.isDefaultNetwork).thenReturn(false) + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + + assertThat(latest).isFalse() + } + + @Test + fun isWifiDefault_noWifiNetwork_isFalse() = + testScope.runTest { + val latest by collectLastValue(underTest.isWifiDefault) + + // First, add a network + val wifiEntry = + mock().apply { whenever(this.isDefaultNetwork).thenReturn(true) } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + + assertThat(latest).isTrue() + + // WHEN the network is lost + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null) + getCallback().onWifiEntriesChanged() + + // THEN we update to false + assertThat(latest).isFalse() + } + + @Test + fun wifiNetwork_initiallyGetsDefault() = + testScope.runTest { + val latest by collectLastValue(underTest.wifiNetwork) + + assertThat(latest).isEqualTo(WIFI_NETWORK_DEFAULT) + } + + @Test + fun wifiNetwork_primaryWifiNetworkAdded_flowHasNetwork() = + testScope.runTest { + val latest by collectLastValue(underTest.wifiNetwork) + + val wifiEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.level).thenReturn(3) + whenever(this.ssid).thenReturn(SSID) + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + + assertThat(latest is WifiNetworkModel.Active).isTrue() + val latestActive = latest as WifiNetworkModel.Active + assertThat(latestActive.level).isEqualTo(3) + assertThat(latestActive.ssid).isEqualTo(SSID) + } + + @Test + fun wifiNetwork_isCarrierMerged_flowHasCarrierMerged() = + testScope.runTest { + val latest by collectLastValue(underTest.wifiNetwork) + + val wifiEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.level).thenReturn(3) + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + + assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue() + val latestMerged = latest as WifiNetworkModel.CarrierMerged + assertThat(latestMerged.level).isEqualTo(3) + // numberOfLevels = maxSignalLevel + 1 + } + + @Test + fun wifiNetwork_isCarrierMerged_getsMaxSignalLevel() = + testScope.runTest { + val latest by collectLastValue(underTest.wifiNetwork) + + val wifiEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + whenever(wifiManager.maxSignalLevel).thenReturn(5) + + getCallback().onWifiEntriesChanged() + + assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue() + val latestMerged = latest as WifiNetworkModel.CarrierMerged + // numberOfLevels = maxSignalLevel + 1 + assertThat(latestMerged.numberOfLevels).isEqualTo(6) + } + + /* TODO(b/292534484): Re-enable this test once WifiTrackerLib gives us the subscription ID. + @Test + fun wifiNetwork_carrierMergedButInvalidSubId_flowHasInvalid() = + testScope.runTest { + val latest by collectLastValue(underTest.wifiNetwork) + + val wifiInfo = + mock().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.isCarrierMerged).thenReturn(true) + whenever(this.subscriptionId).thenReturn(INVALID_SUBSCRIPTION_ID) + } + + getNetworkCallback() + .onCapabilitiesChanged( + NETWORK, + createWifiNetworkCapabilities(wifiInfo), + ) + + assertThat(latest).isInstanceOf(WifiNetworkModel.Invalid::class.java) + } + + */ + + @Test + fun wifiNetwork_notValidated_networkNotValidated() = + testScope.runTest { + val latest by collectLastValue(underTest.wifiNetwork) + + val wifiEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.hasInternetAccess()).thenReturn(false) + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + + assertThat((latest as WifiNetworkModel.Active).isValidated).isFalse() + } + + @Test + fun wifiNetwork_validated_networkValidated() = + testScope.runTest { + val latest by collectLastValue(underTest.wifiNetwork) + + val wifiEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.hasInternetAccess()).thenReturn(true) + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + + assertThat((latest as WifiNetworkModel.Active).isValidated).isTrue() + } + + @Test + fun wifiNetwork_nonPrimaryWifiNetworkAdded_flowHasNoNetwork() = + testScope.runTest { + val latest by collectLastValue(underTest.wifiNetwork) + + val wifiEntry = + mock().apply { whenever(this.isPrimaryNetwork).thenReturn(false) } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + + assertThat(latest).isEqualTo(WifiNetworkModel.Inactive) + } + + @Test + fun wifiNetwork_nonPrimaryCarrierMergedNetworkAdded_flowHasNoNetwork() = + testScope.runTest { + val latest by collectLastValue(underTest.wifiNetwork) + + val wifiEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(false) + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + + assertThat(latest).isEqualTo(WifiNetworkModel.Inactive) + } + + @Test + fun wifiNetwork_newPrimaryWifiNetwork_flowHasNewNetwork() = + testScope.runTest { + val latest by collectLastValue(underTest.wifiNetwork) + + // Start with the original network + val wifiEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.level).thenReturn(3) + whenever(this.ssid).thenReturn("AB") + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + + assertThat(latest is WifiNetworkModel.Active).isTrue() + var latestActive = latest as WifiNetworkModel.Active + assertThat(latestActive.level).isEqualTo(3) + assertThat(latestActive.ssid).isEqualTo("AB") + + // WHEN we update to a new primary network + val newWifiEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.level).thenReturn(4) + whenever(this.ssid).thenReturn("CD") + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(newWifiEntry) + getCallback().onWifiEntriesChanged() + + // THEN we use the new network + assertThat(latest is WifiNetworkModel.Active).isTrue() + latestActive = latest as WifiNetworkModel.Active + assertThat(latestActive.level).isEqualTo(4) + assertThat(latestActive.ssid).isEqualTo("CD") + } + + @Test + fun wifiNetwork_noCurrentNetwork_networkLost_flowHasNoNetwork() = + testScope.runTest { + val latest by collectLastValue(underTest.wifiNetwork) + + // WHEN we receive a null network without any networks beforehand + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null) + getCallback().onWifiEntriesChanged() + + // THEN there's no crash and we still have no network + assertThat(latest is WifiNetworkModel.Inactive).isTrue() + } + + @Test + fun wifiNetwork_currentActiveNetworkLost_flowHasNoNetwork() = + testScope.runTest { + val latest by collectLastValue(underTest.wifiNetwork) + + val wifiEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.ssid).thenReturn(SSID) + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + + assertThat((latest as WifiNetworkModel.Active).ssid).isEqualTo(SSID) + + // WHEN we lose our current network + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null) + getCallback().onWifiEntriesChanged() + + // THEN we update to no network + assertThat(latest is WifiNetworkModel.Inactive).isTrue() + } + + /** Possible regression test for b/278618530. */ + @Test + fun wifiNetwork_currentCarrierMergedNetworkLost_flowHasNoNetwork() = + testScope.runTest { + val latest by collectLastValue(underTest.wifiNetwork) + + val wifiEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.level).thenReturn(3) + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + + assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue() + assertThat((latest as WifiNetworkModel.CarrierMerged).level).isEqualTo(3) + + // WHEN we lose our current network + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null) + getCallback().onWifiEntriesChanged() + + // THEN we update to no network + assertThat(latest is WifiNetworkModel.Inactive).isTrue() + } + + /** Regression test for b/244173280. */ + @Test + fun wifiNetwork_multipleSubscribers_newSubscribersGetCurrentValue() = + testScope.runTest { + val latest1 by collectLastValue(underTest.wifiNetwork) + + val wifiEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.level).thenReturn(1) + whenever(this.ssid).thenReturn(SSID) + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + + assertThat(latest1 is WifiNetworkModel.Active).isTrue() + val latest1Active = latest1 as WifiNetworkModel.Active + assertThat(latest1Active.level).isEqualTo(1) + assertThat(latest1Active.ssid).isEqualTo(SSID) + + // WHEN we add a second subscriber after having already emitted a value + val latest2 by collectLastValue(underTest.wifiNetwork) + + // THEN the second subscribe receives the already-emitted value + assertThat(latest2 is WifiNetworkModel.Active).isTrue() + val latest2Active = latest2 as WifiNetworkModel.Active + assertThat(latest2Active.level).isEqualTo(1) + assertThat(latest2Active.ssid).isEqualTo(SSID) + } + + @Test + fun isWifiConnectedWithValidSsid_inactiveNetwork_false() = + testScope.runTest { + collectLastValue(underTest.wifiNetwork) + + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null) + getCallback().onWifiEntriesChanged() + + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + } + + @Test + fun isWifiConnectedWithValidSsid_nonPrimaryNetwork_false() = + testScope.runTest { + collectLastValue(underTest.wifiNetwork) + + val wifiEntry = + mock().apply { whenever(this.isPrimaryNetwork).thenReturn(false) } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + testScope.runCurrent() + + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + } + + @Test + fun isWifiConnectedWithValidSsid_carrierMergedNetwork_false() = + testScope.runTest { + collectLastValue(underTest.wifiNetwork) + + val wifiEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + testScope.runCurrent() + + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + } + + /* TODO(b/292534484): Re-enable this test once WifiTrackerLib gives us the subscription ID. + @Test + fun isWifiConnectedWithValidSsid_invalidNetwork_false() = + testScope.runTest { + collectLastValue(underTest.wifiNetwork) + + val wifiInfo = + mock().apply { + whenever(this.isPrimary).thenReturn(true) + whenever(this.isCarrierMerged).thenReturn(true) + whenever(this.subscriptionId).thenReturn(INVALID_SUBSCRIPTION_ID) + } + + getNetworkCallback() + .onCapabilitiesChanged( + NETWORK, + createWifiNetworkCapabilities(wifiInfo), + ) + testScope.runCurrent() + + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + } + + */ + + @Test + fun isWifiConnectedWithValidSsid_activeNetwork_nullSsid_false() = + testScope.runTest { + collectLastValue(underTest.wifiNetwork) + + val wifiEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.ssid).thenReturn(null) + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + testScope.runCurrent() + + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + } + + @Test + fun isWifiConnectedWithValidSsid_activeNetwork_unknownSsid_false() = + testScope.runTest { + collectLastValue(underTest.wifiNetwork) + + val wifiEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.ssid).thenReturn(UNKNOWN_SSID) + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + testScope.runCurrent() + + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + } + + @Test + fun isWifiConnectedWithValidSsid_activeNetwork_validSsid_true() = + testScope.runTest { + collectLastValue(underTest.wifiNetwork) + + val wifiEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.ssid).thenReturn("fakeSsid") + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + testScope.runCurrent() + + assertThat(underTest.isWifiConnectedWithValidSsid()).isTrue() + } + + @Test + fun isWifiConnectedWithValidSsid_activeToInactive_trueToFalse() = + testScope.runTest { + collectLastValue(underTest.wifiNetwork) + + // Start with active + val wifiEntry = + mock().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.ssid).thenReturn("fakeSsid") + } + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + getCallback().onWifiEntriesChanged() + testScope.runCurrent() + + assertThat(underTest.isWifiConnectedWithValidSsid()).isTrue() + + // WHEN the network is lost + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null) + getCallback().onWifiEntriesChanged() + testScope.runCurrent() + + // THEN the isWifiConnected updates + assertThat(underTest.isWifiConnectedWithValidSsid()).isFalse() + } + + private fun getCallback(): WifiPickerTracker.WifiPickerTrackerCallback { + testScope.runCurrent() + return callbackCaptor.value + } + + private fun createRepo(): WifiRepositoryViaTrackerLib { + return WifiRepositoryViaTrackerLib( + testScope.backgroundScope, + executor, + wifiPickerTrackerFactory, + wifiManager, + logger, + tableLogger, + ) + } + + private companion object { + const val SSID = "AB" + } +}