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.os.Handler
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
@@ -29,7 +29,10 @@ import android.provider.Settings
* This class works by wrapping a content observer for the relevant keys related to DemoMode
* 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 isDemoModeAvailable = false
@@ -41,9 +44,9 @@ abstract class DemoModeAvailabilityTracker(val context: Context) {
fun startTracking() {
val resolver = context.contentResolver
resolver.registerContentObserver(
Settings.Global.getUriFor(DEMO_MODE_ALLOWED), false, allowedObserver)
globalSettings.getUriFor(DEMO_MODE_ALLOWED), false, allowedObserver)
resolver.registerContentObserver(
Settings.Global.getUriFor(DEMO_MODE_ON), false, onObserver)
globalSettings.getUriFor(DEMO_MODE_ON), false, onObserver)
}
fun stopTracking() {
@@ -57,12 +60,11 @@ abstract class DemoModeAvailabilityTracker(val context: Context) {
abstract fun onDemoModeFinished()
private fun checkIsDemoModeAllowed(): Boolean {
return Settings.Global
.getInt(context.contentResolver, DEMO_MODE_ALLOWED, 0) != 0
return globalSettings.getInt(DEMO_MODE_ALLOWED, 0) != 0
}
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())) {

View File

@@ -24,22 +24,28 @@ import android.os.Bundle
import android.os.UserHandle
import android.util.Log
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.dump.DumpManager
import com.android.systemui.statusbar.policy.CallbackController
import com.android.systemui.util.Assert
import com.android.systemui.util.settings.GlobalSettings
import java.io.PrintWriter
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
/**
* Handles system broadcasts for [DemoMode]
*
* Injected via [DemoModeModule]
*/
class DemoModeController constructor(
class DemoModeController
constructor(
private val context: Context,
private val dumpManager: DumpManager,
private val globalSettings: GlobalSettings
private val globalSettings: GlobalSettings,
private val broadcastDispatcher: BroadcastDispatcher,
) : CallbackController<DemoMode>, Dumpable {
// Var updated when the availability tracker changes, or when we enter/exit demo mode in-process
@@ -58,9 +64,7 @@ class DemoModeController constructor(
requestFinishDemoMode()
val m = mutableMapOf<String, MutableList<DemoMode>>()
DemoMode.COMMANDS.map { command ->
m.put(command, mutableListOf())
}
DemoMode.COMMANDS.map { command -> m.put(command, mutableListOf()) }
receiverMap = m
}
@@ -71,7 +75,7 @@ class DemoModeController constructor(
initialized = true
dumpManager.registerDumpable(TAG, this)
dumpManager.registerNormalDumpable(TAG, this)
// Due to DemoModeFragment running in systemui:tuner process, we have to observe for
// content changes to know if the setting turned on or off
@@ -81,8 +85,13 @@ class DemoModeController constructor(
val demoFilter = IntentFilter()
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) {
@@ -91,16 +100,15 @@ class DemoModeController constructor(
commands.forEach { command ->
if (!receiverMap.containsKey(command)) {
throw IllegalStateException("Command ($command) not recognized. " +
"See DemoMode.java for valid commands")
throw IllegalStateException(
"Command ($command) not recognized. " + "See DemoMode.java for valid commands"
)
}
receiverMap[command]!!.add(listener)
}
synchronized(this) {
receivers.add(listener)
}
synchronized(this) { receivers.add(listener) }
if (isInDemoMode) {
listener.onDemoModeStarted()
@@ -109,14 +117,46 @@ class DemoModeController constructor(
override fun removeCallback(listener: DemoMode) {
synchronized(this) {
listener.demoCommands().forEach { command ->
receiverMap[command]!!.remove(listener)
}
listener.demoCommands().forEach { command -> receiverMap[command]!!.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) {
// Turn off demo mode if it was on
if (isInDemoMode && !enabled) {
@@ -129,13 +169,9 @@ class DemoModeController constructor(
Assert.isMainThread()
val copy: List<DemoModeCommandReceiver>
synchronized(this) {
copy = receivers.toList()
}
synchronized(this) { copy = receivers.toList() }
copy.forEach { r ->
r.onDemoModeStarted()
}
copy.forEach { r -> r.onDemoModeStarted() }
}
private fun exitDemoMode() {
@@ -143,18 +179,13 @@ class DemoModeController constructor(
Assert.isMainThread()
val copy: List<DemoModeCommandReceiver>
synchronized(this) {
copy = receivers.toList()
}
synchronized(this) { copy = receivers.toList() }
copy.forEach { r ->
r.onDemoModeFinished()
}
copy.forEach { r -> r.onDemoModeFinished() }
}
fun dispatchDemoCommand(command: String, args: Bundle) {
Assert.isMainThread()
if (DEBUG) {
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
receiverMap[command]!!.forEach { receiver ->
receiver.dispatchDemoCommand(command, args)
}
receiverMap[command]!!.forEach { receiver -> receiver.dispatchDemoCommand(command, args) }
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
@@ -183,65 +212,64 @@ class DemoModeController constructor(
pw.println(" isDemoModeAllowed=$isAvailable")
pw.print(" receivers=[")
val copy: List<DemoModeCommandReceiver>
synchronized(this) {
copy = receivers.toList()
}
copy.forEach { recv ->
pw.print(" ${recv.javaClass.simpleName}")
}
synchronized(this) { copy = receivers.toList() }
copy.forEach { recv -> pw.print(" ${recv.javaClass.simpleName}") }
pw.println(" ]")
pw.println(" receiverMap= [")
receiverMap.keys.forEach { command ->
pw.print(" $command : [")
val recvs = receiverMap[command]!!.map { receiver ->
receiver.javaClass.simpleName
}.joinToString(",")
val recvs =
receiverMap[command]!!
.map { receiver -> receiver.javaClass.simpleName }
.joinToString(",")
pw.println("$recvs ]")
}
}
private val tracker = object : DemoModeAvailabilityTracker(context) {
override fun onDemoModeAvailabilityChanged() {
setIsDemoModeAllowed(isDemoModeAvailable)
}
private val tracker =
object : DemoModeAvailabilityTracker(context, globalSettings) {
override fun onDemoModeAvailabilityChanged() {
setIsDemoModeAllowed(isDemoModeAvailable)
}
override fun onDemoModeStarted() {
if (this@DemoModeController.isInDemoMode != isInDemoMode) {
enterDemoMode()
override fun onDemoModeStarted() {
if (this@DemoModeController.isInDemoMode != isInDemoMode) {
enterDemoMode()
}
}
override fun onDemoModeFinished() {
if (this@DemoModeController.isInDemoMode != isInDemoMode) {
exitDemoMode()
}
}
}
override fun onDemoModeFinished() {
if (this@DemoModeController.isInDemoMode != isInDemoMode) {
exitDemoMode()
private val broadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (DEBUG) {
Log.v(TAG, "onReceive: $intent")
}
val action = intent.action
if (!ACTION_DEMO.equals(action)) {
return
}
val bundle = intent.extras ?: return
val command = bundle.getString("command", "").trim().lowercase()
if (command.isEmpty()) {
return
}
try {
dispatchDemoCommand(command, bundle)
} catch (t: Throwable) {
Log.w(TAG, "Error running demo command, intent=$intent $t")
}
}
}
}
private val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (DEBUG) {
Log.v(TAG, "onReceive: $intent")
}
val action = intent.action
if (!ACTION_DEMO.equals(action)) {
return
}
val bundle = intent.extras ?: return
val command = bundle.getString("command", "").trim().toLowerCase()
if (command.length == 0) {
return
}
try {
dispatchDemoCommand(command, bundle)
} catch (t: Throwable) {
Log.w(TAG, "Error running demo command, intent=$intent $t")
}
}
}
fun requestSetDemoModeAllowed(allowed: Boolean) {
setGlobal(DEMO_MODE_ALLOWED, if (allowed) 1 else 0)
@@ -258,10 +286,12 @@ class DemoModeController constructor(
private fun setGlobal(key: String, value: Int) {
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 DEMO_MODE_ALLOWED = "sysui_demo_allowed"
private const val DEMO_MODE_ON = "sysui_tuner_demo_on"
private const val DEBUG = false

View File

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

View File

@@ -1302,7 +1302,7 @@ public class NetworkControllerImpl extends BroadcastReceiver
}
}
String wifi = args.getString("wifi");
if (wifi != null) {
if (wifi != null && !mStatusBarPipelineFlags.runNewWifiIconBackend()) {
boolean show = wifi.equals("show");
String level = args.getString("level");
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.pipeline.mobile.ui.view.ModernStatusBarMobileView;
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.List;
@@ -56,6 +58,7 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
private final int mIconSize;
private StatusBarWifiView mWifiView;
private ModernStatusBarWifiView mModernWifiView;
private boolean mDemoMode;
private int mColor;
@@ -236,14 +239,14 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
public void addDemoWifiView(WifiIconState state) {
Log.d(TAG, "addDemoWifiView: ");
// TODO(b/238425913): Migrate this view to {@code ModernStatusBarWifiView}.
StatusBarWifiView view = StatusBarWifiView.fromContext(mContext, state.slot);
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) {
if (child instanceof StatusBarMobileView
|| child instanceof ModernStatusBarMobileView) {
viewIndex = i;
break;
}
@@ -298,6 +301,30 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
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
* 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) {
if (view.getSlot().equals("wifi")) {
removeView(mWifiView);
mWifiView = null;
if (view instanceof StatusBarWifiView) {
removeView(mWifiView);
mWifiView = null;
} else if (view instanceof ModernStatusBarWifiView) {
Log.d(TAG, "onRemoveIcon: removing modern wifi view");
removeView(mModernWifiView);
mModernWifiView = null;
}
} else if (view instanceof StatusBarMobileView) {
StatusBarMobileView mobileView = matchingMobileView(view);
if (mobileView != null) {
@@ -374,8 +407,14 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da
if (mWifiView != null) {
mWifiView.onDarkChanged(areas, darkIntensity, tint);
}
if (mModernWifiView != null) {
mModernWifiView.onDarkChanged(areas, darkIntensity, tint);
}
for (StatusBarMobileView view : mMobileViews) {
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);
mGroup.addView(view, index, onCreateLayoutParams());
if (mIsInDemoMode) {
mDemoStatusIcons.addModernWifiView(mWifiViewModel);
}
return view;
}
@@ -688,6 +693,9 @@ public interface StatusBarIconController {
mIsInDemoMode = true;
if (mDemoStatusIcons == null) {
mDemoStatusIcons = createDemoStatusIcons();
if (mStatusBarPipelineFlags.useNewWifiIcon()) {
mDemoStatusIcons.addModernWifiView(mWifiViewModel);
}
}
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.ConnectivityRepositoryImpl
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.WifiInteractorImpl
import dagger.Binds
@@ -56,7 +56,7 @@ abstract class StatusBarPipelineModule {
@Binds
abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
@Binds abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository
@Binds abstract fun wifiRepository(impl: WifiRepositorySwitcher): WifiRepository
@Binds
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 com.android.settingslib.SignalIcon.MobileIconGroup
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.qualifiers.Application
import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK
import com.android.systemui.demomode.DemoModeController
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 javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
@@ -52,27 +48,7 @@ constructor(
demoModeController: DemoModeController,
@Application scope: CoroutineScope,
) {
private val demoCommandStream: Flow<Bundle> = conflatedCallbackFlow {
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) }
}
private val demoCommandStream = demoModeController.demoFlowForCommand(COMMAND_NETWORK)
// 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

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

View File

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