Merge changes Ia4b79ae0,If1b71c97,I679039bd into main
* changes:
[Satellite] Initial data and domain layers for OEM satellite
[Sb] Add permissions for satellite_communication
[Sb] BindableIcon shim support for StatusBarIconConroller
diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml
index 43683ff..ce2543a 100644
--- a/data/etc/com.android.systemui.xml
+++ b/data/etc/com.android.systemui.xml
@@ -56,6 +56,7 @@
<permission name="android.permission.REAL_GET_TASKS"/>
<permission name="android.permission.REQUEST_NETWORK_SCORES"/>
<permission name="android.permission.RECEIVE_MEDIA_RESOURCE_USAGE"/>
+ <permission name="android.permission.SATELLITE_COMMUNICATION"/>
<permission name="android.permission.SET_WALLPAPER_DIM_AMOUNT"/>
<permission name="android.permission.START_ACTIVITIES_FROM_BACKGROUND" />
<permission name="android.permission.START_ACTIVITY_AS_CALLER"/>
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index a03fa9b..7443e4c 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -84,6 +84,7 @@
<uses-permission android:name="android.permission.READ_WIFI_CREDENTIAL"/>
<uses-permission android:name="android.permission.LOCATION_HARDWARE" />
<uses-permission android:name="android.permission.NETWORK_FACTORY" />
+ <uses-permission android:name="android.permission.SATELLITE_COMMUNICATION" />
<!-- Physical hardware -->
<uses-permission android:name="android.permission.MANAGE_USB" />
<uses-permission android:name="android.permission.CONTROL_DISPLAY_BRIGHTNESS" />
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
index 75d1869..a9ee405 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -68,6 +68,8 @@
override val isSingleCarrier = MutableStateFlow(true)
+ override val icons: MutableStateFlow<List<MobileIconInteractor>> = MutableStateFlow(emptyList())
+
private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
override val defaultMobileIconMapping = _defaultMobileIconMapping
@@ -80,8 +82,12 @@
override val isForceHidden = MutableStateFlow(false)
/** Always returns a new fake interactor */
- override fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor {
- return FakeMobileIconInteractor(tableLogBuffer).also { interactorCache[subId] = it }
+ override fun getMobileConnectionInteractorForSubId(subId: Int): FakeMobileIconInteractor {
+ return FakeMobileIconInteractor(tableLogBuffer).also {
+ interactorCache[subId] = it
+ // Also update the icons
+ icons.value = interactorCache.values.toList()
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
index 8b992fc..b2d7052 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java
@@ -91,6 +91,7 @@
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
+import android.telephony.satellite.SatelliteManager;
import android.view.Choreographer;
import android.view.CrossWindowBlurListeners;
import android.view.IWindowManager;
@@ -712,4 +713,10 @@
ServiceManager.getService(Context.URI_GRANTS_SERVICE)
);
}
+
+ @Provides
+ @Singleton
+ static Optional<SatelliteManager> provideSatelliteManager(Context context) {
+ return Optional.ofNullable(context.getSystemService(SatelliteManager.class));
+ }
}
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 9ae4195..d7cbe5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -14,6 +14,7 @@
package com.android.systemui.statusbar.phone;
+import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_BINDABLE;
import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_ICON;
import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE_NEW;
import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI_NEW;
@@ -40,11 +41,13 @@
import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.StatusIconDisplayable;
import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
+import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder;
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder;
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.WifiUiAdapter;
import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView;
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel;
@@ -432,6 +435,10 @@
case TYPE_MOBILE_NEW:
return addNewMobileIcon(index, slot, holder.getTag());
+
+ case TYPE_BINDABLE:
+ // Safe cast, since only BindableIconHolders can set this tag on themselves
+ return addBindableIcon((BindableIconHolder) holder, index);
}
return null;
@@ -446,6 +453,18 @@
return view;
}
+ /**
+ * ModernStatusBarViews can be created and bound, and thus do not need to update their
+ * drawable by sending multiple calls to setIcon. Instead, by using a bindable
+ * icon view, we can simply create the icon when requested and allow the
+ * ViewBinder to control its visual state.
+ */
+ protected StatusIconDisplayable addBindableIcon(BindableIconHolder holder, int index) {
+ ModernStatusBarView view = holder.getInitializer().createAndBind(mContext);
+ mGroup.addView(view, index, onCreateLayoutParams());
+ return view;
+ }
+
protected StatusIconDisplayable addNewWifiIcon(int index, String slot) {
ModernStatusBarWifiView view = onCreateModernStatusBarWifiView(slot);
mGroup.addView(view, index, onCreateLayoutParams());
@@ -530,6 +549,7 @@
return;
case TYPE_MOBILE_NEW:
case TYPE_WIFI_NEW:
+ case TYPE_BINDABLE:
// Nothing, the new icons update themselves
return;
default:
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 0f4d68c..4f148f1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -38,8 +38,11 @@
import com.android.systemui.dump.DumpManager;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusIconDisplayable;
+import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder;
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState;
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
+import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry;
+import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
import com.android.systemui.tuner.TunerService;
@@ -83,7 +86,8 @@
TunerService tunerService,
DumpManager dumpManager,
StatusBarIconList statusBarIconList,
- StatusBarPipelineFlags statusBarPipelineFlags
+ StatusBarPipelineFlags statusBarPipelineFlags,
+ BindableIconsRegistry modernIconsRegistry
) {
mStatusBarIconList = statusBarIconList;
mContext = context;
@@ -94,6 +98,28 @@
tunerService.addTunable(this, ICON_HIDE_LIST);
demoModeController.addCallback(this);
dumpManager.registerDumpable(getClass().getSimpleName(), this);
+
+ addModernBindableIcons(modernIconsRegistry);
+ }
+
+ /**
+ * BindableIcons will always produce ModernStatusBarViews, which will be initialized and bound
+ * upon being added to any icon group. Because their view policy does not require subsequent
+ * calls to setIcon(), we can simply register them all statically here and not have to build
+ * CoreStartables for each modern icon.
+ *
+ * @param registry a statically defined provider of the modern icons
+ */
+ private void addModernBindableIcons(BindableIconsRegistry registry) {
+ List<BindableIcon> icons = registry.getBindableIcons();
+
+ // Initialization point for the bindable (modern) icons. These icons get their own slot
+ // allocated immediately, and are required to control their own display properties
+ for (BindableIcon i : icons) {
+ if (i.getShouldBindIcon()) {
+ addBindableIcon(i);
+ }
+ }
}
/** */
@@ -182,6 +208,17 @@
mIconGroups.forEach(l -> l.onIconAdded(viewIndex, slot, hidden, holder));
}
+ void addBindableIcon(BindableIcon icon) {
+ StatusBarIconHolder existingHolder = mStatusBarIconList.getIconHolder(icon.getSlot(), 0);
+ // Expected to be null
+ if (existingHolder == null) {
+ BindableIconHolder bindableIcon = new BindableIconHolder(icon.getInitializer());
+ setIcon(icon.getSlot(), bindableIcon);
+ } else {
+ Log.e(TAG, "addBindableIcon called, but icon has already been added. Ignoring");
+ }
+ }
+
/** */
@Override
public void setIcon(String slot, int resourceId, CharSequence contentDescription) {
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 5b55a1e..bef0b28 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.kt
@@ -21,23 +21,24 @@
import android.os.UserHandle
import com.android.internal.statusbar.StatusBarIcon
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState
+import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator
/** Wraps [com.android.internal.statusbar.StatusBarIcon] so we can still have a uniform list */
-class StatusBarIconHolder private constructor() {
- @IntDef(TYPE_ICON, TYPE_MOBILE_NEW, TYPE_WIFI_NEW)
+open class StatusBarIconHolder private constructor() {
+ @IntDef(TYPE_ICON, TYPE_MOBILE_NEW, TYPE_WIFI_NEW, TYPE_BINDABLE)
@Retention(AnnotationRetention.SOURCE)
internal annotation class IconType
var icon: StatusBarIcon? = null
@IconType
- var type = TYPE_ICON
- private set
+ open var type = TYPE_ICON
+ internal set
var tag = 0
private set
- var isVisible: Boolean
+ open var isVisible: Boolean
get() =
when (type) {
TYPE_ICON -> icon!!.visible
@@ -45,6 +46,7 @@
// The new pipeline controls visibilities via the view model and
// view binder, so
// this is effectively an unused return value.
+ TYPE_BINDABLE,
TYPE_MOBILE_NEW,
TYPE_WIFI_NEW -> true
else -> true
@@ -55,6 +57,7 @@
}
when (type) {
TYPE_ICON -> icon!!.visible = visible
+ TYPE_BINDABLE,
TYPE_MOBILE_NEW,
TYPE_WIFI_NEW -> {}
}
@@ -94,6 +97,9 @@
)
const val TYPE_WIFI_NEW = 4
+ /** Only applicable to [BindableIconHolder] */
+ const val TYPE_BINDABLE = 5
+
/** Returns a human-readable string representing the given type. */
fun getTypeString(@IconType type: Int): String {
return when (type) {
@@ -154,4 +160,25 @@
return holder
}
}
+
+ /**
+ * Subclass of StatusBarIconHolder that is responsible only for the registration of an icon into
+ * the [StatusBarIconList]. A bindable icon takes care of its own display, including hiding
+ * itself under the correct conditions.
+ *
+ * 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.
+ */
+ class BindableIconHolder(val initializer: ModernStatusBarViewCreator) : 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)")
+ }
+ }
}
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 e1fd37f..89a2fb7 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
@@ -29,6 +29,8 @@
import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl
import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl
+import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry
+import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistryImpl
import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigCoreStartable
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileRepositorySwitcher
@@ -42,6 +44,8 @@
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl
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.prod.DeviceBasedSatelliteRepositoryImpl
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
import com.android.systemui.statusbar.pipeline.shared.ui.binder.CollapsedStatusBarViewBinder
@@ -76,8 +80,16 @@
abstract fun airplaneModeViewModel(impl: AirplaneModeViewModelImpl): AirplaneModeViewModel
@Binds
+ abstract fun bindableIconsRepository(impl: BindableIconsRegistryImpl): BindableIconsRegistry
+
+ @Binds
abstract fun connectivityRepository(impl: ConnectivityRepositoryImpl): ConnectivityRepository
+ @Binds
+ abstract fun deviceBasedSatelliteRepository(
+ impl: DeviceBasedSatelliteRepositoryImpl
+ ): DeviceBasedSatelliteRepository
+
@Binds abstract fun wifiRepository(impl: WifiRepositorySwitcher): WifiRepository
@Binds abstract fun wifiInteractor(impl: WifiInteractorImpl): WifiInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt
new file mode 100644
index 0000000..e3c3139
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 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.icons.shared
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon
+import javax.inject.Inject
+
+/**
+ * Bindable status bar icons represent icon descriptions which can be registered with
+ * StatusBarIconController and can also create their own bindings. A bound icon is responsible for
+ * its own updates via the [repeatWhenAttached] view lifecycle utility. Thus,
+ * StatusBarIconController can (and will) ignore any call to setIcon.
+ *
+ * In other words, these icons are bound once (at controller init) and they will control their
+ * visibility on their own (while their overall appearance remains at the discretion of
+ * StatusBarIconController, via the ModernStatusBarViewBinding interface).
+ */
+interface BindableIconsRegistry {
+ val bindableIcons: List<BindableIcon>
+}
+
+@SysUISingleton
+class BindableIconsRegistryImpl
+@Inject
+constructor(
+/** Bindables go here */
+) : BindableIconsRegistry {
+ /**
+ * Adding the injected bindables to this list will get them registered with
+ * StatusBarIconController
+ */
+ override val bindableIcons: List<BindableIcon> = listOf()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/BindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/BindableIcon.kt
new file mode 100644
index 0000000..9d0d838
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/BindableIcon.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 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.icons.shared.model
+
+/**
+ * A BindableIcon describes a status bar icon that can be housed in the [ModernStatusBarView]
+ * created by [initializer]. They can be registered statically for [BindableIconsRepositoryImpl].
+ *
+ * Typical usage would be to create an (@SysUISingleton) adapter class that implements the
+ * interface. For example:
+ * ```
+ * @SysuUISingleton
+ * class MyBindableIconAdapter
+ * @Inject constructor(
+ * // deps
+ * val viewModel: MyViewModel
+ * ) : BindableIcon {
+ * override val slot = "icon_slot_name"
+ *
+ * override val initializer = ModernStatusBarViewCreator() {
+ * SingleBindableStatusBarIconView.createView(context).also { iconView ->
+ * MyIconViewBinder.bind(iconView, viewModel)
+ * }
+ * }
+ *
+ * override fun shouldBind() = Flags.myFlag()
+ * }
+ * ```
+ *
+ * By defining this adapter (and injecting it into the repository), we get our icon registered with
+ * the legacy StatusBarIconController while proxying all updates to the view binder that is created
+ * elsewhere.
+ *
+ * Note that the initializer block defines a closure that can pull in the viewModel dependency
+ * without us having to store it directly in the icon controller.
+ */
+interface BindableIcon {
+ val slot: String
+ val initializer: ModernStatusBarViewCreator
+ val shouldBindIcon: Boolean
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/ModernStatusBarViewCreator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/ModernStatusBarViewCreator.kt
new file mode 100644
index 0000000..dbd5c1d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/model/ModernStatusBarViewCreator.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 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.icons.shared.model
+
+import android.content.Context
+import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView
+
+/**
+ * Defined as an interface (as opposed to a typealias) to simplify calling from java.
+ * [ModernStatusBarViewCreator.createAndBind] should return a constructed and bound
+ * [ModernStatusBarView].
+ */
+fun interface ModernStatusBarViewCreator {
+ fun createAndBind(context: Context): ModernStatusBarView
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index dad4093..39135c7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -71,6 +71,12 @@
/** List of subscriptions, potentially filtered for CBRS */
val filteredSubscriptions: Flow<List<SubscriptionModel>>
+ /**
+ * The current list of [MobileIconInteractor]s associated with the current list of
+ * [filteredSubscriptions]
+ */
+ val icons: StateFlow<List<MobileIconInteractor>>
+
/** True if the active mobile data subscription has data enabled */
val activeDataConnectionHasDataEnabled: StateFlow<Boolean>
@@ -259,6 +265,13 @@
}
}
+ override val icons =
+ filteredSubscriptions
+ .mapLatest { subs ->
+ subs.map { getMobileConnectionInteractorForSubId(it.subscriptionId) }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
+
/**
* Copied from the old pipeline. We maintain a 2s period of time where we will keep the
* validated bit from the old active network (A) while data is changing to the new one (B).
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
new file mode 100644
index 0000000..ad8b810
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/DeviceBasedSatelliteRepository.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 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 com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Device-based satellite refers to the capability of a device to connect directly to a satellite
+ * network. This is in contrast to carrier-based satellite connectivity, which is a property of a
+ * given mobile data subscription.
+ */
+interface DeviceBasedSatelliteRepository {
+ /** See [SatelliteConnectionState] for available states */
+ val connectionState: Flow<SatelliteConnectionState>
+
+ /** 0-4 level (similar to wifi and mobile) */
+ // @IntRange(from = 0, to = 4)
+ val signalStrength: Flow<Int>
+
+ /** Clients must observe this property, as device-based satellite is location-dependent */
+ val isSatelliteAllowedForCurrentLocation: Flow<Boolean>
+}
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
new file mode 100644
index 0000000..8fc8b2f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImpl.kt
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2023 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.prod
+
+import android.os.OutcomeReceiver
+import android.telephony.satellite.NtnSignalStrengthCallback
+import android.telephony.satellite.SatelliteManager
+import android.telephony.satellite.SatelliteStateCallback
+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.dagger.qualifiers.Background
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+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
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.SatelliteSupport.Unknown
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.util.kotlin.getOrNull
+import com.android.systemui.util.time.SystemClock
+import java.util.Optional
+import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+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.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+/**
+ * A SatelliteManager that has responded that it has satellite support. Use [SatelliteSupport] to
+ * get one
+ */
+private typealias SupportedSatelliteManager = SatelliteManager
+
+/**
+ * "Supported" here means supported by the device. The value of this should be stable during the
+ * process lifetime.
+ */
+private sealed interface SatelliteSupport {
+ /** Not yet fetched */
+ data object Unknown : SatelliteSupport
+
+ /**
+ * SatelliteManager says that this mode is supported. Note that satellite manager can never be
+ * null now
+ */
+ data class Supported(val satelliteManager: SupportedSatelliteManager) : SatelliteSupport
+
+ /**
+ * Either we were told that there is no support for this feature, or the manager is null, or
+ * some other exception occurred while querying for support.
+ */
+ data object NotSupported : SatelliteSupport
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ companion object {
+ /** Convenience function to switch to the supported flow */
+ fun <T> Flow<SatelliteSupport>.whenSupported(
+ supported: (SatelliteManager) -> Flow<T>,
+ orElse: Flow<T>,
+ ): Flow<T> = flatMapLatest {
+ when (it) {
+ is Supported -> supported(it.satelliteManager)
+ else -> orElse
+ }
+ }
+ }
+}
+
+/**
+ * Basically your everyday run-of-the-mill system service listener, with three notable exceptions.
+ *
+ * First, there is an availability bit that we are tracking via [SatelliteManager]. See
+ * [isSatelliteAllowedForCurrentLocation] for the implementation details. The thing to note about
+ * this bit is that there is no callback that exists. Therefore we implement a simple polling
+ * mechanism here. Since the underlying bit is location-dependent, we simply poll every hour (see
+ * [POLLING_INTERVAL_MS]) and see what the current state is.
+ *
+ * Secondly, there are cases when simply requesting information from SatelliteManager can fail. See
+ * [SatelliteSupport] for details on how we track the state. What's worth noting here is that
+ * SUPPORTED is a stronger guarantee than [satelliteManager] being null. Therefore, the fundamental
+ * data flows here ([connectionState], [signalStrength],...) are wrapped in the convenience method
+ * [SatelliteSupport.whenSupported]. By defining flows as simple functions based on a
+ * [SupportedSatelliteManager], we can guarantee that the manager is non-null AND that it has told
+ * us that satellite is supported. Therefore, we don't expect exceptions to be thrown.
+ *
+ * Lastly, this class is designed to wait a full minute of process uptime before making any requests
+ * to the satellite manager. The hope is that by waiting we don't have to retry due to a modem that
+ * is still booting up or anything like that. We can tune or remove this behavior in the future if
+ * necessary.
+ */
+@SysUISingleton
+class DeviceBasedSatelliteRepositoryImpl
+@Inject
+constructor(
+ satelliteManagerOpt: Optional<SatelliteManager>,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ @Application private val scope: CoroutineScope,
+ private val systemClock: SystemClock,
+) : DeviceBasedSatelliteRepository {
+
+ private val satelliteManager: SatelliteManager?
+
+ override val isSatelliteAllowedForCurrentLocation: MutableStateFlow<Boolean>
+
+ // Some calls into satellite manager will throw exceptions if it is not supported.
+ // This is never expected to change after boot, but may need to be retried in some cases
+ private val satelliteSupport: MutableStateFlow<SatelliteSupport> = MutableStateFlow(Unknown)
+
+ init {
+ satelliteManager = satelliteManagerOpt.getOrNull()
+
+ isSatelliteAllowedForCurrentLocation = MutableStateFlow(false)
+
+ if (satelliteManager != null) {
+ // First, check that satellite is supported on this device
+ scope.launch {
+ ensureMinUptime(systemClock, MIN_UPTIME)
+ satelliteSupport.value = satelliteManager.checkSatelliteSupported()
+
+ // We only need to check location availability if this mode is supported
+ if (satelliteSupport.value is Supported) {
+ isSatelliteAllowedForCurrentLocation.subscriptionCount
+ .map { it > 0 }
+ .distinctUntilChanged()
+ .collectLatest { hasSubscribers ->
+ if (hasSubscribers) {
+ /*
+ * As there is no listener available for checking satellite allowed,
+ * we must poll. Defaulting to polling at most once every hour while
+ * active. Subsequent OOS events will restart the job, so a flaky
+ * connection might cause more frequent checks.
+ */
+ while (true) {
+ checkIsSatelliteAllowed()
+ delay(POLLING_INTERVAL_MS)
+ }
+ }
+ }
+ }
+ }
+ } else {
+ satelliteSupport.value = NotSupported
+ }
+ }
+
+ override val connectionState =
+ satelliteSupport.whenSupported(
+ supported = ::connectionStateFlow,
+ orElse = flowOf(SatelliteConnectionState.Off)
+ )
+
+ // By using the SupportedSatelliteManager here, we expect registration never to fail
+ private fun connectionStateFlow(sm: SupportedSatelliteManager): Flow<SatelliteConnectionState> =
+ conflatedCallbackFlow {
+ val cb = SatelliteStateCallback { state ->
+ trySend(SatelliteConnectionState.fromModemState(state))
+ }
+
+ sm.registerForSatelliteModemStateChanged(bgDispatcher.asExecutor(), cb)
+
+ awaitClose { sm.unregisterForSatelliteModemStateChanged(cb) }
+ }
+ .flowOn(bgDispatcher)
+
+ override val signalStrength =
+ satelliteSupport.whenSupported(supported = ::signalStrengthFlow, orElse = flowOf(0))
+
+ // By using the SupportedSatelliteManager here, we expect registration never to fail
+ private fun signalStrengthFlow(sm: SupportedSatelliteManager) =
+ conflatedCallbackFlow {
+ val cb = NtnSignalStrengthCallback { signalStrength ->
+ trySend(signalStrength.level)
+ }
+
+ sm.registerForNtnSignalStrengthChanged(bgDispatcher.asExecutor(), cb)
+
+ awaitClose { sm.unregisterForNtnSignalStrengthChanged(cb) }
+ }
+ .flowOn(bgDispatcher)
+
+ /** Fire off a request to check for satellite availability. Always runs on the bg context */
+ private suspend fun checkIsSatelliteAllowed() =
+ withContext(bgDispatcher) {
+ satelliteManager?.requestIsSatelliteCommunicationAllowedForCurrentLocation(
+ bgDispatcher.asExecutor(),
+ object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
+ override fun onError(e: SatelliteManager.SatelliteException) {
+ android.util.Log.e(TAG, "Found exception when checking for satellite: ", e)
+ isSatelliteAllowedForCurrentLocation.value = false
+ }
+
+ override fun onResult(allowed: Boolean) {
+ isSatelliteAllowedForCurrentLocation.value = allowed
+ }
+ }
+ )
+ }
+
+ private suspend fun SatelliteManager.checkSatelliteSupported(): SatelliteSupport =
+ suspendCancellableCoroutine { continuation ->
+ val cb =
+ object : OutcomeReceiver<Boolean, SatelliteManager.SatelliteException> {
+ override fun onResult(supported: Boolean) {
+ continuation.resume(
+ if (supported) {
+ Supported(satelliteManager = this@checkSatelliteSupported)
+ } else {
+ NotSupported
+ }
+ )
+ }
+
+ override fun onError(error: SatelliteManager.SatelliteException) {
+ // Assume that an error means it's not supported
+ continuation.resume(NotSupported)
+ }
+ }
+
+ requestIsSatelliteSupported(bgDispatcher.asExecutor(), cb)
+ }
+
+ companion object {
+ // TTL for satellite polling is one hour
+ const val POLLING_INTERVAL_MS: Long = 1000 * 60 * 60
+
+ // Let the system boot up and stabilize before we check for system support
+ const val MIN_UPTIME: Long = 1000 * 60
+
+ private const val TAG = "DeviceBasedSatelliteRepo"
+
+ /** If our process hasn't been up for at least MIN_UPTIME, delay until we reach that time */
+ private suspend fun ensureMinUptime(clock: SystemClock, uptime: Long) {
+ val timeTilMinUptime =
+ uptime - (clock.uptimeMillis() - android.os.Process.getStartUptimeMillis())
+ if (timeTilMinUptime > 0) {
+ delay(timeTilMinUptime)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
new file mode 100644
index 0000000..8779577
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 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.domain.interactor
+
+import com.android.internal.telephony.flags.Flags
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+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.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+@SysUISingleton
+class DeviceBasedSatelliteInteractor
+@Inject
+constructor(
+ val repo: DeviceBasedSatelliteRepository,
+ iconsInteractor: MobileIconsInteractor,
+ @Application scope: CoroutineScope,
+) {
+ /** Must be observed by any UI showing Satellite iconography */
+ val isSatelliteAllowed =
+ if (Flags.oemEnabledSatelliteFlag()) {
+ repo.isSatelliteAllowedForCurrentLocation
+ } else {
+ flowOf(false)
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ /** See [SatelliteConnectionState] for relevant states */
+ val connectionState =
+ if (Flags.oemEnabledSatelliteFlag()) {
+ repo.connectionState
+ } else {
+
+ flowOf(SatelliteConnectionState.Off)
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), SatelliteConnectionState.Off)
+
+ /** 0-4 description of the connection strength */
+ val signalStrength =
+ if (Flags.oemEnabledSatelliteFlag()) {
+ repo.signalStrength
+ } else {
+ flowOf(0)
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
+
+ /** When all connections are considered OOS, satellite connectivity is potentially valid */
+ val areAllConnectionsOutOfService =
+ if (Flags.oemEnabledSatelliteFlag()) {
+ iconsInteractor.icons.aggregateOver(selector = { intr -> intr.isInService }) {
+ isInServiceList ->
+ isInServiceList.all { !it }
+ }
+ } else {
+ flowOf(false)
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+}
+
+/**
+ * aggregateOver allows us to combine over the leaf-nodes of successive lists emitted from the
+ * top-level flow. Re-emits if the list changes, or any of the intermediate values change.
+ *
+ * Provides a way to connect the reactivity of the top-level flow with the reactivity of an
+ * arbitrarily-defined relationship ([selector]) from R to the flow that R exposes.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+private inline fun <R, reified S, T> Flow<List<R>>.aggregateOver(
+ crossinline selector: (R) -> Flow<S>,
+ crossinline transform: (Array<S>) -> T
+): Flow<T> {
+ return map { list -> list.map { selector(it) } }
+ .flatMapLatest { newFlows -> combine(newFlows) { newVals -> transform(newVals) } }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt
new file mode 100644
index 0000000..bfe2941
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/shared/model/SatelliteConnectionState.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2023 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.shared.model
+
+import android.telephony.satellite.SatelliteManager
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_RETRYING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_IDLE
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_LISTENING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_NOT_CONNECTED
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_OFF
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN
+
+enum class SatelliteConnectionState {
+ // State is unknown or undefined
+ Unknown,
+ // Radio is off
+ Off,
+ // Radio is on, but not yet connected
+ On,
+ // Radio is connected, aka satellite is available for use
+ Connected;
+
+ companion object {
+ // TODO(b/316635648): validate these states. We don't need the level of granularity that
+ // SatelliteManager gives us.
+ fun fromModemState(@SatelliteManager.SatelliteModemState modemState: Int) =
+ when (modemState) {
+ // Transferring data is connected
+ SATELLITE_MODEM_STATE_CONNECTED,
+ SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING,
+ SATELLITE_MODEM_STATE_DATAGRAM_RETRYING -> Connected
+
+ // Modem is on but not connected
+ SATELLITE_MODEM_STATE_IDLE,
+ SATELLITE_MODEM_STATE_LISTENING,
+ SATELLITE_MODEM_STATE_NOT_CONNECTED -> On
+
+ // Consider unavailable equivalent to Off
+ SATELLITE_MODEM_STATE_UNAVAILABLE,
+ SATELLITE_MODEM_STATE_OFF -> Off
+
+ // Else, we don't know what's up
+ SATELLITE_MODEM_STATE_UNKNOWN -> Unknown
+ else -> Unknown
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt
index 6f04f36..f6a8243 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImplTest.kt
@@ -23,6 +23,9 @@
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.phone.StatusBarIconController.TAG_PRIMARY
import com.android.systemui.statusbar.phone.StatusBarIconControllerImpl.EXTERNAL_SLOT_SUFFIX
+import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry
+import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon
+import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator
import com.android.systemui.util.mockito.kotlinArgumentCaptor
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
@@ -49,14 +52,15 @@
iconList = StatusBarIconList(arrayOf())
underTest =
StatusBarIconControllerImpl(
- context,
- commandQueue,
- mock(),
- mock(),
- mock(),
- mock(),
- iconList,
- mock(),
+ /* context = */ context,
+ /* commandQueue = */ commandQueue,
+ /* demoModeController = */ mock(),
+ /* configurationController = */ mock(),
+ /* tunerService = */ mock(),
+ /* dumpManager = */ mock(),
+ /* statusBarIconList = */ iconList,
+ /* statusBarPipelineFlags = */ mock(),
+ /* modernIconsRegistry = */ mock(),
)
underTest.addIconGroup(iconGroup)
val commandQueueCallbacksCaptor = kotlinArgumentCaptor<CommandQueue.Callbacks>()
@@ -366,6 +370,31 @@
assertThat(iconList.slots[0].name).isEqualTo("myslot$EXTERNAL_SLOT_SUFFIX")
}
+ @Test
+ fun bindableIcons_addedOnInit() {
+ val fakeIcon = FakeBindableIcon("test_slot")
+
+ iconList = StatusBarIconList(arrayOf())
+
+ // WHEN there are registered icons
+ underTest =
+ StatusBarIconControllerImpl(
+ /* context = */ context,
+ /* commandQueue = */ commandQueue,
+ /* demoModeController = */ mock(),
+ /* configurationController = */ mock(),
+ /* tunerService = */ mock(),
+ /* dumpManager = */ mock(),
+ /* statusBarIconList = */ iconList,
+ /* statusBarPipelineFlags = */ mock(),
+ /* modernIconsRegistry = */ FakeBindableIconsRegistry(listOf(fakeIcon)),
+ )
+
+ // THEN they are properly added to the list on init
+ assertThat(iconList.getIconHolder("test_slot", 0))
+ .isInstanceOf(StatusBarIconHolder.BindableIconHolder::class.java)
+ }
+
private fun createExternalIcon(): StatusBarIcon {
return StatusBarIcon(
"external.package",
@@ -377,3 +406,20 @@
)
}
}
+
+class FakeBindableIconsRegistry(
+ override val bindableIcons: List<BindableIcon>,
+) : BindableIconsRegistry
+
+class FakeBindableIcon(
+ override val slot: String,
+ override val shouldBindIcon: Boolean = true,
+) : BindableIcon {
+ // Track initialized so we can know that our icon was properly bound
+ var hasInitialized = false
+
+ override val initializer = ModernStatusBarViewCreator { _ ->
+ hasInitialized = true
+ mock()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
index 0ff6f20..ca31623 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
@@ -43,6 +43,7 @@
import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager;
import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager;
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
+import com.android.systemui.statusbar.pipeline.icons.shared.BindableIconsRegistry;
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
@@ -108,7 +109,8 @@
mock(TunerService.class),
mock(DumpManager.class),
mock(StatusBarIconList.class),
- flags
+ flags,
+ mock(BindableIconsRegistry.class)
);
iconController.addIconGroup(manager);
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
new file mode 100644
index 0000000..a906a89
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2023 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.prod
+
+import android.os.OutcomeReceiver
+import android.os.Process
+import android.telephony.satellite.NtnSignalStrength
+import android.telephony.satellite.NtnSignalStrengthCallback
+import android.telephony.satellite.SatelliteManager
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_CONNECTED
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_RETRYING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_IDLE
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_LISTENING
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_NOT_CONNECTED
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_OFF
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNAVAILABLE
+import android.telephony.satellite.SatelliteManager.SATELLITE_MODEM_STATE_UNKNOWN
+import android.telephony.satellite.SatelliteManager.SatelliteException
+import android.telephony.satellite.SatelliteStateCallback
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.MIN_UPTIME
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.DeviceBasedSatelliteRepositoryImpl.Companion.POLLING_INTERVAL_MS
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() {
+ private lateinit var underTest: DeviceBasedSatelliteRepositoryImpl
+
+ @Mock private lateinit var satelliteManager: SatelliteManager
+
+ private val systemClock = FakeSystemClock()
+ private val dispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(dispatcher)
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ }
+
+ @Test
+ fun nullSatelliteManager_usesDefaultValues() =
+ testScope.runTest {
+ setupDefaultRepo()
+ underTest =
+ DeviceBasedSatelliteRepositoryImpl(
+ Optional.empty(),
+ dispatcher,
+ testScope.backgroundScope,
+ systemClock,
+ )
+
+ val connectionState by collectLastValue(underTest.connectionState)
+ val strength by collectLastValue(underTest.signalStrength)
+ val allowed by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+
+ assertThat(connectionState).isEqualTo(SatelliteConnectionState.Off)
+ assertThat(strength).isEqualTo(0)
+ assertThat(allowed).isFalse()
+ }
+
+ @Test
+ fun connectionState_mapsFromSatelliteModemState() =
+ testScope.runTest {
+ setupDefaultRepo()
+ val latest by collectLastValue(underTest.connectionState)
+ runCurrent()
+ val callback =
+ withArgCaptor<SatelliteStateCallback> {
+ verify(satelliteManager).registerForSatelliteModemStateChanged(any(), capture())
+ }
+
+ // Mapping from modem state to SatelliteConnectionState is rote, just run all of the
+ // possibilities here
+
+ // Off states
+ callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_OFF)
+ assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+ callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_UNAVAILABLE)
+ assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+
+ // On states
+ callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_IDLE)
+ assertThat(latest).isEqualTo(SatelliteConnectionState.On)
+ callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_LISTENING)
+ assertThat(latest).isEqualTo(SatelliteConnectionState.On)
+ callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_NOT_CONNECTED)
+ assertThat(latest).isEqualTo(SatelliteConnectionState.On)
+
+ // Connected states
+ callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_CONNECTED)
+ assertThat(latest).isEqualTo(SatelliteConnectionState.Connected)
+ callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING)
+ assertThat(latest).isEqualTo(SatelliteConnectionState.Connected)
+ callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_DATAGRAM_RETRYING)
+ assertThat(latest).isEqualTo(SatelliteConnectionState.Connected)
+
+ // Unknown states
+ callback.onSatelliteModemStateChanged(SATELLITE_MODEM_STATE_UNKNOWN)
+ assertThat(latest).isEqualTo(SatelliteConnectionState.Unknown)
+ // Garbage value (for completeness' sake)
+ callback.onSatelliteModemStateChanged(123456)
+ assertThat(latest).isEqualTo(SatelliteConnectionState.Unknown)
+ }
+
+ @Test
+ fun signalStrength_readsSatelliteManagerState() =
+ testScope.runTest {
+ setupDefaultRepo()
+ val latest by collectLastValue(underTest.signalStrength)
+ runCurrent()
+ val callback =
+ withArgCaptor<NtnSignalStrengthCallback> {
+ verify(satelliteManager).registerForNtnSignalStrengthChanged(any(), capture())
+ }
+
+ assertThat(latest).isNull()
+
+ callback.onNtnSignalStrengthChanged(NtnSignalStrength(1))
+ assertThat(latest).isEqualTo(1)
+
+ callback.onNtnSignalStrengthChanged(NtnSignalStrength(2))
+ assertThat(latest).isEqualTo(2)
+
+ callback.onNtnSignalStrengthChanged(NtnSignalStrength(3))
+ assertThat(latest).isEqualTo(3)
+
+ callback.onNtnSignalStrengthChanged(NtnSignalStrength(4))
+ assertThat(latest).isEqualTo(4)
+ }
+
+ @Test
+ fun isSatelliteAllowed_readsSatelliteManagerState_enabled() =
+ testScope.runTest {
+ setupDefaultRepo()
+ // GIVEN satellite is allowed in this location
+ val allowed = true
+
+ doAnswer {
+ val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+ receiver.onResult(allowed)
+ null
+ }
+ .`when`(satelliteManager)
+ .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+ any(),
+ any<OutcomeReceiver<Boolean, SatelliteException>>()
+ )
+
+ val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun isSatelliteAllowed_readsSatelliteManagerState_disabled() =
+ testScope.runTest {
+ setupDefaultRepo()
+ // GIVEN satellite is not allowed in this location
+ val allowed = false
+
+ doAnswer {
+ val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+ receiver.onResult(allowed)
+ null
+ }
+ .`when`(satelliteManager)
+ .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+ any(),
+ any<OutcomeReceiver<Boolean, SatelliteException>>()
+ )
+
+ val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun isSatelliteAllowed_pollsOnTimeout() =
+ testScope.runTest {
+ setupDefaultRepo()
+ // GIVEN satellite is not allowed in this location
+ var allowed = false
+
+ doAnswer {
+ val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+ receiver.onResult(allowed)
+ null
+ }
+ .`when`(satelliteManager)
+ .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+ any(),
+ any<OutcomeReceiver<Boolean, SatelliteException>>()
+ )
+
+ val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+
+ assertThat(latest).isFalse()
+
+ // WHEN satellite becomes enabled
+ allowed = true
+
+ // WHEN the timeout has not yet been reached
+ advanceTimeBy(POLLING_INTERVAL_MS / 2)
+
+ // THEN the value is still false
+ assertThat(latest).isFalse()
+
+ // WHEN time advances beyond the polling interval
+ advanceTimeBy(POLLING_INTERVAL_MS / 2 + 1)
+
+ // THEN then new value is emitted
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun isSatelliteAllowed_pollingRestartsWhenCollectionRestarts() =
+ testScope.runTest {
+ setupDefaultRepo()
+ // Use the old school launch/cancel so we can simulate subscribers arriving and leaving
+
+ var latest: Boolean? = false
+ var job =
+ underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this)
+
+ // GIVEN satellite is not allowed in this location
+ var allowed = false
+
+ doAnswer {
+ val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+ receiver.onResult(allowed)
+ null
+ }
+ .`when`(satelliteManager)
+ .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+ any(),
+ any<OutcomeReceiver<Boolean, SatelliteException>>()
+ )
+
+ assertThat(latest).isFalse()
+
+ // WHEN satellite becomes enabled
+ allowed = true
+
+ // WHEN the job is restarted
+ advanceTimeBy(POLLING_INTERVAL_MS / 2)
+
+ job.cancel()
+ job =
+ underTest.isSatelliteAllowedForCurrentLocation.onEach { latest = it }.launchIn(this)
+
+ // THEN the value is re-fetched
+ assertThat(latest).isTrue()
+
+ job.cancel()
+ }
+
+ @Test
+ fun isSatelliteAllowed_falseWhenErrorOccurs() =
+ testScope.runTest {
+ setupDefaultRepo()
+ doAnswer {
+ val receiver = it.arguments[1] as OutcomeReceiver<Boolean, SatelliteException>
+ receiver.onError(SatelliteException(1 /* unused */))
+ null
+ }
+ .`when`(satelliteManager)
+ .requestIsSatelliteCommunicationAllowedForCurrentLocation(
+ any(),
+ any<OutcomeReceiver<Boolean, SatelliteException>>()
+ )
+
+ val latest by collectLastValue(underTest.isSatelliteAllowedForCurrentLocation)
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun satelliteNotSupported_listenersAreNotRegistered() =
+ testScope.runTest {
+ setupDefaultRepo()
+ // GIVEN satellite is not supported
+ setUpRepo(
+ uptime = MIN_UPTIME,
+ satMan = satelliteManager,
+ satelliteSupported = false,
+ )
+
+ // WHEN data is requested from the repo
+ val connectionState by collectLastValue(underTest.connectionState)
+ val signalStrength by collectLastValue(underTest.signalStrength)
+
+ // THEN the manager is not asked for the information, and default values are returned
+ verify(satelliteManager, never()).registerForSatelliteModemStateChanged(any(), any())
+ verify(satelliteManager, never()).registerForNtnSignalStrengthChanged(any(), any())
+ }
+
+ @Test
+ fun repoDoesNotCheckForSupportUntilMinUptime() =
+ testScope.runTest {
+ // GIVEN we init 100ms after sysui starts up
+ setUpRepo(
+ uptime = 100,
+ satMan = satelliteManager,
+ satelliteSupported = true,
+ )
+
+ // WHEN data is requested
+ val connectionState by collectLastValue(underTest.connectionState)
+ val signalStrength by collectLastValue(underTest.signalStrength)
+
+ // THEN we have not yet talked to satellite manager, since we are well before MIN_UPTIME
+ Mockito.verifyZeroInteractions(satelliteManager)
+
+ // WHEN enough time has passed
+ systemClock.advanceTime(MIN_UPTIME)
+ runCurrent()
+
+ // THEN we finally register with the satellite manager
+ verify(satelliteManager).registerForSatelliteModemStateChanged(any(), any())
+ }
+
+ private fun setUpRepo(
+ uptime: Long = MIN_UPTIME,
+ satMan: SatelliteManager? = satelliteManager,
+ satelliteSupported: Boolean = true,
+ ) {
+ doAnswer {
+ val callback: OutcomeReceiver<Boolean, SatelliteException> =
+ it.getArgument(1) as OutcomeReceiver<Boolean, SatelliteException>
+ callback.onResult(satelliteSupported)
+ }
+ .whenever(satelliteManager)
+ .requestIsSatelliteSupported(any(), any())
+
+ systemClock.setUptimeMillis(Process.getStartUptimeMillis() + uptime)
+
+ underTest =
+ DeviceBasedSatelliteRepositoryImpl(
+ if (satMan != null) Optional.of(satMan) else Optional.empty(),
+ dispatcher,
+ testScope.backgroundScope,
+ systemClock,
+ )
+ }
+
+ // Set system time to MIN_UPTIME and create a repo with satellite supported
+ private fun setupDefaultRepo() {
+ setUpRepo(uptime = MIN_UPTIME, satMan = satelliteManager, satelliteSupported = true)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt
new file mode 100644
index 0000000..5fa2d33
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/FakeDeviceBasedSatelliteRepository.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 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.prod
+
+import com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState.Off
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeDeviceBasedSatelliteRepository() : DeviceBasedSatelliteRepository {
+ override val connectionState = MutableStateFlow(Off)
+
+ override val signalStrength = MutableStateFlow(0)
+
+ override val isSatelliteAllowedForCurrentLocation = MutableStateFlow(false)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
new file mode 100644
index 0000000..e010b86
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2023 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.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.internal.telephony.flags.Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository
+import com.android.systemui.statusbar.pipeline.satellite.shared.model.SatelliteConnectionState
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+
+@SmallTest
+class DeviceBasedSatelliteInteractorTest : SysuiTestCase() {
+ private lateinit var underTest: DeviceBasedSatelliteInteractor
+
+ private val dispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(dispatcher)
+
+ private val iconsInteractor =
+ FakeMobileIconsInteractor(
+ FakeMobileMappingsProxy(),
+ mock(),
+ )
+
+ private val repo = FakeDeviceBasedSatelliteRepository()
+
+ @Before
+ fun setUp() {
+ mSetFlagsRule.enableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+ underTest =
+ DeviceBasedSatelliteInteractor(
+ repo,
+ iconsInteractor,
+ testScope.backgroundScope,
+ )
+ }
+
+ @Test
+ fun isSatelliteAllowed_falseWhenNotAllowed() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.isSatelliteAllowed)
+
+ // WHEN satellite is allowed
+ repo.isSatelliteAllowedForCurrentLocation.value = false
+
+ // THEN the interactor returns false due to the flag value
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun isSatelliteAllowed_trueWhenAllowed() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.isSatelliteAllowed)
+
+ // WHEN satellite is allowed
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+
+ // THEN the interactor returns false due to the flag value
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun isSatelliteAllowed_offWhenFlagIsOff() =
+ testScope.runTest {
+ // GIVEN feature is disabled
+ mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+ // Remake the interactor so the flag is read
+ underTest =
+ DeviceBasedSatelliteInteractor(
+ repo,
+ iconsInteractor,
+ testScope.backgroundScope,
+ )
+
+ val latest by collectLastValue(underTest.isSatelliteAllowed)
+
+ // WHEN satellite is allowed
+ repo.isSatelliteAllowedForCurrentLocation.value = true
+
+ // THEN the interactor returns false due to the flag value
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun connectionState_matchesRepositoryValue() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.connectionState)
+
+ // Off
+ repo.connectionState.value = SatelliteConnectionState.Off
+ assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+
+ // On
+ repo.connectionState.value = SatelliteConnectionState.On
+ assertThat(latest).isEqualTo(SatelliteConnectionState.On)
+
+ // Connected
+ repo.connectionState.value = SatelliteConnectionState.Connected
+ assertThat(latest).isEqualTo(SatelliteConnectionState.Connected)
+
+ // Unknown
+ repo.connectionState.value = SatelliteConnectionState.Unknown
+ assertThat(latest).isEqualTo(SatelliteConnectionState.Unknown)
+ }
+
+ @Test
+ fun connectionState_offWhenFeatureIsDisabled() =
+ testScope.runTest {
+ // GIVEN the flag is disabled
+ mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+ // Remake the interactor so the flag is read
+ underTest =
+ DeviceBasedSatelliteInteractor(
+ repo,
+ iconsInteractor,
+ testScope.backgroundScope,
+ )
+
+ val latest by collectLastValue(underTest.connectionState)
+
+ // THEN the state is always Off, regardless of status in system_server
+
+ // Off
+ repo.connectionState.value = SatelliteConnectionState.Off
+ assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+
+ // On
+ repo.connectionState.value = SatelliteConnectionState.On
+ assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+
+ // Connected
+ repo.connectionState.value = SatelliteConnectionState.Connected
+ assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+
+ // Unknown
+ repo.connectionState.value = SatelliteConnectionState.Unknown
+ assertThat(latest).isEqualTo(SatelliteConnectionState.Off)
+ }
+
+ @Test
+ fun signalStrength_matchesRepo() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.signalStrength)
+
+ repo.signalStrength.value = 1
+ assertThat(latest).isEqualTo(1)
+
+ repo.signalStrength.value = 2
+ assertThat(latest).isEqualTo(2)
+
+ repo.signalStrength.value = 3
+ assertThat(latest).isEqualTo(3)
+
+ repo.signalStrength.value = 4
+ assertThat(latest).isEqualTo(4)
+ }
+
+ @Test
+ fun signalStrength_zeroWhenDisabled() =
+ testScope.runTest {
+ // GIVEN the flag is enabled
+ mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+ // Remake the interactor so the flag is read
+ underTest =
+ DeviceBasedSatelliteInteractor(
+ repo,
+ iconsInteractor,
+ testScope.backgroundScope,
+ )
+
+ val latest by collectLastValue(underTest.signalStrength)
+
+ // THEN the value is always 0, regardless of what the system says
+ repo.signalStrength.value = 1
+ assertThat(latest).isEqualTo(0)
+
+ repo.signalStrength.value = 2
+ assertThat(latest).isEqualTo(0)
+
+ repo.signalStrength.value = 3
+ assertThat(latest).isEqualTo(0)
+
+ repo.signalStrength.value = 4
+ assertThat(latest).isEqualTo(0)
+ }
+
+ @Test
+ fun areAllConnectionsOutOfService_twoConnectionsOos_yes() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+ // GIVEN, 2 connections
+ val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+ val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2)
+
+ // WHEN all of the connections are OOS
+ i1.isInService.value = false
+ i2.isInService.value = false
+
+ // THEN the value is propagated to this interactor
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun areAllConnectionsOutOfService_oneConnectionOos_yes() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+ // GIVEN, 1 connection
+ val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+
+ // WHEN all of the connections are OOS
+ i1.isInService.value = false
+
+ // THEN the value is propagated to this interactor
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun areAllConnectionsOutOfService_oneConnectionInService_no() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+ // GIVEN, 1 connection
+ val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+
+ // WHEN all of the connections are NOT OOS
+ i1.isInService.value = true
+
+ // THEN the value is propagated to this interactor
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun areAllConnectionsOutOfService_twoConnectionsOneInService_no() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+ // GIVEN, 2 connection
+ val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+ val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(2)
+
+ // WHEN at least 1 connection is NOT OOS.
+ i1.isInService.value = false
+ i2.isInService.value = true
+
+ // THEN the value is propagated to this interactor
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun areAllConnectionsOutOfService_twoConnectionsInService_no() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+ // GIVEN, 2 connection
+ val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+ val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+
+ // WHEN all connections are NOT OOS.
+ i1.isInService.value = true
+ i2.isInService.value = true
+
+ // THEN the value is propagated to this interactor
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun areAllConnectionsOutOfService_falseWhenFlagIsOff() =
+ testScope.runTest {
+ // GIVEN the flag is disabled
+ mSetFlagsRule.disableFlags(FLAG_OEM_ENABLED_SATELLITE_FLAG)
+
+ // Remake the interactor so the flag is read
+ underTest =
+ DeviceBasedSatelliteInteractor(
+ repo,
+ iconsInteractor,
+ testScope.backgroundScope,
+ )
+
+ val latest by collectLastValue(underTest.areAllConnectionsOutOfService)
+
+ // GIVEN a condition that should return true (all conections OOS)
+
+ val i1 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+ val i2 = iconsInteractor.getMobileConnectionInteractorForSubId(1)
+
+ i1.isInService.value = true
+ i2.isInService.value = true
+
+ // THEN the value is still false, because the flag is off
+ assertThat(latest).isFalse()
+ }
+}