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()
+        }
+}