[SB][Sat] Add demo repo for device-based satellite.
Fixes: 323522792
Test: See below:
1) `adb shell am broadcast -a com.android.systemui.demo -e command
enter` -> starts demo mode
2) `adb shell am broadcast -a com.android.systemui.demo -e command
network -e mobile show -e level 0` -> sets mobile to be
out-of-service
3) Wait 10s
4) Verify dimmed satellite icon appears
5) `adb shell am broadcast -a com.android.systemui.demo -e command
network -e satellite show -e level 4 -e connection connected` -> Verify
full satellite icon appears with level
6) Update the `level` value or `connection` value in the satellite
command -> Verify satellite icon adjusts appropriately
7) `adb shell am broadcast -a com.android.systemui.demo -e command
network -e mobile show -e level 1` -> sets mobile to be in-service,
so verify satellite icon disappears
Test: all unit tests in the statusbar.pipeline.satellite directory
Flag: ACONFIG com.android.internal.telephony.flags.oem_enabled_satellite_flag
NEXTFOOD
Change-Id: I43622852763f6ac1d28446e0afd77a8cb92d26e0
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 5deb08a7..cff46ab 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.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 @@
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 d7cbe5d..5b82cf7 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.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 @@
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 @@
* 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 @@
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 4f148f1..ad2ea2f 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 @@
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 bef0b28..08a890d 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 @@
* 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 4129b3a..5d91c7f 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.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 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 ad8b810..d38e834 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 @@
*/
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 0000000..6b1bc65
--- /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 0000000..fecd7fe
--- /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 0000000..56034f0
--- /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 3e3ea85..0739b836 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.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.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 @@
@Application private val scope: CoroutineScope,
@OemSatelliteInputLog private val logBuffer: LogBuffer,
private val systemClock: SystemClock,
-) : DeviceBasedSatelliteRepository {
+) : RealDeviceBasedSatelliteRepository {
private val satelliteManager: SatelliteManager?
@@ -200,10 +202,12 @@
}
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 @@
.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 0000000..7ca3b1c
--- /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 0000000..f77fd19
--- /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 77e48bff..6b0ad4b 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 @@
verify(satelliteManager).registerForNtnSignalStrengthChanged(any(), capture())
}
- assertThat(latest).isNull()
+ assertThat(latest).isEqualTo(0)
callback.onNtnSignalStrengthChanged(NtnSignalStrength(1))
assertThat(latest).isEqualTo(1)