[Sb refactor] Wifi demo mode

Test: WifiRepositorySwitcherTest
Bug: 238425913
Change-Id: Iea3d94f6543cd599f667b297edf9d5019e841067
This commit is contained in:
Evan Laird
2022-12-17 14:24:17 -05:00
parent 2775b3b39e
commit a3cef0ce4f
9 changed files with 523 additions and 7 deletions

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

@@ -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

@@ -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
}
}