Merge changes from topic "demo-mode-flow" into tm-qpr-dev

* changes:
  [Sb refactor] Wifi demo mode
  [Sb refactor] Add a demo command flow method
This commit is contained in:
Evan Laird
2022-12-22 20:23:46 +00:00
committed by Android (Google) Code Review
16 changed files with 772 additions and 127 deletions

View File

@@ -20,7 +20,7 @@ import android.content.Context
import android.database.ContentObserver import android.database.ContentObserver
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.Settings import com.android.systemui.util.settings.GlobalSettings
/** /**
* Class to track the availability of [DemoMode]. Use this class to track the availability and * Class to track the availability of [DemoMode]. Use this class to track the availability and
@@ -29,7 +29,10 @@ import android.provider.Settings
* This class works by wrapping a content observer for the relevant keys related to DemoMode * This class works by wrapping a content observer for the relevant keys related to DemoMode
* availability and current on/off state, and triggering callbacks. * availability and current on/off state, and triggering callbacks.
*/ */
abstract class DemoModeAvailabilityTracker(val context: Context) { abstract class DemoModeAvailabilityTracker(
val context: Context,
val globalSettings: GlobalSettings,
) {
var isInDemoMode = false var isInDemoMode = false
var isDemoModeAvailable = false var isDemoModeAvailable = false
@@ -41,9 +44,9 @@ abstract class DemoModeAvailabilityTracker(val context: Context) {
fun startTracking() { fun startTracking() {
val resolver = context.contentResolver val resolver = context.contentResolver
resolver.registerContentObserver( resolver.registerContentObserver(
Settings.Global.getUriFor(DEMO_MODE_ALLOWED), false, allowedObserver) globalSettings.getUriFor(DEMO_MODE_ALLOWED), false, allowedObserver)
resolver.registerContentObserver( resolver.registerContentObserver(
Settings.Global.getUriFor(DEMO_MODE_ON), false, onObserver) globalSettings.getUriFor(DEMO_MODE_ON), false, onObserver)
} }
fun stopTracking() { fun stopTracking() {
@@ -57,12 +60,11 @@ abstract class DemoModeAvailabilityTracker(val context: Context) {
abstract fun onDemoModeFinished() abstract fun onDemoModeFinished()
private fun checkIsDemoModeAllowed(): Boolean { private fun checkIsDemoModeAllowed(): Boolean {
return Settings.Global return globalSettings.getInt(DEMO_MODE_ALLOWED, 0) != 0
.getInt(context.contentResolver, DEMO_MODE_ALLOWED, 0) != 0
} }
private fun checkIsDemoModeOn(): Boolean { private fun checkIsDemoModeOn(): Boolean {
return Settings.Global.getInt(context.contentResolver, DEMO_MODE_ON, 0) != 0 return globalSettings.getInt(DEMO_MODE_ON, 0) != 0
} }
private val allowedObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { private val allowedObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {

View File

@@ -24,22 +24,28 @@ import android.os.Bundle
import android.os.UserHandle import android.os.UserHandle
import android.util.Log import android.util.Log
import com.android.systemui.Dumpable import com.android.systemui.Dumpable
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.demomode.DemoMode.ACTION_DEMO import com.android.systemui.demomode.DemoMode.ACTION_DEMO
import com.android.systemui.dump.DumpManager import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.policy.CallbackController import com.android.systemui.statusbar.policy.CallbackController
import com.android.systemui.util.Assert import com.android.systemui.util.Assert
import com.android.systemui.util.settings.GlobalSettings import com.android.systemui.util.settings.GlobalSettings
import java.io.PrintWriter import java.io.PrintWriter
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
/** /**
* Handles system broadcasts for [DemoMode] * Handles system broadcasts for [DemoMode]
* *
* Injected via [DemoModeModule] * Injected via [DemoModeModule]
*/ */
class DemoModeController constructor( class DemoModeController
constructor(
private val context: Context, private val context: Context,
private val dumpManager: DumpManager, private val dumpManager: DumpManager,
private val globalSettings: GlobalSettings private val globalSettings: GlobalSettings,
private val broadcastDispatcher: BroadcastDispatcher,
) : CallbackController<DemoMode>, Dumpable { ) : CallbackController<DemoMode>, Dumpable {
// Var updated when the availability tracker changes, or when we enter/exit demo mode in-process // Var updated when the availability tracker changes, or when we enter/exit demo mode in-process
@@ -58,9 +64,7 @@ class DemoModeController constructor(
requestFinishDemoMode() requestFinishDemoMode()
val m = mutableMapOf<String, MutableList<DemoMode>>() val m = mutableMapOf<String, MutableList<DemoMode>>()
DemoMode.COMMANDS.map { command -> DemoMode.COMMANDS.map { command -> m.put(command, mutableListOf()) }
m.put(command, mutableListOf())
}
receiverMap = m receiverMap = m
} }
@@ -71,7 +75,7 @@ class DemoModeController constructor(
initialized = true initialized = true
dumpManager.registerDumpable(TAG, this) dumpManager.registerNormalDumpable(TAG, this)
// Due to DemoModeFragment running in systemui:tuner process, we have to observe for // Due to DemoModeFragment running in systemui:tuner process, we have to observe for
// content changes to know if the setting turned on or off // content changes to know if the setting turned on or off
@@ -81,8 +85,13 @@ class DemoModeController constructor(
val demoFilter = IntentFilter() val demoFilter = IntentFilter()
demoFilter.addAction(ACTION_DEMO) demoFilter.addAction(ACTION_DEMO)
context.registerReceiverAsUser(broadcastReceiver, UserHandle.ALL, demoFilter,
android.Manifest.permission.DUMP, null, Context.RECEIVER_EXPORTED) broadcastDispatcher.registerReceiver(
receiver = broadcastReceiver,
filter = demoFilter,
user = UserHandle.ALL,
permission = android.Manifest.permission.DUMP,
)
} }
override fun addCallback(listener: DemoMode) { override fun addCallback(listener: DemoMode) {
@@ -91,16 +100,15 @@ class DemoModeController constructor(
commands.forEach { command -> commands.forEach { command ->
if (!receiverMap.containsKey(command)) { if (!receiverMap.containsKey(command)) {
throw IllegalStateException("Command ($command) not recognized. " + throw IllegalStateException(
"See DemoMode.java for valid commands") "Command ($command) not recognized. " + "See DemoMode.java for valid commands"
)
} }
receiverMap[command]!!.add(listener) receiverMap[command]!!.add(listener)
} }
synchronized(this) { synchronized(this) { receivers.add(listener) }
receivers.add(listener)
}
if (isInDemoMode) { if (isInDemoMode) {
listener.onDemoModeStarted() listener.onDemoModeStarted()
@@ -109,14 +117,46 @@ class DemoModeController constructor(
override fun removeCallback(listener: DemoMode) { override fun removeCallback(listener: DemoMode) {
synchronized(this) { synchronized(this) {
listener.demoCommands().forEach { command -> listener.demoCommands().forEach { command -> receiverMap[command]!!.remove(listener) }
receiverMap[command]!!.remove(listener)
}
receivers.remove(listener) receivers.remove(listener)
} }
} }
/**
* Create a [Flow] for the stream of demo mode arguments that come in for the given [command]
*
* This is equivalent of creating a listener manually and adding an event handler for the given
* command, like so:
*
* ```
* class Demoable {
* private val demoHandler = object : DemoMode {
* override fun demoCommands() = listOf(<command>)
*
* override fun dispatchDemoCommand(command: String, args: Bundle) {
* handleDemoCommand(args)
* }
* }
* }
* ```
*
* @param command The top-level demo mode command you want a stream for
*/
fun demoFlowForCommand(command: String): Flow<Bundle> = conflatedCallbackFlow {
val callback =
object : DemoMode {
override fun demoCommands(): List<String> = listOf(command)
override fun dispatchDemoCommand(command: String, args: Bundle) {
trySend(args)
}
}
addCallback(callback)
awaitClose { removeCallback(callback) }
}
private fun setIsDemoModeAllowed(enabled: Boolean) { private fun setIsDemoModeAllowed(enabled: Boolean) {
// Turn off demo mode if it was on // Turn off demo mode if it was on
if (isInDemoMode && !enabled) { if (isInDemoMode && !enabled) {
@@ -129,13 +169,9 @@ class DemoModeController constructor(
Assert.isMainThread() Assert.isMainThread()
val copy: List<DemoModeCommandReceiver> val copy: List<DemoModeCommandReceiver>
synchronized(this) { synchronized(this) { copy = receivers.toList() }
copy = receivers.toList()
}
copy.forEach { r -> copy.forEach { r -> r.onDemoModeStarted() }
r.onDemoModeStarted()
}
} }
private fun exitDemoMode() { private fun exitDemoMode() {
@@ -143,18 +179,13 @@ class DemoModeController constructor(
Assert.isMainThread() Assert.isMainThread()
val copy: List<DemoModeCommandReceiver> val copy: List<DemoModeCommandReceiver>
synchronized(this) { synchronized(this) { copy = receivers.toList() }
copy = receivers.toList()
}
copy.forEach { r -> copy.forEach { r -> r.onDemoModeFinished() }
r.onDemoModeFinished()
}
} }
fun dispatchDemoCommand(command: String, args: Bundle) { fun dispatchDemoCommand(command: String, args: Bundle) {
Assert.isMainThread() Assert.isMainThread()
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "dispatchDemoCommand: $command, args=$args") Log.d(TAG, "dispatchDemoCommand: $command, args=$args")
} }
@@ -172,9 +203,7 @@ class DemoModeController constructor(
} }
// See? demo mode is easy now, you just notify the listeners when their command is called // See? demo mode is easy now, you just notify the listeners when their command is called
receiverMap[command]!!.forEach { receiver -> receiverMap[command]!!.forEach { receiver -> receiver.dispatchDemoCommand(command, args) }
receiver.dispatchDemoCommand(command, args)
}
} }
override fun dump(pw: PrintWriter, args: Array<out String>) { override fun dump(pw: PrintWriter, args: Array<out String>) {
@@ -183,24 +212,22 @@ class DemoModeController constructor(
pw.println(" isDemoModeAllowed=$isAvailable") pw.println(" isDemoModeAllowed=$isAvailable")
pw.print(" receivers=[") pw.print(" receivers=[")
val copy: List<DemoModeCommandReceiver> val copy: List<DemoModeCommandReceiver>
synchronized(this) { synchronized(this) { copy = receivers.toList() }
copy = receivers.toList() copy.forEach { recv -> pw.print(" ${recv.javaClass.simpleName}") }
}
copy.forEach { recv ->
pw.print(" ${recv.javaClass.simpleName}")
}
pw.println(" ]") pw.println(" ]")
pw.println(" receiverMap= [") pw.println(" receiverMap= [")
receiverMap.keys.forEach { command -> receiverMap.keys.forEach { command ->
pw.print(" $command : [") pw.print(" $command : [")
val recvs = receiverMap[command]!!.map { receiver -> val recvs =
receiver.javaClass.simpleName receiverMap[command]!!
}.joinToString(",") .map { receiver -> receiver.javaClass.simpleName }
.joinToString(",")
pw.println("$recvs ]") pw.println("$recvs ]")
} }
} }
private val tracker = object : DemoModeAvailabilityTracker(context) { private val tracker =
object : DemoModeAvailabilityTracker(context, globalSettings) {
override fun onDemoModeAvailabilityChanged() { override fun onDemoModeAvailabilityChanged() {
setIsDemoModeAllowed(isDemoModeAvailable) setIsDemoModeAllowed(isDemoModeAvailable)
} }
@@ -218,7 +245,8 @@ class DemoModeController constructor(
} }
} }
private val broadcastReceiver = object : BroadcastReceiver() { private val broadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (DEBUG) { if (DEBUG) {
Log.v(TAG, "onReceive: $intent") Log.v(TAG, "onReceive: $intent")
@@ -230,8 +258,8 @@ class DemoModeController constructor(
} }
val bundle = intent.extras ?: return val bundle = intent.extras ?: return
val command = bundle.getString("command", "").trim().toLowerCase() val command = bundle.getString("command", "").trim().lowercase()
if (command.length == 0) { if (command.isEmpty()) {
return return
} }
@@ -258,10 +286,12 @@ class DemoModeController constructor(
private fun setGlobal(key: String, value: Int) { private fun setGlobal(key: String, value: Int) {
globalSettings.putInt(key, value) globalSettings.putInt(key, value)
} }
companion object {
const val DEMO_MODE_ALLOWED = "sysui_demo_allowed"
const val DEMO_MODE_ON = "sysui_tuner_demo_on"
}
} }
private const val TAG = "DemoModeController" private const val TAG = "DemoModeController"
private const val DEMO_MODE_ALLOWED = "sysui_demo_allowed"
private const val DEMO_MODE_ON = "sysui_tuner_demo_on"
private const val DEBUG = false private const val DEBUG = false

View File

@@ -18,6 +18,7 @@ package com.android.systemui.demomode.dagger;
import android.content.Context; import android.content.Context;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.demomode.DemoModeController; import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.dump.DumpManager; import com.android.systemui.dump.DumpManager;
@@ -37,8 +38,14 @@ public abstract class DemoModeModule {
static DemoModeController provideDemoModeController( static DemoModeController provideDemoModeController(
Context context, Context context,
DumpManager dumpManager, DumpManager dumpManager,
GlobalSettings globalSettings) { GlobalSettings globalSettings,
DemoModeController dmc = new DemoModeController(context, dumpManager, globalSettings); BroadcastDispatcher broadcastDispatcher
) {
DemoModeController dmc = new DemoModeController(
context,
dumpManager,
globalSettings,
broadcastDispatcher);
dmc.initialize(); dmc.initialize();
return /*run*/dmc; return /*run*/dmc;
} }

View File

@@ -1302,7 +1302,7 @@ public class NetworkControllerImpl extends BroadcastReceiver
} }
} }
String wifi = args.getString("wifi"); String wifi = args.getString("wifi");
if (wifi != null) { if (wifi != null && !mStatusBarPipelineFlags.runNewWifiIconBackend()) {
boolean show = wifi.equals("show"); boolean show = wifi.equals("show");
String level = args.getString("level"); String level = args.getString("level");
if (level != null) { if (level != null) {

View File

@@ -42,6 +42,8 @@ import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconStat
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState;
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView; import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView;
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -56,6 +58,7 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
private final int mIconSize; private final int mIconSize;
private StatusBarWifiView mWifiView; private StatusBarWifiView mWifiView;
private ModernStatusBarWifiView mModernWifiView;
private boolean mDemoMode; private boolean mDemoMode;
private int mColor; private int mColor;
@@ -236,14 +239,14 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
public void addDemoWifiView(WifiIconState state) { public void addDemoWifiView(WifiIconState state) {
Log.d(TAG, "addDemoWifiView: "); Log.d(TAG, "addDemoWifiView: ");
// TODO(b/238425913): Migrate this view to {@code ModernStatusBarWifiView}.
StatusBarWifiView view = StatusBarWifiView.fromContext(mContext, state.slot); StatusBarWifiView view = StatusBarWifiView.fromContext(mContext, state.slot);
int viewIndex = getChildCount(); int viewIndex = getChildCount();
// If we have mobile views, put wifi before them // If we have mobile views, put wifi before them
for (int i = 0; i < getChildCount(); i++) { for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i); View child = getChildAt(i);
if (child instanceof StatusBarMobileView) { if (child instanceof StatusBarMobileView
|| child instanceof ModernStatusBarMobileView) {
viewIndex = i; viewIndex = i;
break; break;
} }
@@ -298,6 +301,30 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
addView(view, getChildCount(), createLayoutParams()); addView(view, getChildCount(), createLayoutParams());
} }
/**
* Add a {@link ModernStatusBarWifiView}
*/
public void addModernWifiView(LocationBasedWifiViewModel viewModel) {
Log.d(TAG, "addModernDemoWifiView: ");
ModernStatusBarWifiView view = ModernStatusBarWifiView
.constructAndBind(mContext, "wifi", viewModel);
int viewIndex = getChildCount();
// If we have mobile views, put wifi before them
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child instanceof StatusBarMobileView
|| child instanceof ModernStatusBarMobileView) {
viewIndex = i;
break;
}
}
mModernWifiView = view;
mModernWifiView.setStaticDrawableColor(mColor);
addView(view, viewIndex, createLayoutParams());
}
/** /**
* Apply an update to a mobile icon view for the given {@link MobileIconState}. For * Apply an update to a mobile icon view for the given {@link MobileIconState}. For
* compatibility with {@link MobileContextProvider}, we have to recreate the view every time we * compatibility with {@link MobileContextProvider}, we have to recreate the view every time we
@@ -320,8 +347,14 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
public void onRemoveIcon(StatusIconDisplayable view) { public void onRemoveIcon(StatusIconDisplayable view) {
if (view.getSlot().equals("wifi")) { if (view.getSlot().equals("wifi")) {
if (view instanceof StatusBarWifiView) {
removeView(mWifiView); removeView(mWifiView);
mWifiView = null; mWifiView = null;
} else if (view instanceof ModernStatusBarWifiView) {
Log.d(TAG, "onRemoveIcon: removing modern wifi view");
removeView(mModernWifiView);
mModernWifiView = null;
}
} else if (view instanceof StatusBarMobileView) { } else if (view instanceof StatusBarMobileView) {
StatusBarMobileView mobileView = matchingMobileView(view); StatusBarMobileView mobileView = matchingMobileView(view);
if (mobileView != null) { if (mobileView != null) {
@@ -374,8 +407,14 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
if (mWifiView != null) { if (mWifiView != null) {
mWifiView.onDarkChanged(areas, darkIntensity, tint); mWifiView.onDarkChanged(areas, darkIntensity, tint);
} }
if (mModernWifiView != null) {
mModernWifiView.onDarkChanged(areas, darkIntensity, tint);
}
for (StatusBarMobileView view : mMobileViews) { for (StatusBarMobileView view : mMobileViews) {
view.onDarkChanged(areas, darkIntensity, tint); view.onDarkChanged(areas, darkIntensity, tint);
} }
for (ModernStatusBarMobileView view : mModernMobileViews) {
view.onDarkChanged(areas, darkIntensity, tint);
}
} }
} }

View File

@@ -497,6 +497,11 @@ public interface StatusBarIconController {
ModernStatusBarWifiView view = onCreateModernStatusBarWifiView(slot); ModernStatusBarWifiView view = onCreateModernStatusBarWifiView(slot);
mGroup.addView(view, index, onCreateLayoutParams()); mGroup.addView(view, index, onCreateLayoutParams());
if (mIsInDemoMode) {
mDemoStatusIcons.addModernWifiView(mWifiViewModel);
}
return view; return view;
} }
@@ -688,6 +693,9 @@ public interface StatusBarIconController {
mIsInDemoMode = true; mIsInDemoMode = true;
if (mDemoStatusIcons == null) { if (mDemoStatusIcons == null) {
mDemoStatusIcons = createDemoStatusIcons(); mDemoStatusIcons = createDemoStatusIcons();
if (mStatusBarPipelineFlags.useNewWifiIcon()) {
mDemoStatusIcons.addModernWifiView(mWifiViewModel);
}
} }
mDemoStatusIcons.onDemoModeStarted(); mDemoStatusIcons.onDemoModeStarted();
} }

View File

@@ -36,7 +36,7 @@ import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyIm
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl
import dagger.Binds import dagger.Binds
@@ -56,7 +56,7 @@ abstract class StatusBarPipelineModule {
@Binds @Binds
abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
@Binds abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository @Binds abstract fun wifiRepository(impl: WifiRepositorySwitcher): WifiRepository
@Binds @Binds
abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor

View File

@@ -24,10 +24,8 @@ import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
import android.telephony.TelephonyManager.DATA_ACTIVITY_OUT import android.telephony.TelephonyManager.DATA_ACTIVITY_OUT
import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK
import com.android.systemui.demomode.DemoModeController import com.android.systemui.demomode.DemoModeController
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
@@ -35,8 +33,6 @@ import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
@@ -52,27 +48,7 @@ constructor(
demoModeController: DemoModeController, demoModeController: DemoModeController,
@Application scope: CoroutineScope, @Application scope: CoroutineScope,
) { ) {
private val demoCommandStream: Flow<Bundle> = conflatedCallbackFlow { private val demoCommandStream = demoModeController.demoFlowForCommand(COMMAND_NETWORK)
val callback =
object : DemoMode {
override fun demoCommands(): List<String> = listOf(COMMAND_NETWORK)
override fun dispatchDemoCommand(command: String, args: Bundle) {
trySend(args)
}
override fun onDemoModeFinished() {
// Handled elsewhere
}
override fun onDemoModeStarted() {
// Handled elsewhere
}
}
demoModeController.addCallback(callback)
awaitClose { demoModeController.removeCallback(callback) }
}
// If the args contains "mobile", then all of the args are relevant. It's just the way demo mode // If the args contains "mobile", then all of the args are relevant. It's just the way demo mode
// commands work and it's a little silly // commands work and it's a little silly

View File

@@ -0,0 +1,120 @@
/*
* Copyright (C) 2022 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
import android.os.Bundle
import androidx.annotation.VisibleForTesting
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoModeController
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoWifiRepository
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
/**
* Provides the [WifiRepository] interface either through the [DemoWifiRepository] implementation,
* or the [WifiRepositoryImpl]'s prod implementation, based on the current demo mode value. In this
* way, downstream clients can all consist of real implementations and not care about which
* repository is responsible for the data. Graphically:
*
* ```
* RealRepository
* │
* ├──►RepositorySwitcher──►RealInteractor──►RealViewModel
* │
* DemoRepository
* ```
*
* When demo mode turns on, every flow will [flatMapLatest] to the current provider's version of
* that flow.
*/
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
class WifiRepositorySwitcher
@Inject
constructor(
private val realImpl: WifiRepositoryImpl,
private val demoImpl: DemoWifiRepository,
private val demoModeController: DemoModeController,
@Application scope: CoroutineScope,
) : WifiRepository {
private val isDemoMode =
conflatedCallbackFlow {
val callback =
object : DemoMode {
override fun dispatchDemoCommand(command: String?, args: Bundle?) {
// Don't care
}
override fun onDemoModeStarted() {
demoImpl.startProcessingCommands()
trySend(true)
}
override fun onDemoModeFinished() {
demoImpl.stopProcessingCommands()
trySend(false)
}
}
demoModeController.addCallback(callback)
awaitClose { demoModeController.removeCallback(callback) }
}
.stateIn(scope, SharingStarted.WhileSubscribed(), demoModeController.isInDemoMode)
@VisibleForTesting
val activeRepo =
isDemoMode
.mapLatest { isDemoMode ->
if (isDemoMode) {
demoImpl
} else {
realImpl
}
}
.stateIn(scope, SharingStarted.WhileSubscribed(), realImpl)
override val isWifiEnabled: StateFlow<Boolean> =
activeRepo
.flatMapLatest { it.isWifiEnabled }
.stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.isWifiEnabled.value)
override val isWifiDefault: StateFlow<Boolean> =
activeRepo
.flatMapLatest { it.isWifiDefault }
.stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.isWifiDefault.value)
override val wifiNetwork: StateFlow<WifiNetworkModel> =
activeRepo
.flatMapLatest { it.wifiNetwork }
.stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.wifiNetwork.value)
override val wifiActivity: StateFlow<DataActivityModel> =
activeRepo
.flatMapLatest { it.wifiActivity }
.stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.wifiActivity.value)
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2022 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.demo
import android.net.wifi.WifiManager
import android.os.Bundle
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK
import com.android.systemui.demomode.DemoModeController
import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
/** Data source to map between demo mode commands and inputs into [DemoWifiRepository]'s flows */
@SysUISingleton
class DemoModeWifiDataSource
@Inject
constructor(
demoModeController: DemoModeController,
@Application scope: CoroutineScope,
) {
private val demoCommandStream = demoModeController.demoFlowForCommand(COMMAND_NETWORK)
private val _wifiCommands = demoCommandStream.map { args -> args.toWifiEvent() }
val wifiEvents = _wifiCommands.shareIn(scope, SharingStarted.WhileSubscribed())
private fun Bundle.toWifiEvent(): FakeWifiEventModel? {
val wifi = getString("wifi") ?: return null
return if (wifi == "show") {
activeWifiEvent()
} else {
FakeWifiEventModel.WifiDisabled
}
}
private fun Bundle.activeWifiEvent(): FakeWifiEventModel.Wifi {
val level = getString("level")?.toInt()
val activity = getString("activity")?.toActivity()
val ssid = getString("ssid")
val validated = getString("fully").toBoolean()
return FakeWifiEventModel.Wifi(
level = level,
activity = activity,
ssid = ssid,
validated = validated,
)
}
private fun String.toActivity(): Int =
when (this) {
"inout" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT
"in" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN
"out" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_OUT
else -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_NONE
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright (C) 2022 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.demo
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
/** Demo-able wifi repository to support SystemUI demo mode commands. */
class DemoWifiRepository
@Inject
constructor(
private val dataSource: DemoModeWifiDataSource,
@Application private val scope: CoroutineScope,
) : WifiRepository {
private var demoCommandJob: Job? = null
private val _isWifiEnabled = MutableStateFlow(false)
override val isWifiEnabled: StateFlow<Boolean> = _isWifiEnabled
private val _isWifiDefault = MutableStateFlow(false)
override val isWifiDefault: StateFlow<Boolean> = _isWifiDefault
private val _wifiNetwork = MutableStateFlow<WifiNetworkModel>(WifiNetworkModel.Inactive)
override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork
private val _wifiActivity =
MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
override val wifiActivity: StateFlow<DataActivityModel> = _wifiActivity
fun startProcessingCommands() {
demoCommandJob =
scope.launch {
dataSource.wifiEvents.filterNotNull().collect { event -> processEvent(event) }
}
}
fun stopProcessingCommands() {
demoCommandJob?.cancel()
}
private fun processEvent(event: FakeWifiEventModel) =
when (event) {
is FakeWifiEventModel.Wifi -> processEnabledWifiState(event)
is FakeWifiEventModel.WifiDisabled -> processDisabledWifiState()
}
private fun processDisabledWifiState() {
_isWifiEnabled.value = false
_isWifiDefault.value = false
_wifiActivity.value = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
_wifiNetwork.value = WifiNetworkModel.Inactive
}
private fun processEnabledWifiState(event: FakeWifiEventModel.Wifi) {
_isWifiEnabled.value = true
_isWifiDefault.value = true
_wifiActivity.value =
event.activity?.toWifiDataActivityModel()
?: DataActivityModel(hasActivityIn = false, hasActivityOut = false)
_wifiNetwork.value = event.toWifiNetworkModel()
}
private fun FakeWifiEventModel.Wifi.toWifiNetworkModel(): WifiNetworkModel =
WifiNetworkModel.Active(
networkId = DEMO_NET_ID,
isValidated = validated ?: true,
level = level,
ssid = ssid,
// These fields below aren't supported in demo mode, since they aren't needed to satisfy
// the interface.
isPasspointAccessPoint = false,
isOnlineSignUpForPasspointAccessPoint = false,
passpointProviderFriendlyName = null,
)
companion object {
private const val DEMO_NET_ID = 1234
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2022 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.demo.model
/**
* Model for demo wifi commands, ported from [NetworkControllerImpl]
*
* Nullable fields represent optional command line arguments
*/
sealed interface FakeWifiEventModel {
data class Wifi(
val level: Int?,
val activity: Int?,
val ssid: String?,
val validated: Boolean?,
) : FakeWifiEventModel
object WifiDisabled : FakeWifiEventModel
}

View File

@@ -33,6 +33,7 @@ import com.android.systemui.R;
import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoMode;
import com.android.systemui.demomode.DemoModeAvailabilityTracker; import com.android.systemui.demomode.DemoModeAvailabilityTracker;
import com.android.systemui.demomode.DemoModeController; import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.util.settings.GlobalSettings;
public class DemoModeFragment extends PreferenceFragment implements OnPreferenceChangeListener { public class DemoModeFragment extends PreferenceFragment implements OnPreferenceChangeListener {
@@ -54,13 +55,15 @@ public class DemoModeFragment extends PreferenceFragment implements OnPreference
private SwitchPreference mOnSwitch; private SwitchPreference mOnSwitch;
private DemoModeController mDemoModeController; private DemoModeController mDemoModeController;
private GlobalSettings mGlobalSettings;
private Tracker mDemoModeTracker; private Tracker mDemoModeTracker;
// We are the only ones who ever call this constructor, so don't worry about the warning // We are the only ones who ever call this constructor, so don't worry about the warning
@SuppressLint("ValidFragment") @SuppressLint("ValidFragment")
public DemoModeFragment(DemoModeController demoModeController) { public DemoModeFragment(DemoModeController demoModeController, GlobalSettings globalSettings) {
super(); super();
mDemoModeController = demoModeController; mDemoModeController = demoModeController;
mGlobalSettings = globalSettings;
} }
@@ -80,7 +83,7 @@ public class DemoModeFragment extends PreferenceFragment implements OnPreference
screen.addPreference(mOnSwitch); screen.addPreference(mOnSwitch);
setPreferenceScreen(screen); setPreferenceScreen(screen);
mDemoModeTracker = new Tracker(context); mDemoModeTracker = new Tracker(context, mGlobalSettings);
mDemoModeTracker.startTracking(); mDemoModeTracker.startTracking();
updateDemoModeEnabled(); updateDemoModeEnabled();
updateDemoModeOn(); updateDemoModeOn();
@@ -202,8 +205,8 @@ public class DemoModeFragment extends PreferenceFragment implements OnPreference
} }
private class Tracker extends DemoModeAvailabilityTracker { private class Tracker extends DemoModeAvailabilityTracker {
Tracker(Context context) { Tracker(Context context, GlobalSettings globalSettings) {
super(context); super(context, globalSettings);
} }
@Override @Override

View File

@@ -33,6 +33,7 @@ import com.android.systemui.Dependency;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.demomode.DemoModeController; import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.fragments.FragmentService; import com.android.systemui.fragments.FragmentService;
import com.android.systemui.util.settings.GlobalSettings;
import javax.inject.Inject; import javax.inject.Inject;
@@ -44,12 +45,18 @@ public class TunerActivity extends Activity implements
private final DemoModeController mDemoModeController; private final DemoModeController mDemoModeController;
private final TunerService mTunerService; private final TunerService mTunerService;
private final GlobalSettings mGlobalSettings;
@Inject @Inject
TunerActivity(DemoModeController demoModeController, TunerService tunerService) { TunerActivity(
DemoModeController demoModeController,
TunerService tunerService,
GlobalSettings globalSettings
) {
super(); super();
mDemoModeController = demoModeController; mDemoModeController = demoModeController;
mTunerService = tunerService; mTunerService = tunerService;
mGlobalSettings = globalSettings;
} }
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -69,7 +76,7 @@ public class TunerActivity extends Activity implements
boolean showDemoMode = action != null && action.equals( boolean showDemoMode = action != null && action.equals(
"com.android.settings.action.DEMO_MODE"); "com.android.settings.action.DEMO_MODE");
final PreferenceFragment fragment = showDemoMode final PreferenceFragment fragment = showDemoMode
? new DemoModeFragment(mDemoModeController) ? new DemoModeFragment(mDemoModeController, mGlobalSettings)
: new TunerFragment(mTunerService); : new TunerFragment(mTunerService);
getFragmentManager().beginTransaction().replace(R.id.content_frame, getFragmentManager().beginTransaction().replace(R.id.content_frame,
fragment, TAG_TUNER).commit(); fragment, TAG_TUNER).commit();

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2022 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.demomode
import android.content.Intent
import android.os.Bundle
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.demomode.DemoMode.ACTION_DEMO
import com.android.systemui.demomode.DemoMode.COMMAND_STATUS
import com.android.systemui.dump.DumpManager
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
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.MockitoAnnotations
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
@SmallTest
class DemoModeControllerTest : SysuiTestCase() {
private lateinit var underTest: DemoModeController
@Mock private lateinit var dumpManager: DumpManager
private val globalSettings = FakeSettings()
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
@Before
fun setUp() {
allowTestableLooperAsMainThread()
MockitoAnnotations.initMocks(this)
globalSettings.putInt(DemoModeController.DEMO_MODE_ALLOWED, 1)
globalSettings.putInt(DemoModeController.DEMO_MODE_ON, 1)
underTest =
DemoModeController(
context = context,
dumpManager = dumpManager,
globalSettings = globalSettings,
broadcastDispatcher = fakeBroadcastDispatcher
)
underTest.initialize()
}
@Test
fun `demo command flow - returns args`() =
testScope.runTest {
var latest: Bundle? = null
val flow = underTest.demoFlowForCommand(TEST_COMMAND)
val job = launch { flow.collect { latest = it } }
sendDemoCommand(args = mapOf("key1" to "val1"))
assertThat(latest!!.getString("key1")).isEqualTo("val1")
sendDemoCommand(args = mapOf("key2" to "val2"))
assertThat(latest!!.getString("key2")).isEqualTo("val2")
job.cancel()
}
private fun sendDemoCommand(command: String? = TEST_COMMAND, args: Map<String, String>) {
val intent = Intent(ACTION_DEMO)
intent.putExtra("command", command)
args.forEach { arg -> intent.putExtra(arg.key, arg.value) }
fakeBroadcastDispatcher.registeredReceivers.forEach { it.onReceive(context, intent) }
}
companion object {
// Use a valid command until we properly fake out everything
const val TEST_COMMAND = COMMAND_STATUS
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright (C) 2022 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
import android.net.ConnectivityManager
import android.net.wifi.WifiManager
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoModeController
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoWifiRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.kotlinArgumentCaptor
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class WifiRepositorySwitcherTest : SysuiTestCase() {
private lateinit var underTest: WifiRepositorySwitcher
private lateinit var realImpl: WifiRepositoryImpl
private lateinit var demoImpl: DemoWifiRepository
@Mock private lateinit var demoModeController: DemoModeController
@Mock private lateinit var logger: ConnectivityPipelineLogger
@Mock private lateinit var tableLogger: TableLogBuffer
@Mock private lateinit var connectivityManager: ConnectivityManager
@Mock private lateinit var wifiManager: WifiManager
@Mock private lateinit var demoModeWifiDataSource: DemoModeWifiDataSource
private val demoModelFlow = MutableStateFlow<FakeWifiEventModel?>(null)
private val mainExecutor = FakeExecutor(FakeSystemClock())
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
// Never start in demo mode
whenever(demoModeController.isInDemoMode).thenReturn(false)
realImpl =
WifiRepositoryImpl(
fakeBroadcastDispatcher,
connectivityManager,
logger,
tableLogger,
mainExecutor,
testScope.backgroundScope,
wifiManager,
)
whenever(demoModeWifiDataSource.wifiEvents).thenReturn(demoModelFlow)
demoImpl =
DemoWifiRepository(
demoModeWifiDataSource,
testScope.backgroundScope,
)
underTest =
WifiRepositorySwitcher(
realImpl,
demoImpl,
demoModeController,
testScope.backgroundScope,
)
}
@Test
fun `switcher active repo - updates when demo mode changes`() =
testScope.runTest {
assertThat(underTest.activeRepo.value).isSameInstanceAs(realImpl)
var latest: WifiRepository? = null
val job = underTest.activeRepo.onEach { latest = it }.launchIn(this)
startDemoMode()
assertThat(latest).isSameInstanceAs(demoImpl)
finishDemoMode()
assertThat(latest).isSameInstanceAs(realImpl)
job.cancel()
}
private fun startDemoMode() {
whenever(demoModeController.isInDemoMode).thenReturn(true)
getDemoModeCallback().onDemoModeStarted()
}
private fun finishDemoMode() {
whenever(demoModeController.isInDemoMode).thenReturn(false)
getDemoModeCallback().onDemoModeFinished()
}
private fun getDemoModeCallback(): DemoMode {
val captor = kotlinArgumentCaptor<DemoMode>()
Mockito.verify(demoModeController).addCallback(captor.capture())
return captor.value
}
}