diff options
13 files changed, 558 insertions, 18 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java index 5deb08a75dff..cff46ab812bf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java @@ -37,6 +37,7 @@ import com.android.systemui.statusbar.StatusIconDisplayable; import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger; 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.shared.ui.view.ModernStatusBarView; import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView; import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel; @@ -277,6 +278,15 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da addView(view, viewIndex, createLayoutParams()); } + /** Adds a bindable icon to the demo mode view. */ + public void addBindableIcon(StatusBarIconHolder.BindableIconHolder holder) { + // This doesn't do any correct ordering, and also doesn't check if we already have an + // existing icon for the slot. But since we hope to remove this class soon, we won't spend + // the time adding that logic. + ModernStatusBarView view = holder.getInitializer().createAndBind(mContext); + addView(view, createLayoutParams()); + } + public void onRemoveIcon(StatusIconDisplayable view) { if (view.getSlot().equals("wifi")) { if (view instanceof ModernStatusBarWifiView) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index d7cbe5df419b..5b82cf7addf3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -54,7 +54,9 @@ import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWi import com.android.systemui.util.Assert; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.inject.Inject; @@ -348,7 +350,11 @@ public interface StatusBarIconController { private final MobileContextProvider mMobileContextProvider; private final LocationBasedWifiViewModel mWifiViewModel; private final MobileIconsViewModel mMobileIconsViewModel; - + /** + * Stores the list of bindable icons that have been added, keyed on slot name. This ensures + * we don't accidentally add the same bindable icon twice. + */ + private final Map<String, BindableIconHolder> mBindableIcons = new HashMap<>(); protected final Context mContext; protected int mIconSize; // Whether or not these icons show up in dumpsys @@ -460,8 +466,12 @@ public interface StatusBarIconController { * ViewBinder to control its visual state. */ protected StatusIconDisplayable addBindableIcon(BindableIconHolder holder, int index) { + mBindableIcons.put(holder.getSlot(), holder); ModernStatusBarView view = holder.getInitializer().createAndBind(mContext); mGroup.addView(view, index, onCreateLayoutParams()); + if (mIsInDemoMode) { + mDemoStatusIcons.addBindableIcon(holder); + } return view; } @@ -572,6 +582,9 @@ public interface StatusBarIconController { if (mDemoStatusIcons == null) { mDemoStatusIcons = createDemoStatusIcons(); mDemoStatusIcons.addModernWifiView(mWifiViewModel); + for (BindableIconHolder holder : mBindableIcons.values()) { + mDemoStatusIcons.addBindableIcon(holder); + } } mDemoStatusIcons.onDemoModeStarted(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java index 4f148f112c52..ad2ea2fe1e39 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java @@ -212,7 +212,8 @@ public class StatusBarIconControllerImpl implements Tunable, StatusBarIconHolder existingHolder = mStatusBarIconList.getIconHolder(icon.getSlot(), 0); // Expected to be null if (existingHolder == null) { - BindableIconHolder bindableIcon = new BindableIconHolder(icon.getInitializer()); + BindableIconHolder bindableIcon = + new BindableIconHolder(icon.getInitializer(), icon.getSlot()); setIcon(icon.getSlot(), bindableIcon); } else { Log.e(TAG, "addBindableIcon called, but icon has already been added. Ignoring"); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt index bef0b286ebf8..08a890dbadb5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt @@ -169,16 +169,19 @@ open class StatusBarIconHolder private constructor() { * StatusBarIconController will register all available bindable icons on init (see * [BindableIconsRepository]), and will ignore any call to setIcon for these. * - * [initializer] a view creator that can bind the relevant view models to the created view. + * @property initializer a view creator that can bind the relevant view models to the created + * view. + * @property slot the name of the slot that this holder is used for. */ - class BindableIconHolder(val initializer: ModernStatusBarViewCreator) : StatusBarIconHolder() { + class BindableIconHolder(val initializer: ModernStatusBarViewCreator, val slot: String) : + StatusBarIconHolder() { override var type: Int = TYPE_BINDABLE /** This is unused, as bindable icons use their own view binders to control visibility */ override var isVisible: Boolean = true override fun toString(): String { - return ("StatusBarIconHolder(type=BINDABLE)") + return ("StatusBarIconHolder(type=BINDABLE, slot=$slot)") } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 4129b3a8d9ec..5d91c7fd91ad 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -41,6 +41,8 @@ import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyIm import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxy import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxyImpl import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository +import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepositorySwitcher +import com.android.systemui.statusbar.pipeline.satellite.data.RealDeviceBasedSatelliteRepository import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl @@ -81,8 +83,13 @@ abstract class StatusBarPipelineModule { abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository @Binds - abstract fun deviceBasedSatelliteRepository( + abstract fun realDeviceBasedSatelliteRepository( impl: DeviceBasedSatelliteRepositoryImpl + ): RealDeviceBasedSatelliteRepository + + @Binds + abstract fun deviceBasedSatelliteRepository( + impl: DeviceBasedSatelliteRepositorySwitcher ): DeviceBasedSatelliteRepository @Binds abstract fun wifiRepository(impl: WifiRepositorySwitcher): WifiRepository diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt index ad8b8100f14d..d38e834ff1e2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt @@ -17,7 +17,7 @@ package com.android.systemui.statusbar.pipeline.satellite.data import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow /** * Device-based satellite refers to the capability of a device to connect directly to a satellite @@ -26,12 +26,22 @@ import kotlinx.coroutines.flow.Flow */ interface DeviceBasedSatelliteRepository { /** See [SatelliteConnectionState] for available states */ - val connectionState: Flow<SatelliteConnectionState> + val connectionState: StateFlow<SatelliteConnectionState> /** 0-4 level (similar to wifi and mobile) */ // @IntRange(from = 0, to = 4) - val signalStrength: Flow<Int> + val signalStrength: StateFlow<Int> /** Clients must observe this property, as device-based satellite is location-dependent */ - val isSatelliteAllowedForCurrentLocation: Flow<Boolean> + val isSatelliteAllowedForCurrentLocation: StateFlow<Boolean> } + +/** + * A no-op interface used for Dagger bindings. + * + * [DeviceBasedSatelliteRepositorySwitcher] needs to inject both the real repository and the demo + * mode repository, both of which implement the [DeviceBasedSatelliteRepository] interface. To help + * distinguish the two for the switcher, [DeviceBasedSatelliteRepositoryImpl] will implement this + * [RealDeviceBasedSatelliteRepository] interface. + */ +interface RealDeviceBasedSatelliteRepository : DeviceBasedSatelliteRepository diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcher.kt new file mode 100644 index 000000000000..6b1bc65e86db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcher.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2024 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.satellite.data + +import android.os.Bundle +import androidx.annotation.VisibleForTesting +import com.android.systemui.dagger.SysUISingleton +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.satellite.data.demo.DemoDeviceBasedSatelliteRepository +import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +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 + +/** + * A provider for the [DeviceBasedSatelliteRepository] interface that can choose between the Demo + * and Prod concrete implementations at runtime. It works by defining a base flow, [activeRepo], + * which switches based on the latest information from [DemoModeController], and switches every flow + * in the interface to point to the currently-active provider. This allows us to put the demo mode + * interface in its own repository, completely separate from the real version, while still using all + * of the prod implementations for the rest of the pipeline (interactors and onward). Looks + * something like this: + * ``` + * RealRepository + * │ + * ├──►RepositorySwitcher──►RealInteractor──►RealViewModel + * │ + * DemoRepository + * ``` + */ +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class DeviceBasedSatelliteRepositorySwitcher +@Inject +constructor( + private val realImpl: RealDeviceBasedSatelliteRepository, + private val demoImpl: DemoDeviceBasedSatelliteRepository, + private val demoModeController: DemoModeController, + @Application scope: CoroutineScope, +) : DeviceBasedSatelliteRepository { + 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: StateFlow<DeviceBasedSatelliteRepository> = + isDemoMode + .mapLatest { isDemoMode -> + if (isDemoMode) { + demoImpl + } else { + realImpl + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl) + + override val connectionState: StateFlow<SatelliteConnectionState> = + activeRepo + .flatMapLatest { it.connectionState } + .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.connectionState.value) + + override val signalStrength: StateFlow<Int> = + activeRepo + .flatMapLatest { it.signalStrength } + .stateIn(scope, SharingStarted.WhileSubscribed(), realImpl.signalStrength.value) + + override val isSatelliteAllowedForCurrentLocation: StateFlow<Boolean> = + activeRepo + .flatMapLatest { it.isSatelliteAllowedForCurrentLocation } + .stateIn( + scope, + SharingStarted.WhileSubscribed(), + realImpl.isSatelliteAllowedForCurrentLocation.value + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteDataSource.kt new file mode 100644 index 000000000000..fecd7fec2d2c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteDataSource.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 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.satellite.data.demo + +import android.os.Bundle +import com.android.systemui.dagger.SysUISingleton +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.satellite.shared.model.SatelliteConnectionState +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn + +/** + * Reads the incoming demo commands and emits the satellite-related commands to [satelliteEvents] + * for the demo repository to consume. + */ +@SysUISingleton +class DemoDeviceBasedSatelliteDataSource +@Inject +constructor( + demoModeController: DemoModeController, + @Application scope: CoroutineScope, +) { + private val demoCommandStream = demoModeController.demoFlowForCommand(DemoMode.COMMAND_NETWORK) + private val _satelliteCommands = + demoCommandStream.map { args -> args.toSatelliteEvent() }.filterNotNull() + + /** A flow that emits the demo commands that are satellite-related. */ + val satelliteEvents = _satelliteCommands.shareIn(scope, SharingStarted.WhileSubscribed()) + + private fun Bundle.toSatelliteEvent(): DemoSatelliteEvent? { + val satellite = getString("satellite") ?: return null + if (satellite != "show") { + return null + } + + return DemoSatelliteEvent( + connectionState = getString("connection").toConnectionState(), + signalStrength = getString("level")?.toInt() ?: 0, + ) + } + + data class DemoSatelliteEvent( + val connectionState: SatelliteConnectionState, + val signalStrength: Int, + ) + + private fun String?.toConnectionState(): SatelliteConnectionState { + if (this == null) { + return SatelliteConnectionState.Unknown + } + return try { + // Lets people use "connected" on the command line and have it be correctly converted + // to [SatelliteConnectionState.Connected] with a capital C. + SatelliteConnectionState.valueOf(this.replaceFirstChar { it.uppercase() }) + } catch (e: IllegalArgumentException) { + SatelliteConnectionState.Unknown + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepository.kt new file mode 100644 index 000000000000..56034f08503d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepository.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 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.satellite.data.demo + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository +import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch + +/** A satellite repository that represents the latest satellite values sent via demo mode. */ +@SysUISingleton +class DemoDeviceBasedSatelliteRepository +@Inject +constructor( + private val dataSource: DemoDeviceBasedSatelliteDataSource, + @Application private val scope: CoroutineScope, +) : DeviceBasedSatelliteRepository { + private var demoCommandJob: Job? = null + + override val connectionState = MutableStateFlow(SatelliteConnectionState.Unknown) + override val signalStrength = MutableStateFlow(0) + override val isSatelliteAllowedForCurrentLocation = MutableStateFlow(true) + + fun startProcessingCommands() { + demoCommandJob = + scope.launch { dataSource.satelliteEvents.collect { event -> processEvent(event) } } + } + + fun stopProcessingCommands() { + demoCommandJob?.cancel() + } + + private fun processEvent(event: DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent) { + connectionState.value = event.connectionState + signalStrength.value = event.signalStrength + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt index 3e3ea855ccf7..0739b8362420 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt @@ -31,7 +31,7 @@ import com.android.systemui.log.core.LogLevel import com.android.systemui.log.core.MessageInitializer import com.android.systemui.log.core.MessagePrinter import com.android.systemui.statusbar.pipeline.dagger.OemSatelliteInputLog -import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository +import com.android.systemui.statusbar.pipeline.satellite.data.RealDeviceBasedSatelliteRepository import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Companion.whenSupported import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.NotSupported import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Supported @@ -50,12 +50,14 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext @@ -134,7 +136,7 @@ constructor( @Application private val scope: CoroutineScope, @OemSatelliteInputLog private val logBuffer: LogBuffer, private val systemClock: SystemClock, -) : DeviceBasedSatelliteRepository { +) : RealDeviceBasedSatelliteRepository { private val satelliteManager: SatelliteManager? @@ -200,10 +202,12 @@ constructor( } override val connectionState = - satelliteSupport.whenSupported( - supported = ::connectionStateFlow, - orElse = flowOf(SatelliteConnectionState.Off) - ) + satelliteSupport + .whenSupported( + supported = ::connectionStateFlow, + orElse = flowOf(SatelliteConnectionState.Off) + ) + .stateIn(scope, SharingStarted.Eagerly, SatelliteConnectionState.Off) // By using the SupportedSatelliteManager here, we expect registration never to fail private fun connectionStateFlow(sm: SupportedSatelliteManager): Flow<SatelliteConnectionState> = @@ -227,7 +231,9 @@ constructor( .flowOn(bgDispatcher) override val signalStrength = - satelliteSupport.whenSupported(supported = ::signalStrengthFlow, orElse = flowOf(0)) + satelliteSupport + .whenSupported(supported = ::signalStrengthFlow, orElse = flowOf(0)) + .stateIn(scope, SharingStarted.Eagerly, 0) // By using the SupportedSatelliteManager here, we expect registration never to fail private fun signalStrengthFlow(sm: SupportedSatelliteManager) = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt new file mode 100644 index 000000000000..7ca3b1c425d3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepositorySwitcherTest.kt @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2024 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.satellite.data + +import android.telephony.satellite.SatelliteManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.demomode.DemoMode +import com.android.systemui.demomode.DemoModeController +import com.android.systemui.log.core.FakeLogBuffer +import com.android.systemui.statusbar.pipeline.satellite.data.demo.DemoDeviceBasedSatelliteDataSource +import com.android.systemui.statusbar.pipeline.satellite.data.demo.DemoDeviceBasedSatelliteRepository +import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl +import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState +import com.android.systemui.util.mockito.kotlinArgumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.mockito.Mockito.verify + +@SmallTest +class DeviceBasedSatelliteRepositorySwitcherTest : SysuiTestCase() { + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private val demoModeController = + mock<DemoModeController>().apply { whenever(this.isInDemoMode).thenReturn(false) } + private val satelliteManager = mock<SatelliteManager>() + private val systemClock = FakeSystemClock() + + private val realImpl = + DeviceBasedSatelliteRepositoryImpl( + Optional.of(satelliteManager), + testDispatcher, + testScope.backgroundScope, + FakeLogBuffer.Factory.create(), + systemClock, + ) + private val demoDataSource = + mock<DemoDeviceBasedSatelliteDataSource>().also { + whenever(it.satelliteEvents) + .thenReturn( + MutableStateFlow( + DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent( + connectionState = SatelliteConnectionState.Unknown, + signalStrength = 0, + ) + ) + ) + } + private val demoImpl = + DemoDeviceBasedSatelliteRepository(demoDataSource, testScope.backgroundScope) + + private val underTest = + DeviceBasedSatelliteRepositorySwitcher( + realImpl, + demoImpl, + demoModeController, + testScope.backgroundScope, + ) + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun switcherActiveRepo_updatesWhenDemoModeChanges() = + testScope.runTest { + assertThat(underTest.activeRepo.value).isSameInstanceAs(realImpl) + + val latest by collectLastValue(underTest.activeRepo) + runCurrent() + + startDemoMode() + + assertThat(latest).isSameInstanceAs(demoImpl) + + finishDemoMode() + + assertThat(latest).isSameInstanceAs(realImpl) + } + + 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>() + verify(demoModeController).addCallback(captor.capture()) + return captor.value + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepositoryTest.kt new file mode 100644 index 000000000000..f77fd1999007 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/demo/DemoDeviceBasedSatelliteRepositoryTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2024 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.satellite.data.demo + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before + +@SmallTest +class DemoDeviceBasedSatelliteRepositoryTest : SysuiTestCase() { + + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private val fakeSatelliteEvents = + MutableStateFlow( + DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent( + connectionState = SatelliteConnectionState.Unknown, + signalStrength = 0, + ) + ) + + private lateinit var dataSource: DemoDeviceBasedSatelliteDataSource + + private lateinit var underTest: DemoDeviceBasedSatelliteRepository + + @Before + fun setUp() { + dataSource = + mock<DemoDeviceBasedSatelliteDataSource>().also { + whenever(it.satelliteEvents).thenReturn(fakeSatelliteEvents) + } + + underTest = DemoDeviceBasedSatelliteRepository(dataSource, testScope.backgroundScope) + } + + @Test + fun startProcessing_getsNewUpdates() = + testScope.runTest { + val latestConnection by collectLastValue(underTest.connectionState) + val latestSignalStrength by collectLastValue(underTest.signalStrength) + + underTest.startProcessingCommands() + + fakeSatelliteEvents.value = + DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent( + connectionState = SatelliteConnectionState.On, + signalStrength = 3, + ) + + assertThat(latestConnection).isEqualTo(SatelliteConnectionState.On) + assertThat(latestSignalStrength).isEqualTo(3) + + fakeSatelliteEvents.value = + DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent( + connectionState = SatelliteConnectionState.Connected, + signalStrength = 4, + ) + + assertThat(latestConnection).isEqualTo(SatelliteConnectionState.Connected) + assertThat(latestSignalStrength).isEqualTo(4) + } + + @Test + fun stopProcessing_stopsGettingUpdates() = + testScope.runTest { + val latestConnection by collectLastValue(underTest.connectionState) + val latestSignalStrength by collectLastValue(underTest.signalStrength) + + underTest.startProcessingCommands() + + fakeSatelliteEvents.value = + DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent( + connectionState = SatelliteConnectionState.On, + signalStrength = 3, + ) + assertThat(latestConnection).isEqualTo(SatelliteConnectionState.On) + assertThat(latestSignalStrength).isEqualTo(3) + + underTest.stopProcessingCommands() + + // WHEN new values are emitted + fakeSatelliteEvents.value = + DemoDeviceBasedSatelliteDataSource.DemoSatelliteEvent( + connectionState = SatelliteConnectionState.Connected, + signalStrength = 4, + ) + + // THEN they're not collected because we stopped processing commands, so the old values + // are still present + assertThat(latestConnection).isEqualTo(SatelliteConnectionState.On) + assertThat(latestSignalStrength).isEqualTo(3) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt index 77e48bff04de..6b0ad4bdb770 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt @@ -156,7 +156,7 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { verify(satelliteManager).registerForNtnSignalStrengthChanged(any(), capture()) } - assertThat(latest).isNull() + assertThat(latest).isEqualTo(0) callback.onNtnSignalStrengthChanged(NtnSignalStrength(1)) assertThat(latest).isEqualTo(1) |