[kairos] Kairos in mobile pipeline view binders

Flag: com.android.systemui.status_bar_mobile_icon_kairos
Bug: 383172066
Test: atest

Change-Id: I0922d00d3128a6a3f182c58fb60f7bc803e18bbd
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index 86c8fc3..addb08f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -43,6 +43,7 @@
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -67,12 +68,15 @@
 import com.android.compose.modifiers.thenIf
 import com.android.compose.theme.colorAttr
 import com.android.settingslib.Utils
+import com.android.systemui.Flags
 import com.android.systemui.battery.BatteryMeterView
 import com.android.systemui.battery.BatteryMeterViewController
 import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
 import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
 import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
 import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.buildSpec
 import com.android.systemui.privacy.OngoingPrivacyChip
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.model.Scenes
@@ -86,8 +90,12 @@
 import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.phone.StatusIconContainer
 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModelKairosComposeWrapper
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModelKairos
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.composeWrapper
 import com.android.systemui.statusbar.policy.Clock
+import com.android.systemui.util.composable.kairos.ActivatedKairosSpec
 
 object ShadeHeader {
     object Elements {
@@ -520,8 +528,14 @@
     )
 }
 
+@OptIn(ExperimentalKairosApi::class)
 @Composable
 private fun ShadeCarrierGroup(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier) {
+    if (Flags.statusBarMobileIconKairos()) {
+        ShadeCarrierGroupKairos(viewModel, modifier)
+        return
+    }
+
     Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
         for (subId in viewModel.mobileSubIds) {
             AndroidView(
@@ -543,6 +557,49 @@
     }
 }
 
+@ExperimentalKairosApi
+@Composable
+private fun ShadeCarrierGroupKairos(
+    viewModel: ShadeHeaderViewModel,
+    modifier: Modifier = Modifier,
+) {
+    Row(modifier = modifier) {
+        ActivatedKairosSpec(
+            buildSpec = viewModel.mobileIconsViewModelKairos.get().composeWrapper(),
+            kairosNetwork = viewModel.kairosNetwork,
+        ) { iconsViewModel: MobileIconsViewModelKairosComposeWrapper ->
+            for ((subId, icon) in iconsViewModel.icons) {
+                Spacer(modifier = Modifier.width(5.dp))
+                val scope = rememberCoroutineScope()
+                AndroidView(
+                    factory = { context ->
+                        ModernShadeCarrierGroupMobileView.constructAndBind(
+                                context = context,
+                                logger = iconsViewModel.logger,
+                                slot = "mobile_carrier_shade_group",
+                                viewModel =
+                                    buildSpec {
+                                        ShadeCarrierGroupMobileIconViewModelKairos(
+                                            icon,
+                                            icon.iconInteractor,
+                                        )
+                                    },
+                                scope = scope,
+                                subscriptionId = subId,
+                                location = StatusBarLocation.SHADE_CARRIER_GROUP,
+                                kairosNetwork = viewModel.kairosNetwork,
+                            )
+                            .first
+                            .also {
+                                it.setOnClickListener { viewModel.onShadeCarrierGroupClicked() }
+                            }
+                    }
+                )
+            }
+        }
+    }
+}
+
 @Composable
 private fun ContentScope.StatusIcons(
     viewModel: ShadeHeaderViewModel,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java
index cd55bb2..6f1150e5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java
@@ -46,6 +46,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.keyguard.CarrierTextManager;
+import com.android.systemui.kairos.KairosNetwork;
 import com.android.systemui.log.core.FakeLogBuffer;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.statusbar.connectivity.IconState;
@@ -55,6 +56,7 @@
 import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos;
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger;
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel;
@@ -63,6 +65,7 @@
 import com.android.systemui.utils.leaks.LeakCheckedTest;
 import com.android.systemui.utils.os.FakeHandler;
 
+import kotlinx.coroutines.CoroutineScope;
 import kotlinx.coroutines.flow.MutableStateFlow;
 
 import org.junit.Before;
@@ -178,8 +181,10 @@
                 mSlotIndexResolver,
                 mMobileUiAdapter,
                 mMobileContextProvider,
-                mStatusBarPipelineFlags
-        )
+                mStatusBarPipelineFlags,
+                mock(CoroutineScope.class),
+                mock(KairosNetwork.class),
+                () -> mock(MobileUiAdapterKairos.class))
                 .setShadeCarrierGroup(mShadeCarrierGroup)
                 .build();
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/IconManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/IconManagerTest.kt
index 90732d01..318eb87 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/IconManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/IconManagerTest.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalKairosApi::class)
+
 package com.android.systemui.statusbar.phone.ui
 
 import android.app.Flags
@@ -28,13 +30,17 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.statusbar.StatusBarIcon
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider
 import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos
 import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter
 import com.android.systemui.util.Assert
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -58,7 +64,10 @@
                 StatusBarLocation.HOME,
                 mock<WifiUiAdapter>(defaultAnswer = RETURNS_DEEP_STUBS),
                 mock<MobileUiAdapter>(defaultAnswer = RETURNS_DEEP_STUBS),
+                { mock<MobileUiAdapterKairos>(defaultAnswer = RETURNS_DEEP_STUBS) },
                 mock<MobileContextProvider>(defaultAnswer = RETURNS_DEEP_STUBS),
+                mock<KairosNetwork>(defaultAnswer = RETURNS_DEEP_STUBS),
+                mock<CoroutineScope>(defaultAnswer = RETURNS_DEEP_STUBS),
             )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerTest.java
index 891ff38..8e3117f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ui/StatusBarIconControllerTest.java
@@ -35,6 +35,7 @@
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.kairos.KairosNetwork;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.StatusBarIconView;
@@ -45,12 +46,15 @@
 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.MobileUiAdapterKairos;
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
 import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.utils.leaks.LeakCheckedTest;
 
+import kotlinx.coroutines.CoroutineScope;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -76,7 +80,9 @@
     public void testSetCalledOnAdd_IconManager() {
         LinearLayout layout = new LinearLayout(mContext);
         TestIconManager manager =
-                new TestIconManager(layout, mMobileUiAdapter, mMobileContextProvider);
+                new TestIconManager(layout, mMobileUiAdapter, mMobileContextProvider,
+                        mock(MobileUiAdapterKairos.class), mock(
+                        KairosNetwork.class), mock(CoroutineScope.class));
         testCallOnAdd_forManager(manager);
     }
 
@@ -89,7 +95,9 @@
                 mock(WifiUiAdapter.class),
                 mMobileUiAdapter,
                 mMobileContextProvider,
-                mock(DarkIconDispatcher.class));
+                mock(DarkIconDispatcher.class),
+                mock(MobileUiAdapterKairos.class), mock(KairosNetwork.class),
+                mock(CoroutineScope.class));
         testCallOnAdd_forManager(manager);
     }
 
@@ -139,12 +147,18 @@
                 WifiUiAdapter wifiUiAdapter,
                 MobileUiAdapter mobileUiAdapter,
                 MobileContextProvider contextProvider,
-                DarkIconDispatcher darkIconDispatcher) {
+                DarkIconDispatcher darkIconDispatcher,
+                MobileUiAdapterKairos mobileUiAdapterKairos,
+                KairosNetwork kairosNetwork,
+                CoroutineScope appScope) {
             super(group,
                     location,
                     wifiUiAdapter,
                     mobileUiAdapter,
+                    () -> mobileUiAdapterKairos,
                     contextProvider,
+                    kairosNetwork,
+                    appScope,
                     darkIconDispatcher);
         }
 
@@ -167,13 +181,19 @@
         TestIconManager(
                 ViewGroup group,
                 MobileUiAdapter adapter,
-                MobileContextProvider contextProvider
+                MobileContextProvider contextProvider,
+                MobileUiAdapterKairos adapterKairos,
+                KairosNetwork kairosNetwork,
+                CoroutineScope appScope
         ) {
             super(group,
                     StatusBarLocation.HOME,
                     mock(WifiUiAdapter.class),
                     adapter,
-                    contextProvider);
+                    () -> adapterKairos,
+                    contextProvider,
+                    kairosNetwork,
+                    appScope);
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
diff --git a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java
index 4b8cc00..421b5ea 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java
@@ -39,9 +39,13 @@
 import com.android.keyguard.CarrierTextManager;
 import com.android.settingslib.AccessibilityContentDescriptions;
 import com.android.settingslib.mobile.TelephonyIcons;
+import com.android.systemui.Flags;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.kairos.ExperimentalKairosApi;
+import com.android.systemui.kairos.KairosNetwork;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.res.R;
 import com.android.systemui.shade.ShadeDisplayAware;
@@ -52,17 +56,30 @@
 import com.android.systemui.statusbar.phone.StatusBarLocation;
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos;
 import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder;
 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView;
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModelKairos;
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel;
 import com.android.systemui.util.CarrierConfigTracker;
 
+import dagger.Lazy;
+
+import kotlin.OptIn;
+import kotlin.Pair;
+
+import kotlinx.coroutines.CoroutineScope;
+import kotlinx.coroutines.Job;
+
+import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CancellationException;
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
 
