[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,
+ )
+}