+@OptIn(markerClass = ExperimentalKairosApi.class)
 public class ShadeCarrierGroupController {
     private static final String TAG = "ShadeCarrierGroup";
 
@@ -100,38 +117,43 @@
 
     private final ShadeCarrierGroupControllerLogger mLogger;
 
-    private final SignalCallback mSignalCallback = new SignalCallback() {
-                @Override
-                public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) {
-                    int slotIndex = getSlotIndex(indicators.subId);
-                    if (slotIndex >= SIM_SLOTS) {
-                        Log.w(TAG, "setMobileDataIndicators - slot: " + slotIndex);
-                        return;
-                    }
-                    if (slotIndex == INVALID_SIM_SLOT_INDEX) {
-                        Log.e(TAG, "Invalid SIM slot index for subscription: " + indicators.subId);
-                        return;
-                    }
-                    mInfos[slotIndex] = new CellSignalState(
-                            indicators.statusIcon.visible,
-                            indicators.statusIcon.icon,
-                            indicators.statusIcon.contentDescription,
-                            indicators.typeContentDescription.toString(),
-                            indicators.roaming
-                    );
-                    mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget();
-                }
+    private final Lazy<MobileUiAdapterKairos> mMobileUiAdapterKairos;
+    private final CoroutineScope mAppScope;
+    private final KairosNetwork mKairosNetwork;
 
-                @Override
-                public void setNoSims(boolean hasNoSims, boolean simDetected) {
-                    if (hasNoSims) {
-                        for (int i = 0; i < SIM_SLOTS; i++) {
-                            mInfos[i] = mInfos[i].changeVisibility(false);
-                        }
-                    }
-                    mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget();
+    private final SignalCallback mSignalCallback = new SignalCallback() {
+        @Override
+        public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) {
+            int slotIndex = getSlotIndex(indicators.subId);
+            if (slotIndex >= SIM_SLOTS) {
+                Log.w(TAG, "setMobileDataIndicators - slot: " + slotIndex);
+                return;
+            }
+            if (slotIndex == INVALID_SIM_SLOT_INDEX) {
+                Log.e(TAG, "Invalid SIM slot index for subscription: " + indicators.subId);
+                return;
+            }
+            mInfos[slotIndex] = new CellSignalState(
+                    indicators.statusIcon.visible,
+                    indicators.statusIcon.icon,
+                    indicators.statusIcon.contentDescription,
+                    indicators.typeContentDescription.toString(),
+                    indicators.roaming
+            );
+            mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget();
+        }
+
+        @Override
+        public void setNoSims(boolean hasNoSims, boolean simDetected) {
+            if (hasNoSims) {
+                for (int i = 0; i < SIM_SLOTS; i++) {
+                    mInfos[i] = mInfos[i].changeVisibility(false);
                 }
-            };
+            }
+            mMainHandler.obtainMessage(H.MSG_UPDATE_STATE).sendToTarget();
+        }
+    };
+    private final ArrayList<Job> mBindingJobs = new ArrayList<>();
 
     private static class Callback implements CarrierTextManager.CarrierTextCallback {
         private H mHandler;
@@ -159,7 +181,10 @@
             SlotIndexResolver slotIndexResolver,
             MobileUiAdapter mobileUiAdapter,
             MobileContextProvider mobileContextProvider,
-            StatusBarPipelineFlags statusBarPipelineFlags
+            StatusBarPipelineFlags statusBarPipelineFlags,
+            Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos,
+            CoroutineScope appScope,
+            KairosNetwork kairosNetwork
     ) {
         mContext = context;
         mActivityStarter = activityStarter;
@@ -174,6 +199,9 @@
                 .build();
         mCarrierConfigTracker = carrierConfigTracker;
         mSlotIndexResolver = slotIndexResolver;
+        mMobileUiAdapterKairos = mobileUiAdapterKairos;
+        mAppScope = appScope;
+        mKairosNetwork = kairosNetwork;
         View.OnClickListener onClickListener = v -> {
             if (!v.isVisibleToUser()) {
                 return;
@@ -195,8 +223,12 @@
         mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel();
 
         if (mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) {
-            mobileUiAdapter.setShadeCarrierGroupController(this);
-            MobileIconsBinder.bind(view, mMobileIconsViewModel);
+            if (Flags.statusBarMobileIconKairos()) {
+                mobileUiAdapterKairos.get().setShadeCarrierGroupController(this);
+            } else {
+                mobileUiAdapter.setShadeCarrierGroupController(this);
+                MobileIconsBinder.bind(view, mMobileIconsViewModel);
+            }
         }
 
         mCarrierDividers[0] = view.getCarrierDivider1();
@@ -243,21 +275,52 @@
 
         List<IconData> iconDataList = processSubIdList(subIds);
 
-        for (IconData iconData : iconDataList) {
-            ShadeCarrier carrier = mCarrierGroups[iconData.slotIndex];
+        if (Flags.statusBarMobileIconKairos()) {
+            for (Job job : mBindingJobs) {
+                job.cancel(new CancellationException());
+            }
+            mBindingJobs.clear();
+            MobileIconsViewModelKairos mobileIconsViewModel =
+                    mMobileUiAdapterKairos.get().getMobileIconsViewModel();
+            for (IconData iconData : iconDataList) {
+                ShadeCarrier carrier = mCarrierGroups[iconData.slotIndex];
 
-            Context mobileContext =
-                    mMobileContextProvider.getMobileContextForSub(iconData.subId, mContext);
-            ModernShadeCarrierGroupMobileView modernMobileView = ModernShadeCarrierGroupMobileView
-                    .constructAndBind(
-                        mobileContext,
-                        mMobileIconsViewModel.getLogger(),
-                        "mobile_carrier_shade_group",
-                        (ShadeCarrierGroupMobileIconViewModel) mMobileIconsViewModel
-                                .viewModelForSub(iconData.subId,
-                                    StatusBarLocation.SHADE_CARRIER_GROUP)
-                    );
-            carrier.addModernMobileView(modernMobileView);
+                Context mobileContext =
+                        mMobileContextProvider.getMobileContextForSub(iconData.subId, mContext);
+
+                Pair<ModernShadeCarrierGroupMobileView, Job> viewAndJob =
+                        ModernShadeCarrierGroupMobileView.constructAndBind(
+                                mobileContext,
+                                mobileIconsViewModel.getLogger(),
+                                "mobile_carrier_shade_group",
+                                mobileIconsViewModel.shadeCarrierGroupIcon(iconData.subId),
+                                mAppScope,
+                                iconData.subId,
+                                StatusBarLocation.SHADE_CARRIER_GROUP,
+                                mKairosNetwork
+                        );
+                mBindingJobs.add(viewAndJob.getSecond());
+                carrier.addModernMobileView(viewAndJob.getFirst());
+            }
+        } else {
+            for (IconData iconData : iconDataList) {
+                ShadeCarrier carrier = mCarrierGroups[iconData.slotIndex];
+
+                Context mobileContext =
+                        mMobileContextProvider.getMobileContextForSub(iconData.subId, mContext);
+
+                ModernShadeCarrierGroupMobileView modernMobileView =
+                        ModernShadeCarrierGroupMobileView
+                                .constructAndBind(
+                                        mobileContext,
+                                        mMobileIconsViewModel.getLogger(),
+                                        "mobile_carrier_shade_group",
+                                        (ShadeCarrierGroupMobileIconViewModel) mMobileIconsViewModel
+                                                .viewModelForSub(iconData.subId,
+                                                        StatusBarLocation.SHADE_CARRIER_GROUP)
+                                );
+                carrier.addModernMobileView(modernMobileView);
+            }
         }
     }
 
@@ -288,7 +351,6 @@
      * Sets a {@link OnSingleCarrierChangedListener}.
      *
      * This will get notified when the number of carriers changes between 1 and "not one".
-     * @param listener
      */
     public void setOnSingleCarrierChangedListener(
             @Nullable OnSingleCarrierChangedListener listener) {
@@ -489,6 +551,9 @@
         private final MobileUiAdapter mMobileUiAdapter;
         private final MobileContextProvider mMobileContextProvider;
         private final StatusBarPipelineFlags mStatusBarPipelineFlags;
+        private final CoroutineScope mAppScope;
+        private final KairosNetwork mKairosNetwork;
+        private final Lazy<MobileUiAdapterKairos> mMobileUiAdapterKairos;
 
         @Inject
         public Builder(
@@ -503,7 +568,10 @@
                 SlotIndexResolver slotIndexResolver,
                 MobileUiAdapter mobileUiAdapter,
                 MobileContextProvider mobileContextProvider,
-                StatusBarPipelineFlags statusBarPipelineFlags
+                StatusBarPipelineFlags statusBarPipelineFlags,
+                @Application CoroutineScope appScope,
+                KairosNetwork kairosNetwork,
+                Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos
         ) {
             mActivityStarter = activityStarter;
             mHandler = handler;
@@ -517,6 +585,9 @@
             mMobileUiAdapter = mobileUiAdapter;
             mMobileContextProvider = mobileContextProvider;
             mStatusBarPipelineFlags = statusBarPipelineFlags;
+            mAppScope = appScope;
+            mKairosNetwork = kairosNetwork;
+            mMobileUiAdapterKairos = mobileUiAdapterKairos;
         }
 
         public Builder setShadeCarrierGroup(ShadeCarrierGroup view) {
@@ -538,8 +609,10 @@
                     mSlotIndexResolver,
                     mMobileUiAdapter,
                     mMobileContextProvider,
-                    mStatusBarPipelineFlags
-            );
+                    mStatusBarPipelineFlags,
+                    mMobileUiAdapterKairos,
+                    mAppScope,
+                    mKairosNetwork);
         }
     }
 
@@ -571,7 +644,8 @@
     public static class SubscriptionManagerSlotIndexResolver implements SlotIndexResolver {
 
         @Inject
-        public SubscriptionManagerSlotIndexResolver() {}
+        public SubscriptionManagerSlotIndexResolver() {
+        }
 
         @Override
         public int getSlotIndex(int subscriptionId) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
index 5609326..8824276 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalKairosApi::class)
+
 package com.android.systemui.shade.ui.viewmodel
 
 import android.content.Context
@@ -28,6 +30,8 @@
 import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.compose.animation.scene.OverlayKey
 import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
 import com.android.systemui.lifecycle.ExclusiveActivatable
 import com.android.systemui.lifecycle.Hydrator
 import com.android.systemui.plugins.ActivityStarter
@@ -47,6 +51,7 @@
 import com.android.systemui.statusbar.phone.ui.TintedIconManager
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModelKairos
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import java.util.Locale
@@ -76,6 +81,8 @@
     private val tintedIconManagerFactory: TintedIconManager.Factory,
     private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
     val statusBarIconController: StatusBarIconController,
+    val kairosNetwork: KairosNetwork,
+    val mobileIconsViewModelKairos: dagger.Lazy<MobileIconsViewModelKairos>,
 ) : ExclusiveActivatable() {
 
     private val hydrator = Hydrator("ShadeHeaderViewModel.hydrator")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
index 0ba4aab..34c193d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java
@@ -27,13 +27,19 @@
 import android.view.ViewGroup;
 import android.widget.LinearLayout;
 
+import androidx.collection.MutableIntObjectMap;
+
 import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.Flags;
 import com.android.systemui.demomode.DemoMode;
+import com.android.systemui.kairos.ExperimentalKairosApi;
+import com.android.systemui.kairos.KairosNetwork;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.StatusBarIconView;
 import com.android.systemui.statusbar.StatusIconDisplayable;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos;
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger;
 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
@@ -41,10 +47,20 @@
 import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView;
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel;
 
+import dagger.Lazy;
+
+import kotlin.OptIn;
+import kotlin.Pair;
+
+import kotlinx.coroutines.CoroutineScope;
+import kotlinx.coroutines.Job;
+
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CancellationException;
 
 //TODO: This should be a controller, not its own view
+@OptIn(markerClass = ExperimentalKairosApi.class)
 public class DemoStatusIcons extends StatusIconContainer implements DemoMode, DarkReceiver {
     private static final String TAG = "DemoStatusIcons";
 
@@ -60,15 +76,27 @@
     private final MobileIconsViewModel mMobileIconsViewModel;
     private final StatusBarLocation mLocation;
 
+    private final Lazy<MobileUiAdapterKairos> mMobileUiAdapterKairos;
+    private final KairosNetwork mKairosNetwork;
+    private final CoroutineScope mAppScope;
+
+    private final MutableIntObjectMap<Job> mBindingJobs = new MutableIntObjectMap<>();
+
     public DemoStatusIcons(
             LinearLayout statusIcons,
             MobileIconsViewModel mobileIconsViewModel,
             StatusBarLocation location,
-            int iconSize
+            int iconSize,
+            Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos,
+            KairosNetwork kairosNetwork,
+            CoroutineScope appScope
     ) {
         super(statusIcons.getContext());
         mStatusIcons = statusIcons;
         mIconSize = iconSize;
+        mMobileUiAdapterKairos = mobileUiAdapterKairos;
+        mKairosNetwork = kairosNetwork;
+        mAppScope = appScope;
         mColor = DarkIconDispatcher.DEFAULT_ICON_TINT;
         mContrastColor = DarkIconDispatcher.DEFAULT_INVERSE_ICON_TINT;
         mMobileIconsViewModel = mobileIconsViewModel;
@@ -236,24 +264,46 @@
 
     /**
      * Add a {@link ModernStatusBarMobileView}
+     *
      * @param mobileContext possibly mcc/mnc overridden mobile context
-     * @param subId the subscriptionId for this mobile view
+     * @param subId         the subscriptionId for this mobile view
      */
     public void addModernMobileView(
             Context mobileContext,
             MobileViewLogger mobileViewLogger,
             int subId) {
         Log.d(TAG, "addModernMobileView (subId=" + subId + ")");
-        ModernStatusBarMobileView view = ModernStatusBarMobileView.constructAndBind(
-                mobileContext,
-                mobileViewLogger,
-                "mobile",
-                mMobileIconsViewModel.viewModelForSub(subId, mLocation)
-        );
+        if (Flags.statusBarMobileIconKairos()) {
+            Pair<ModernStatusBarMobileView, Job> viewAndJob =
+                    ModernStatusBarMobileView.constructAndBind(
+                            mobileContext,
+                            mobileViewLogger,
+                            "mobile",
+                            mMobileUiAdapterKairos.get().getMobileIconsViewModel().viewModelForSub(
+                                    subId, mLocation),
+                            mAppScope,
+                            subId,
+                            mLocation,
+                            mKairosNetwork
+                    );
+            ModernStatusBarMobileView view = viewAndJob.getFirst();
+            mBindingJobs.put(subId, viewAndJob.getSecond());
+            // mobile always goes at the end
+            mModernMobileViews.add(view);
+            addView(view, getChildCount(), createLayoutParams());
+        } else {
+            ModernStatusBarMobileView view =
+                    ModernStatusBarMobileView.constructAndBind(
+                            mobileContext,
+                            mobileViewLogger,
+                            "mobile",
+                            mMobileIconsViewModel.viewModelForSub(subId, mLocation)
+                    );
 
-        // mobile always goes at the end
-        mModernMobileViews.add(view);
-        addView(view, getChildCount(), createLayoutParams());
+            // mobile always goes at the end
+            mModernMobileViews.add(view);
+            addView(view, getChildCount(), createLayoutParams());
+        }
     }
 
     /**
@@ -299,6 +349,12 @@
             ModernStatusBarMobileView mobileView = matchingModernMobileView(
                     (ModernStatusBarMobileView) view);
             if (mobileView != null) {
+                if (Flags.statusBarMobileIconKairos()) {
+                    Job job = mBindingJobs.remove(mobileView.getSubId());
+                    if (job != null) {
+                        job.cancel(new CancellationException());
+                    }
+                }
                 removeView(mobileView);
                 mModernMobileViews.remove(mobileView);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/DarkIconManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/DarkIconManager.java
index 8d314ee..6dcc0ad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/DarkIconManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/DarkIconManager.java
@@ -19,6 +19,9 @@
 import android.widget.LinearLayout;
 
 import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.kairos.ExperimentalKairosApi;
+import com.android.systemui.kairos.KairosNetwork;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.statusbar.StatusIconDisplayable;
 import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
@@ -26,13 +29,20 @@
 import com.android.systemui.statusbar.phone.StatusBarIconHolder;
 import com.android.systemui.statusbar.phone.StatusBarLocation;
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos;
 import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
 
+import dagger.Lazy;
 import dagger.assisted.Assisted;
 import dagger.assisted.AssistedFactory;
 import dagger.assisted.AssistedInject;
 
+import kotlin.OptIn;
+
+import kotlinx.coroutines.CoroutineScope;
+
 /** Version of {@link IconManager} that observes state from the {@link DarkIconDispatcher}. */
+@OptIn(markerClass = ExperimentalKairosApi.class)
 public class DarkIconManager extends IconManager {
     private final DarkIconDispatcher mDarkIconDispatcher;
     private final int mIconHorizontalMargin;
@@ -43,13 +53,21 @@
             @Assisted StatusBarLocation location,
             WifiUiAdapter wifiUiAdapter,
             MobileUiAdapter mobileUiAdapter,
+            Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos,
             MobileContextProvider mobileContextProvider,
+            KairosNetwork kairosNetwork,
+            @Application CoroutineScope appScope,
             @Assisted DarkIconDispatcher darkIconDispatcher) {
-        super(linearLayout, location, wifiUiAdapter, mobileUiAdapter, mobileContextProvider);
-        mIconHorizontalMargin =
-                mContext.getResources()
-                        .getDimensionPixelSize(
-                                com.android.systemui.res.R.dimen.status_bar_icon_horizontal_margin);
+        super(linearLayout,
+                location,
+                wifiUiAdapter,
+                mobileUiAdapter,
+                mobileUiAdapterKairos,
+                mobileContextProvider,
+                kairosNetwork,
+                appScope);
+        mIconHorizontalMargin = mContext.getResources().getDimensionPixelSize(
+                com.android.systemui.res.R.dimen.status_bar_icon_horizontal_margin);
         mDarkIconDispatcher = darkIconDispatcher;
     }
 
@@ -104,7 +122,7 @@
         super.exitDemoMode();
     }
 
-    /** */
+    /**  */
     @AssistedFactory
     public interface Factory {
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java
index fd16c60..862931a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/IconManager.java
@@ -24,12 +24,19 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.os.Bundle;
+import android.view.View;
 import android.view.ViewGroup;
 import android.widget.LinearLayout;
 
+import androidx.annotation.OptIn;
+import androidx.collection.MutableIntObjectMap;
+
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.internal.statusbar.StatusBarIcon.Shape;
+import com.android.systemui.Flags;
 import com.android.systemui.demomode.DemoModeCommandReceiver;
+import com.android.systemui.kairos.ExperimentalKairosApi;
+import com.android.systemui.kairos.KairosNetwork;
 import com.android.systemui.modes.shared.ModesUiIcons;
 import com.android.systemui.statusbar.BaseStatusBarFrameLayout;
 import com.android.systemui.statusbar.StatusBarIconView;
@@ -40,6 +47,7 @@
 import com.android.systemui.statusbar.phone.StatusBarIconHolder.BindableIconHolder;
 import com.android.systemui.statusbar.phone.StatusBarLocation;
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos;
 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;
@@ -49,20 +57,34 @@
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel;
 import com.android.systemui.util.Assert;
 
+import dagger.Lazy;
+
+import kotlin.Pair;
+
+import kotlinx.coroutines.CoroutineScope;
+import kotlinx.coroutines.Job;
+
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CancellationException;
 
 /**
  * Turns info from StatusBarIconController into ImageViews in a ViewGroup.
  */
+@OptIn(markerClass = ExperimentalKairosApi.class)
 public class IconManager implements DemoModeCommandReceiver {
     protected final ViewGroup mGroup;
     private final MobileContextProvider mMobileContextProvider;
     private final LocationBasedWifiViewModel mWifiViewModel;
     private final MobileIconsViewModel mMobileIconsViewModel;
 
+    private final Lazy<MobileUiAdapterKairos> mMobileUiAdapterKairos;
+    private final KairosNetwork mKairosNetwork;
+    private final CoroutineScope mAppScope;
+    private final MutableIntObjectMap<Job> mBindingJobs = new MutableIntObjectMap<>();
+
     /**
      * Stores the list of bindable icons that have been added, keyed on slot name. This ensures
      * we don't accidentally add the same bindable icon twice.
@@ -87,12 +109,17 @@
             StatusBarLocation location,
             WifiUiAdapter wifiUiAdapter,
             MobileUiAdapter mobileUiAdapter,
-            MobileContextProvider mobileContextProvider
+            Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos,
+            MobileContextProvider mobileContextProvider,
+            KairosNetwork kairosNetwork,
+            CoroutineScope appScope
     ) {
         mGroup = group;
         mMobileContextProvider = mobileContextProvider;
         mContext = group.getContext();
         mLocation = location;
+        mKairosNetwork = kairosNetwork;
+        mAppScope = appScope;
 
         reloadDimens();
 
@@ -101,6 +128,9 @@
         mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel();
         MobileIconsBinder.bind(mGroup, mMobileIconsViewModel);
 
+
+        mMobileUiAdapterKairos = mobileUiAdapterKairos;
+
         mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, mLocation);
     }
 
@@ -150,7 +180,7 @@
             case TYPE_MOBILE_NEW -> addNewMobileIcon(index, slot, holder.getTag());
             case TYPE_BINDABLE ->
                 // Safe cast, since only BindableIconHolders can set this tag on themselves
-                addBindableIcon((BindableIconHolder) holder, index);
+                    addBindableIcon((BindableIconHolder) holder, index);
             default -> null;
         };
     }
@@ -223,13 +253,30 @@
     private ModernStatusBarMobileView onCreateModernStatusBarMobileView(
             String slot, int subId) {
         Context mobileContext = mMobileContextProvider.getMobileContextForSub(subId, mContext);
-        return ModernStatusBarMobileView
-                .constructAndBind(
-                        mobileContext,
-                        mMobileIconsViewModel.getLogger(),
-                        slot,
-                        mMobileIconsViewModel.viewModelForSub(subId, mLocation)
-                );
+        if (Flags.statusBarMobileIconKairos()) {
+            Pair<ModernStatusBarMobileView, Job> viewAndJob =
+                    ModernStatusBarMobileView.constructAndBind(
+                            mobileContext,
+                            mMobileUiAdapterKairos.get().getMobileIconsViewModel().getLogger(),
+                            slot,
+                            mMobileUiAdapterKairos.get().getMobileIconsViewModel()
+                                    .viewModelForSub(subId, mLocation),
+                            mAppScope,
+                            subId,
+                            mLocation,
+                            mKairosNetwork
+                    );
+            mBindingJobs.put(subId, viewAndJob.getSecond());
+            return viewAndJob.getFirst();
+        } else {
+            return ModernStatusBarMobileView
+                    .constructAndBind(
+                            mobileContext,
+                            mMobileIconsViewModel.getLogger(),
+                            slot,
+                            mMobileIconsViewModel.viewModelForSub(subId, mLocation)
+                    );
+        }
     }
 
     protected LinearLayout.LayoutParams onCreateLayoutParams(Shape shape) {
@@ -253,6 +300,15 @@
         if (mIsInDemoMode) {
             mDemoStatusIcons.onRemoveIcon((StatusIconDisplayable) mGroup.getChildAt(viewIndex));
         }
+        if (Flags.statusBarMobileIconKairos()) {
+            View view = mGroup.getChildAt(viewIndex);
+            if (view instanceof ModernStatusBarMobileView) {
+                Job bindingJob = mBindingJobs.remove(((ModernStatusBarMobileView) view).getSubId());
+                if (bindingJob != null) {
+                    bindingJob.cancel(new CancellationException());
+                }
+            }
+        }
         mGroup.removeViewAt(viewIndex);
     }
 
@@ -326,7 +382,10 @@
                 (LinearLayout) mGroup,
                 mMobileIconsViewModel,
                 mLocation,
-                mIconSize
+                mIconSize,
+                mMobileUiAdapterKairos,
+                mKairosNetwork,
+                mAppScope
         );
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/TintedIconManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/TintedIconManager.java
index e520148..da59fc0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/TintedIconManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ui/TintedIconManager.java
@@ -20,19 +20,30 @@
 import android.view.ViewGroup;
 
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.kairos.ExperimentalKairosApi;
+import com.android.systemui.kairos.KairosNetwork;
 import com.android.systemui.statusbar.StatusIconDisplayable;
 import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider;
 import com.android.systemui.statusbar.phone.DemoStatusIcons;
 import com.android.systemui.statusbar.phone.StatusBarIconHolder;
 import com.android.systemui.statusbar.phone.StatusBarLocation;
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapterKairos;
 import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
 
+import dagger.Lazy;
+
+import kotlin.OptIn;
+
+import kotlinx.coroutines.CoroutineScope;
+
 import javax.inject.Inject;
 
 /**
  * Version of {@link IconManager} that can tint the icons to a particular color.
  */
+@OptIn(markerClass = ExperimentalKairosApi.class)
 public class TintedIconManager extends IconManager {
     // The main tint, used as the foreground in non layer drawables
     private int mColor;
@@ -44,13 +55,17 @@
             StatusBarLocation location,
             WifiUiAdapter wifiUiAdapter,
             MobileUiAdapter mobileUiAdapter,
-            MobileContextProvider mobileContextProvider
+            Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos,
+            MobileContextProvider mobileContextProvider,
+            KairosNetwork kairosNetwork,
+            CoroutineScope appScope
     ) {
         super(group,
                 location,
                 wifiUiAdapter,
                 mobileUiAdapter,
-                mobileContextProvider);
+                mobileUiAdapterKairos,
+                mobileContextProvider, kairosNetwork, appScope);
     }
 
     @Override
@@ -99,16 +114,25 @@
         private final WifiUiAdapter mWifiUiAdapter;
         private final MobileContextProvider mMobileContextProvider;
         private final MobileUiAdapter mMobileUiAdapter;
+        private final Lazy<MobileUiAdapterKairos> mMobileUiAdapterKairos;
+        private final KairosNetwork mKairosNetwork;
+        private final CoroutineScope mAppScope;
 
         @Inject
         public Factory(
                 WifiUiAdapter wifiUiAdapter,
                 MobileUiAdapter mobileUiAdapter,
-                MobileContextProvider mobileContextProvider
+                MobileContextProvider mobileContextProvider,
+                Lazy<MobileUiAdapterKairos> mobileUiAdapterKairos,
+                KairosNetwork kairosNetwork,
+                @Application CoroutineScope appScope
         ) {
             mWifiUiAdapter = wifiUiAdapter;
             mMobileUiAdapter = mobileUiAdapter;
             mMobileContextProvider = mobileContextProvider;
+            mMobileUiAdapterKairos = mobileUiAdapterKairos;
+            mKairosNetwork = kairosNetwork;
+            mAppScope = appScope;
         }
 
         /** Creates a new {@link TintedIconManager} for the given view group and location. */
@@ -118,7 +142,10 @@
                     location,
                     mWifiUiAdapter,
                     mMobileUiAdapter,
-                    mMobileContextProvider);
+                    mMobileUiAdapterKairos,
+                    mMobileContextProvider,
+                    mKairosNetwork,
+                    mAppScope);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairos.kt
new file mode 100644
index 0000000..9881b35
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairos.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.ui
+
+import com.android.systemui.Dumpable
+import com.android.systemui.KairosActivatable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.kairos.BuildScope
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.awaitClose
+import com.android.systemui.kairos.combine
+import com.android.systemui.kairos.launchEffect
+import com.android.systemui.shade.carrier.ShadeCarrierGroupController
+import com.android.systemui.statusbar.phone.ui.StatusBarIconController
+import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorKairos
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModelKairos
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * This class is intended to provide a context to collect on the
+ * [MobileIconsInteractorKairos.filteredSubscriptions] data source and supply a state flow that can
+ * control [StatusBarIconController] to keep the old UI in sync with the new data source.
+ *
+ * It also provides a mechanism to create a top-level view model for each IconManager to know about
+ * the list of available mobile lines of service for which we want to show icons.
+ */
+@ExperimentalKairosApi
+@SysUISingleton
+class MobileUiAdapterKairos
+@Inject
+constructor(
+    private val iconController: StatusBarIconController,
+    val mobileIconsViewModel: MobileIconsViewModelKairos,
+    private val logger: MobileViewLogger,
+    dumpManager: DumpManager,
+) : KairosActivatable, Dumpable {
+
+    init {
+        dumpManager.registerNormalDumpable(this)
+    }
+
+    private var isCollecting: Boolean = false
+    private var lastValue: List<Int>? = null
+
+    private var shadeCarrierGroupController: ShadeCarrierGroupController? = null
+
+    override fun BuildScope.activate() {
+        launchEffect {
+            isCollecting = true
+            awaitClose { isCollecting = false }
+        }
+        // Start notifying the icon controller of subscriptions
+        combine(mobileIconsViewModel.subscriptionIds, mobileIconsViewModel.isStackable) { a, b ->
+                Pair(a, b)
+            }
+            .observe { (subIds, isStackable) ->
+                logger.logUiAdapterSubIdsSentToIconController(subIds, isStackable)
+                lastValue = subIds
+                if (isStackable) {
+                    // Passing an empty list to remove pre-existing mobile icons.
+                    // StackedMobileBindableIcon will show the stacked icon instead.
+                    iconController.setNewMobileIconSubIds(emptyList())
+                } else {
+                    iconController.setNewMobileIconSubIds(subIds)
+                }
+                shadeCarrierGroupController?.updateModernMobileIcons(subIds)
+            }
+    }
+
+    /** Set the [ShadeCarrierGroupController] to notify of subscription updates */
+    fun setShadeCarrierGroupController(controller: ShadeCarrierGroupController) {
+        shadeCarrierGroupController = controller
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("isCollecting=$isCollecting")
+        pw.println("Last values sent to icon controller: $lastValue")
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt
index 4c2849d..dec2688 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt
@@ -20,10 +20,12 @@
 import com.android.systemui.Dumpable
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.kairos.ExperimentalKairosApi
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
 import com.android.systemui.statusbar.pipeline.dagger.MobileViewLog
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModelKairos
 import java.io.PrintWriter
 import javax.inject.Inject
 
@@ -52,14 +54,17 @@
         )
     }
 
-    fun logNewViewBinding(view: View, viewModel: LocationBasedMobileViewModel) {
+    fun logNewViewBinding(view: View, viewModel: LocationBasedMobileViewModel) =
+        logNewViewBinding(view, viewModel, viewModel.location.name)
+
+    fun logNewViewBinding(view: View, viewModel: Any, location: String) {
         buffer.log(
             TAG,
             LogLevel.INFO,
             {
                 str1 = view.getIdForLogging()
                 str2 = viewModel.getIdForLogging()
-                str3 = viewModel.location.name
+                str3 = location
             },
             { "New view binding. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" },
         )
@@ -93,6 +98,36 @@
         )
     }
 
+    @OptIn(ExperimentalKairosApi::class)
+    fun logCollectionStarted(view: View, viewModel: LocationBasedMobileViewModelKairos) {
+        collectionStatuses[view.getIdForLogging()] = true
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            {
+                str1 = view.getIdForLogging()
+                str2 = viewModel.getIdForLogging()
+                str3 = viewModel.location.name
+            },
+            { "Collection started. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" },
+        )
+    }
+
+    @OptIn(ExperimentalKairosApi::class)
+    fun logCollectionStopped(view: View, viewModel: LocationBasedMobileViewModelKairos) {
+        collectionStatuses[view.getIdForLogging()] = false
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            {
+                str1 = view.getIdForLogging()
+                str2 = viewModel.getIdForLogging()
+                str3 = viewModel.location.name
+            },
+            { "Collection stopped. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" },
+        )
+    }
+
     override fun dump(pw: PrintWriter, args: Array<out String>) {
         pw.println("Collection statuses per view:---")
         collectionStatuses.forEach { viewId, isCollecting ->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt
index 32ebe88..bda76b7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt
@@ -19,15 +19,19 @@
 import android.content.Context
 import com.android.settingslib.flags.Flags
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
 import com.android.systemui.statusbar.core.StatusBarRootModernization
 import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon
 import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator
 import com.android.systemui.statusbar.pipeline.mobile.ui.binder.StackedMobileIconBinder
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModelImpl
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModelKairos
 import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView
 import javax.inject.Inject
 
+@OptIn(ExperimentalKairosApi::class)
 @SysUISingleton
 class StackedMobileBindableIcon
 @Inject
@@ -35,6 +39,8 @@
     context: Context,
     mobileIconsViewModel: MobileIconsViewModel,
     viewModelFactory: StackedMobileIconViewModelImpl.Factory,
+    kairosViewModelFactory: StackedMobileIconViewModelKairos.Factory,
+    kairosNetwork: KairosNetwork,
 ) : BindableIcon {
     override val slot: String =
         context.getString(com.android.internal.R.string.status_bar_stacked_mobile)
@@ -42,7 +48,13 @@
     override val initializer = ModernStatusBarViewCreator { context ->
         SingleBindableStatusBarComposeIconView.createView(context).also { view ->
             view.initView(slot) {
-                StackedMobileIconBinder.bind(view, mobileIconsViewModel, viewModelFactory)
+                StackedMobileIconBinder.bind(
+                    view,
+                    mobileIconsViewModel,
+                    viewModelFactory,
+                    kairosViewModelFactory,
+                    kairosNetwork,
+                )
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index 0eef2e1..0abd6d8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -50,7 +50,7 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 
-private data class Colors(@ColorInt val tint: Int, @ColorInt val contrast: Int)
+data class MobileIconColors(@ColorInt val tint: Int, @ColorInt val contrast: Int)
 
 object MobileIconBinder {
     /** Binds the view to the view-model, continuing to update the former based on the latter */
@@ -80,9 +80,9 @@
         @StatusBarIconView.VisibleState
         val visibilityState: MutableStateFlow<Int> = MutableStateFlow(initialVisibilityState)
 
-        val iconTint: MutableStateFlow<Colors> =
+        val iconTint: MutableStateFlow<MobileIconColors> =
             MutableStateFlow(
-                Colors(
+                MobileIconColors(
                     tint = DarkIconDispatcher.DEFAULT_ICON_TINT,
                     contrast = DarkIconDispatcher.DEFAULT_INVERSE_ICON_TINT,
                 )
@@ -291,7 +291,7 @@
             }
 
             override fun onIconTintChanged(newTint: Int, contrastTint: Int) {
-                iconTint.value = Colors(tint = newTint, contrast = contrastTint)
+                iconTint.value = MobileIconColors(tint = newTint, contrast = contrastTint)
             }
 
             override fun onDecorTintChanged(newTint: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinderKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinderKairos.kt
new file mode 100644
index 0000000..1078ae3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinderKairos.kt
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2025 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.mobile.ui.binder
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.Space
+import androidx.core.view.isVisible
+import com.android.settingslib.graph.SignalDrawable
+import com.android.systemui.Flags
+import com.android.systemui.common.ui.binder.IconViewBinder
+import com.android.systemui.kairos.BuildScope
+import com.android.systemui.kairos.BuildSpec
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
+import com.android.systemui.kairos.MutableState
+import com.android.systemui.kairos.effect
+import com.android.systemui.lifecycle.repeatWhenAttachedToWindow
+import com.android.systemui.lifecycle.repeatWhenWindowIsVisible
+import com.android.systemui.plugins.DarkIconDispatcher
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModelKairos
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper
+import com.android.systemui.statusbar.pipeline.shared.ui.binder.StatusBarViewBinderConstants
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.launch
+
+object MobileIconBinderKairos {
+
+    @ExperimentalKairosApi
+    fun bind(
+        view: ViewGroup,
+        viewModel: BuildSpec<LocationBasedMobileViewModelKairos>,
+        @StatusBarIconView.VisibleState
+        initialVisibilityState: Int = StatusBarIconView.STATE_HIDDEN,
+        logger: MobileViewLogger,
+        scope: CoroutineScope,
+        kairosNetwork: KairosNetwork,
+    ): Pair<ModernStatusBarViewBinding, Job> {
+        val binding = ModernStatusBarViewBindingKairosImpl(kairosNetwork, initialVisibilityState)
+        return binding to
+            scope.launch {
+                view.repeatWhenAttachedToWindow {
+                    kairosNetwork.activateSpec {
+                        bind(
+                            view = view,
+                            viewModel = viewModel.applySpec(),
+                            logger = logger,
+                            binding = binding,
+                        )
+                    }
+                }
+            }
+    }
+
+    @ExperimentalKairosApi
+    private class ModernStatusBarViewBindingKairosImpl(
+        kairosNetwork: KairosNetwork,
+        initialVisibilityState: Int,
+    ) : ModernStatusBarViewBinding {
+
+        @JvmField var shouldIconBeVisible: Boolean = false
+        @JvmField var isCollecting: Boolean = false
+
+        // TODO(b/238425913): We should log this visibility state.
+        val visibility = MutableState(kairosNetwork, initialVisibilityState)
+        val iconTint =
+            MutableState(
+                kairosNetwork,
+                MobileIconColors(
+                    tint = DarkIconDispatcher.DEFAULT_ICON_TINT,
+                    contrast = DarkIconDispatcher.DEFAULT_INVERSE_ICON_TINT,
+                ),
+            )
+        val decorTint = MutableState(kairosNetwork, Color.WHITE)
+
+        override fun getShouldIconBeVisible(): Boolean = shouldIconBeVisible
+
+        override fun onVisibilityStateChanged(state: Int) {
+            visibility.setValue(state)
+        }
+
+        override fun onIconTintChanged(newTint: Int, contrastTint: Int) {
+            iconTint.setValue(MobileIconColors(tint = newTint, contrast = contrastTint))
+        }
+
+        override fun onDecorTintChanged(newTint: Int) {
+            decorTint.setValue(newTint)
+        }
+
+        override fun isCollecting(): Boolean = isCollecting
+    }
+
+    @ExperimentalKairosApi
+    private fun BuildScope.bind(
+        view: ViewGroup,
+        viewModel: LocationBasedMobileViewModelKairos,
+        logger: MobileViewLogger,
+        binding: ModernStatusBarViewBindingKairosImpl,
+    ) {
+        viewModel.isVisible.observe { binding.shouldIconBeVisible = it }
+
+        val mobileGroupView = view.requireViewById<ViewGroup>(R.id.mobile_group)
+        val activityContainer = view.requireViewById<View>(R.id.inout_container)
+        val activityIn = view.requireViewById<ImageView>(R.id.mobile_in)
+        val activityOut = view.requireViewById<ImageView>(R.id.mobile_out)
+        val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type)
+        val networkTypeContainer = view.requireViewById<FrameLayout>(R.id.mobile_type_container)
+        val iconView = view.requireViewById<ImageView>(R.id.mobile_signal)
+        val mobileDrawable = SignalDrawable(view.context)
+        val roamingView = view.requireViewById<ImageView>(R.id.mobile_roaming)
+        val roamingSpace = view.requireViewById<Space>(R.id.mobile_roaming_space)
+        val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot)
+
+        effect {
+            view.isVisible = viewModel.isVisible.sample()
+            iconView.isVisible = true
+            launch {
+                view.repeatWhenAttachedToWindow {
+                    // isVisible controls the visibility state of the outer group, and thus it needs
+                    // to run in the CREATED lifecycle so it can continue to watch while invisible
+                    // See (b/291031862) for details
+                    kairosNetwork.activateSpec {
+                        viewModel.isVisible.observe { isVisible ->
+                            viewModel.verboseLogger?.logBinderReceivedVisibility(
+                                view,
+                                viewModel.subscriptionId,
+                                isVisible,
+                            )
+                            view.isVisible = isVisible
+                            // [StatusIconContainer] can get out of sync sometimes. Make sure to
+                            // request another layout when this changes.
+                            view.requestLayout()
+                        }
+                    }
+                }
+            }
+            launch {
+                view.repeatWhenWindowIsVisible {
+                    logger.logCollectionStarted(view, viewModel)
+                    binding.isCollecting = true
+                    kairosNetwork.activateSpec {
+                        binding.visibility.observe { state ->
+                            ModernStatusBarViewVisibilityHelper.setVisibilityState(
+                                state,
+                                mobileGroupView,
+                                dotView,
+                            )
+                            view.requestLayout()
+                        }
+
+                        // Set the icon for the triangle
+                        viewModel.icon.observe { icon ->
+                            viewModel.verboseLogger?.logBinderReceivedSignalIcon(
+                                view,
+                                viewModel.subscriptionId,
+                                icon,
+                            )
+                            if (icon is SignalIconModel.Cellular) {
+                                iconView.setImageDrawable(mobileDrawable)
+                                mobileDrawable.level = icon.toSignalDrawableState()
+                            } else if (icon is SignalIconModel.Satellite) {
+                                IconViewBinder.bind(icon.icon, iconView)
+                            }
+                        }
+
+                        viewModel.contentDescription.observe {
+                            MobileContentDescriptionViewBinder.bind(it, view)
+                        }
+
+                        // Set the network type icon
+                        viewModel.networkTypeIcon.observe { dataTypeId ->
+                            viewModel.verboseLogger?.logBinderReceivedNetworkTypeIcon(
+                                view,
+                                viewModel.subscriptionId,
+                                dataTypeId,
+                            )
+                            dataTypeId?.let { IconViewBinder.bind(dataTypeId, networkTypeView) }
+                            val prevVis = networkTypeContainer.visibility
+                            networkTypeContainer.visibility =
+                                if (dataTypeId != null) View.VISIBLE else View.GONE
+
+                            if (prevVis != networkTypeContainer.visibility) {
+                                view.requestLayout()
+                            }
+                        }
+
+                        // Set the network type background
+                        viewModel.networkTypeBackground.observe { background ->
+                            networkTypeContainer.setBackgroundResource(background?.res ?: 0)
+
+                            // Tint will invert when this bit changes
+                            if (background?.res != null) {
+                                networkTypeContainer.backgroundTintList =
+                                    ColorStateList.valueOf(binding.iconTint.sample().tint)
+                                networkTypeView.imageTintList =
+                                    ColorStateList.valueOf(binding.iconTint.sample().contrast)
+                            } else {
+                                networkTypeView.imageTintList =
+                                    ColorStateList.valueOf(binding.iconTint.sample().tint)
+                            }
+                        }
+
+                        // Set the roaming indicator
+                        viewModel.roaming.observe { isRoaming ->
+                            roamingView.isVisible = isRoaming
+                            roamingSpace.isVisible = isRoaming
+                        }
+
+                        if (Flags.statusBarStaticInoutIndicators()) {
+                            // Set the opacity of the activity indicators
+                            viewModel.activityInVisible.observe { visible ->
+                                activityIn.imageAlpha =
+                                    (if (visible) StatusBarViewBinderConstants.ALPHA_ACTIVE
+                                    else StatusBarViewBinderConstants.ALPHA_INACTIVE)
+                            }
+                            viewModel.activityOutVisible.observe { visible ->
+                                activityOut.imageAlpha =
+                                    (if (visible) StatusBarViewBinderConstants.ALPHA_ACTIVE
+                                    else StatusBarViewBinderConstants.ALPHA_INACTIVE)
+                            }
+                        } else {
+                            // Set the activity indicators
+                            viewModel.activityInVisible.observe { activityIn.isVisible = it }
+                            viewModel.activityOutVisible.observe { activityOut.isVisible = it }
+                        }
+
+                        viewModel.activityContainerVisible.observe {
+                            activityContainer.isVisible = it
+                        }
+
+                        // Set the tint
+                        binding.iconTint.observe { colors ->
+                            val tint = ColorStateList.valueOf(colors.tint)
+                            val contrast = ColorStateList.valueOf(colors.contrast)
+
+                            iconView.imageTintList = tint
+
+                            // If the bg is visible, tint it and use the contrast for the fg
+                            if (viewModel.networkTypeBackground.sample() != null) {
+                                networkTypeContainer.backgroundTintList = tint
+                                networkTypeView.imageTintList = contrast
+                            } else {
+                                networkTypeView.imageTintList = tint
+                            }
+
+                            roamingView.imageTintList = tint
+                            activityIn.imageTintList = tint
+                            activityOut.imageTintList = tint
+                            dotView.setDecorColor(colors.tint)
+                        }
+
+                        binding.decorTint.observe { tint -> dotView.setDecorColor(tint) }
+                    }
+
+                    try {
+                        awaitCancellation()
+                    } finally {
+                        binding.isCollecting = false
+                        logger.logCollectionStopped(view, viewModel)
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt
index 5c80fda..5238f3e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt
@@ -19,10 +19,10 @@
 import androidx.core.view.isVisible
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel
 import com.android.systemui.util.AutoMarqueeTextView
-import com.android.app.tracing.coroutines.launchTraced as launch
 
 object ShadeCarrierBinder {
     /** Binds the view to the view-model, continuing to update the former based on the latter */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinderKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinderKairos.kt
new file mode 100644
index 0000000..a782116
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinderKairos.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2025 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.mobile.ui.binder
+
+import androidx.core.view.isVisible
+import com.android.systemui.kairos.BuildSpec
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
+import com.android.systemui.lifecycle.repeatWhenWindowIsVisible
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModelKairos
+import com.android.systemui.util.AutoMarqueeTextView
+
+object ShadeCarrierBinderKairos {
+    /** Binds the view to the view-model, continuing to update the former based on the latter */
+    @ExperimentalKairosApi
+    suspend fun bind(
+        carrierTextView: AutoMarqueeTextView,
+        viewModel: BuildSpec<ShadeCarrierGroupMobileIconViewModelKairos>,
+        kairosNetwork: KairosNetwork,
+    ) {
+        carrierTextView.isVisible = true
+        carrierTextView.repeatWhenWindowIsVisible {
+            kairosNetwork.activateSpec {
+                viewModel.applySpec().carrierName.observe { carrierTextView.text = it }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt
index fef5bfe..54cd8e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt
@@ -22,19 +22,28 @@
 import androidx.compose.ui.platform.ViewCompositionStrategy
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.Flags
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
 import com.android.systemui.lifecycle.rememberViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModelImpl
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModelKairos
 import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
 import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIcon
 import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView
+import com.android.systemui.util.composable.kairos.rememberKairosActivatable
 
 object StackedMobileIconBinder {
+    @OptIn(ExperimentalKairosApi::class)
     fun bind(
         view: SingleBindableStatusBarComposeIconView,
         mobileIconsViewModel: MobileIconsViewModel,
         viewModelFactory: StackedMobileIconViewModelImpl.Factory,
+        kairosViewModelFactory: StackedMobileIconViewModelKairos.Factory,
+        kairosNetwork: KairosNetwork,
     ): ModernStatusBarViewBinding {
         return SingleBindableStatusBarComposeIconView.withDefaultBinding(
             view = view,
@@ -47,9 +56,15 @@
                             ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
                         )
                         setContent {
-                            val viewModel =
-                                rememberViewModel("StackedMobileIconBinder") {
-                                    viewModelFactory.create()
+                            val viewModel: StackedMobileIconViewModel =
+                                if (Flags.statusBarMobileIconKairos()) {
+                                    rememberKairosActivatable(kairosNetwork) {
+                                        kairosViewModelFactory.create()
+                                    }
+                                } else {
+                                    rememberViewModel("StackedMobileIconBinder") {
+                                        viewModelFactory.create()
+                                    }
                                 }
                             if (viewModel.isIconVisible) {
                                 CompositionLocalProvider(LocalContentColor provides Color(tint())) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt
index fbd074d..f1c5fee 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt
@@ -20,22 +20,30 @@
 import android.util.AttributeSet
 import android.view.LayoutInflater
 import android.widget.LinearLayout
+import com.android.systemui.kairos.BuildSpec
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
+import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
 import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder
+import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinderKairos
 import com.android.systemui.statusbar.pipeline.mobile.ui.binder.ShadeCarrierBinder
+import com.android.systemui.statusbar.pipeline.mobile.ui.binder.ShadeCarrierBinderKairos
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModelKairos
 import com.android.systemui.util.AutoMarqueeTextView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
 
 /**
  * ViewGroup containing a mobile carrier name and icon in the Shade Header. Can be multiple
  * instances as children under [ShadeCarrierGroup]
  */
-class ModernShadeCarrierGroupMobileView(
-    context: Context,
-    attrs: AttributeSet?,
-) : LinearLayout(context, attrs) {
+class ModernShadeCarrierGroupMobileView(context: Context, attrs: AttributeSet?) :
+    LinearLayout(context, attrs) {
 
     var subId: Int = -1
 
@@ -73,5 +81,49 @@
                     ShadeCarrierBinder.bind(textView, viewModel)
                 }
         }
+
+        /**
+         * Inflates a new instance of [ModernShadeCarrierGroupMobileView], binds it to [viewModel],
+         * and returns it.
+         */
+        @ExperimentalKairosApi
+        @JvmStatic
+        fun constructAndBind(
+            context: Context,
+            logger: MobileViewLogger,
+            slot: String,
+            viewModel: BuildSpec<ShadeCarrierGroupMobileIconViewModelKairos>,
+            scope: CoroutineScope,
+            subscriptionId: Int,
+            location: StatusBarLocation,
+            kairosNetwork: KairosNetwork,
+        ): Pair<ModernShadeCarrierGroupMobileView, Job> {
+            val view =
+                (LayoutInflater.from(context).inflate(R.layout.shade_carrier_new, null)
+                        as ModernShadeCarrierGroupMobileView)
+                    .apply { subId = subscriptionId }
+            return view to
+                scope.launch {
+                    val iconView =
+                        view.requireViewById<ModernStatusBarMobileView>(R.id.mobile_combo)
+                    iconView.initView(slot) {
+                        val (binding, _) =
+                            MobileIconBinderKairos.bind(
+                                view = iconView,
+                                viewModel = viewModel,
+                                initialVisibilityState = STATE_ICON,
+                                logger = logger,
+                                scope = this,
+                                kairosNetwork = kairosNetwork,
+                            )
+                        binding
+                    }
+                    logger.logNewViewBinding(view, viewModel, location.name)
+
+                    val textView =
+                        view.requireViewById<AutoMarqueeTextView>(R.id.mobile_carrier_text)
+                    launch { ShadeCarrierBinderKairos.bind(textView, viewModel, kairosNetwork) }
+                }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
index 7eda87f..382af7e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt
@@ -21,13 +21,22 @@
 import android.view.LayoutInflater
 import android.widget.FrameLayout
 import android.widget.ImageView
+import com.android.settingslib.flags.Flags.newStatusBarIcons
+import com.android.systemui.kairos.BuildSpec
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.StatusBarIconView.getVisibleStateString
 import com.android.systemui.statusbar.core.NewStatusBarIcons
+import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
 import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder
+import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinderKairos
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModelKairos
 import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
 
 class ModernStatusBarMobileView(context: Context, attrs: AttributeSet?) :
     ModernStatusBarView(context, attrs) {
@@ -98,5 +107,58 @@
                     logger.logNewViewBinding(it, viewModel)
                 }
         }
+
+        /**
+         * Inflates a new instance of [ModernStatusBarMobileView], binds it to [viewModel], and
+         * returns it.
+         */
+        @ExperimentalKairosApi
+        @JvmStatic
+        fun constructAndBind(
+            context: Context,
+            logger: MobileViewLogger,
+            slot: String,
+            viewModel: BuildSpec<LocationBasedMobileViewModelKairos>,
+            scope: CoroutineScope,
+            subscriptionId: Int,
+            location: StatusBarLocation,
+            kairosNetwork: KairosNetwork,
+        ): Pair<ModernStatusBarMobileView, Job> {
+            val view =
+                (LayoutInflater.from(context)
+                        .inflate(R.layout.status_bar_mobile_signal_group_new, null)
+                        as ModernStatusBarMobileView)
+                    .apply {
+                        // Flag-specific configuration
+                        if (newStatusBarIcons()) {
+                            // New icon (with no embedded whitespace) is slightly shorter
+                            // (but actually taller)
+                            val iconView = requireViewById<ImageView>(R.id.mobile_signal)
+                            val lp = iconView.layoutParams
+                            lp.height =
+                                context.resources.getDimensionPixelSize(
+                                    R.dimen.status_bar_mobile_signal_size_updated
+                                )
+                        }
+
+                        subId = subscriptionId
+                    }
+
+            lateinit var jobResult: Job
+            view.initView(slot) {
+                val (binding, job) =
+                    MobileIconBinderKairos.bind(
+                        view = view,
+                        viewModel = viewModel,
+                        logger = logger,
+                        scope = scope,
+                        kairosNetwork = kairosNetwork,
+                    )
+                jobResult = job
+                binding
+            }
+            logger.logNewViewBinding(view, viewModel, location.name)
+            return view to jobResult
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt
index 0a0f964..bd42d5b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt
@@ -60,17 +60,12 @@
 }
 
 /**
- * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
- * a single line of service via [MobileIconInteractorKairos] and update the UI based on that
- * subscription's information.
+ * View model for the state of a single mobile icon. Each [MobileIconViewModelKairos] will keep
+ * watch over a single line of service via [MobileIconInteractorKairos] and update the UI based on
+ * that subscription's information.
  *
  * There will be exactly one [MobileIconViewModelKairos] per filtered subscription offered from
  * [MobileIconsInteractorKairos.filteredSubscriptions].
- *
- * For the sake of keeping log spam in check, every flow funding the
- * [MobileIconViewModelKairosCommon] interface is implemented as a [StateFlow]. This ensures that
- * each location-based mobile icon view model gets the exact same information, as well as allows us
- * to log that unified state only once per icon.
  */
 @ExperimentalKairosApi
 class MobileIconViewModelKairos(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt
index ada5500..41c72be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
 
 import androidx.compose.runtime.State as ComposeState
-import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.getValue
 import com.android.systemui.Flags
 import com.android.systemui.KairosActivatable
 import com.android.systemui.KairosBuilder
@@ -31,7 +31,6 @@
 import com.android.systemui.kairos.State as KairosState
 import com.android.systemui.kairos.State
 import com.android.systemui.kairos.buildSpec
-import com.android.systemui.kairos.changes
 import com.android.systemui.kairos.combine
 import com.android.systemui.kairos.flatten
 import com.android.systemui.kairos.map
@@ -46,6 +45,7 @@
 import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger
 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
+import com.android.systemui.util.composable.kairos.toComposeState
 import dagger.Provides
 import dagger.multibindings.ElementsIntoSet
 import javax.inject.Inject
@@ -53,7 +53,7 @@
 
 /**
  * View model for describing the system's current mobile cellular connections. The result is a list
- * of [MobileIconViewModel]s which describe the individual icons and can be bound to
+ * of [MobileIconViewModelKairos]s which describe the individual icons and can be bound to
  * [ModernStatusBarMobileView].
  */
 @ExperimentalKairosApi
@@ -145,20 +145,16 @@
 
 @ExperimentalKairosApi
 class MobileIconsViewModelKairosComposeWrapper(
-    val icons: ComposeState<Map<Int, MobileIconViewModelKairos>>
-)
-
-@ExperimentalKairosApi
-fun composeWrapper(
-    viewModel: MobileIconsViewModelKairos
-): BuildSpec<MobileIconsViewModelKairosComposeWrapper> = buildSpec {
-    MobileIconsViewModelKairosComposeWrapper(icons = toComposeState(viewModel.icons))
+    icons: ComposeState<Map<Int, MobileIconViewModelKairos>>,
+    val logger: MobileViewLogger,
+) {
+    val icons: Map<Int, MobileIconViewModelKairos> by icons
 }
 
 @ExperimentalKairosApi
-fun <T> BuildScope.toComposeState(state: KairosState<T>): ComposeState<T> {
-    val initial = state.sample()
-    val cState = mutableStateOf(initial)
-    state.changes.observe { cState.value = it }
-    return cState
+fun MobileIconsViewModelKairos.composeWrapper(): BuildSpec<MobileIconsViewModelKairosComposeWrapper> = buildSpec {
+    MobileIconsViewModelKairosComposeWrapper(
+        icons = toComposeState(icons),
+        logger = logger,
+    )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
index a052008..cba06e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt
@@ -33,17 +33,20 @@
     fun setIconMap(map: Map<String, MobileIconGroup>) {
         iconMap = map
     }
+
     override fun mapIconSets(config: Config): Map<String, MobileIconGroup> {
         if (useRealImpl) {
             return realImpl.mapIconSets(config)
         }
         return iconMap
     }
+
     fun getIconMap() = iconMap
 
     fun setDefaultIcons(group: MobileIconGroup) {
         defaultIcons = group
     }
+
     override fun getDefaultIcons(config: Config): MobileIconGroup {
         if (useRealImpl) {
             return realImpl.getDefaultIcons(config)
@@ -72,3 +75,6 @@
         return toIconKey(networkType) + "_override"
     }
 }
+
+val MobileMappingsProxy.fake
+    get() = this as FakeMobileMappingsProxy
diff --git a/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ActivatedKairosSpec.kt b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ActivatedKairosSpec.kt
new file mode 100644
index 0000000..a108b10
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ActivatedKairosSpec.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.composable.kairos
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import com.android.systemui.kairos.BuildSpec
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
+import com.android.systemui.kairos.awaitClose
+import com.android.systemui.kairos.launchEffect
+
+/**
+ * Activates the Kairos [buildSpec] within [kairosNetwork], bound to the current composition.
+ *
+ * [block] will be invoked with the result of activating the [buildSpec]. [buildSpec] will be
+ * deactivated automatically when [ActivatedKairosSpec] leaves the composition.
+ */
+@ExperimentalKairosApi
+@Composable
+fun <T> ActivatedKairosSpec(
+    buildSpec: BuildSpec<T>,
+    kairosNetwork: KairosNetwork,
+    block: @Composable (T) -> Unit,
+) {
+    val uninit = Any()
+    var state by remember { mutableStateOf<Any?>(uninit) }
+    LaunchedEffect(key1 = Unit) {
+        kairosNetwork.activateSpec {
+            val v = buildSpec.applySpec()
+            launchEffect {
+                state = v
+                awaitClose { state = uninit }
+            }
+        }
+    }
+    state.let {
+        if (it !== uninit) {
+            @Suppress("UNCHECKED_CAST") block(it as T)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/composable/kairos/RememberKairosActivatable.kt b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/RememberKairosActivatable.kt
new file mode 100644
index 0000000..03a60bc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/RememberKairosActivatable.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2025 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.util.composable.kairos
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import com.android.systemui.KairosActivatable
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
+
+@ExperimentalKairosApi
+@Composable
+fun <T : KairosActivatable> rememberKairosActivatable(
+    kairosNetwork: KairosNetwork,
+    key: Any = Unit,
+    factory: () -> T,
+): T {
+    val instance = remember(key, factory) { factory() }
+    LaunchedEffect(instance, kairosNetwork) {
+        kairosNetwork.activateSpec { instance.run { activate() } }
+    }
+    return instance
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ToComposeState.kt b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ToComposeState.kt
new file mode 100644
index 0000000..b3accb6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/ToComposeState.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2025 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.util.composable.kairos
+
+import androidx.compose.runtime.State as ComposeState
+import androidx.compose.runtime.mutableStateOf
+import com.android.systemui.kairos.BuildScope
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.State as KairosState
+import com.android.systemui.kairos.changes
+
+/**
+ * Returns a [ComposeState] that is kept hydrated with the current value of [state] within this
+ * [BuildScope].
+ */
+@ExperimentalKairosApi
+fun <T> BuildScope.toComposeState(state: KairosState<T>): ComposeState<T> {
+    val initial = state.sample()
+    val cState = mutableStateOf(initial)
+    state.changes.observe { cState.value = it }
+    return cState
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
index 34e5bfd..9a9f335 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
@@ -14,10 +14,14 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalKairosApi::class)
+
 package com.android.systemui.shade.ui.viewmodel
 
 import android.content.applicationContext
 import com.android.systemui.battery.batteryMeterViewControllerFactory
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.kairos
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -46,6 +50,8 @@
             tintedIconManagerFactory = tintedIconManagerFactory,
             batteryMeterViewControllerFactory = batteryMeterViewControllerFactory,
             statusBarIconController = mock<StatusBarIconController>(),
+            kairosNetwork = kairos,
+            mobileIconsViewModelKairos = mock(),
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ui/TintedIconManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ui/TintedIconManagerKosmos.kt
index 8e13b62..fd63ce2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ui/TintedIconManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ui/TintedIconManagerKosmos.kt
@@ -16,16 +16,24 @@
 
 package com.android.systemui.statusbar.phone.ui
 
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.kairos
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.statusbar.connectivity.ui.mobileContextProvider
 import com.android.systemui.statusbar.pipeline.mobile.ui.mobileUiAdapter
+import com.android.systemui.statusbar.pipeline.mobile.ui.mobileUiAdapterKairos
 import com.android.systemui.statusbar.pipeline.wifi.ui.wifiUiAdapter
 
+@OptIn(ExperimentalKairosApi::class)
 val Kosmos.tintedIconManagerFactory by
-Kosmos.Fixture {
-    TintedIconManager.Factory(
-        wifiUiAdapter,
-        mobileUiAdapter,
-        mobileContextProvider,
-    )
-}
\ No newline at end of file
+    Kosmos.Fixture {
+        TintedIconManager.Factory(
+            wifiUiAdapter,
+            mobileUiAdapter,
+            mobileContextProvider,
+            { mobileUiAdapterKairos },
+            kairos,
+            applicationCoroutineScope,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairosKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairosKosmos.kt
new file mode 100644
index 0000000..3a3f18a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapterKairosKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2025 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.mobile.ui
+
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kairos.ActivatedKairosFixture
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.phone.ui.statusBarIconController
+import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.mobileIconsViewModelKairos
+
+@ExperimentalKairosApi
+val Kosmos.mobileUiAdapterKairos by ActivatedKairosFixture {
+    MobileUiAdapterKairos(
+        statusBarIconController,
+        mobileIconsViewModelKairos,
+        mobileViewLogger,
+        dumpManager,
+    )
+}