diff --git a/packages/SystemUI/res-keyguard/layout/shade_carrier_new.xml b/packages/SystemUI/res-keyguard/layout/shade_carrier_new.xml new file mode 100644 index 0000000000000..952f056b30239 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/shade_carrier_new.xml @@ -0,0 +1,42 @@ + + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index b92bf6b616696..aec7974989160 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -398,6 +398,10 @@ object Flags { // TODO(b/294588085): Tracking Bug val WIFI_SECONDARY_NETWORKS = releasedFlag("wifi_secondary_networks") + // TODO(b/290676905): Tracking Bug + val NEW_SHADE_CARRIER_GROUP_MOBILE_ICONS = + unreleasedFlag("new_shade_carrier_group_mobile_icons") + // 700 - dialer/calls // TODO(b/254512734): Tracking Bug val ONGOING_CALL_STATUS_BAR_CHIP = releasedFlag("ongoing_call_status_bar_chip") diff --git a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrier.java b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrier.java index 8586828af0cdd..8612cdf12c6e4 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrier.java +++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrier.java @@ -34,6 +34,7 @@ import com.android.settingslib.Utils; import com.android.settingslib.graph.SignalDrawable; import com.android.systemui.FontSizeUtils; import com.android.systemui.R; +import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView; import com.android.systemui.util.LargeScreenUtils; import java.util.Objects; @@ -44,6 +45,7 @@ public class ShadeCarrier extends LinearLayout { private TextView mCarrierText; private ImageView mMobileSignal; private ImageView mMobileRoaming; + private ModernShadeCarrierGroupMobileView mModernMobileView; private View mSpacer; @Nullable private CellSignalState mLastSignalState; @@ -77,6 +79,23 @@ public class ShadeCarrier extends LinearLayout { updateResources(); } + /** Removes a ModernStatusBarMobileView from the ViewGroup. */ + public void removeModernMobileView() { + if (mModernMobileView != null) { + removeView(mModernMobileView); + mModernMobileView = null; + } + } + + /** Adds a ModernStatusBarMobileView to the ViewGroup. */ + public void addModernMobileView(ModernShadeCarrierGroupMobileView mobileView) { + mModernMobileView = mobileView; + mMobileGroup.setVisibility(View.GONE); + mSpacer.setVisibility(View.GONE); + mCarrierText.setVisibility(View.GONE); + addView(mobileView); + } + /** * Update the state of this view * @param state the current state of the signal for this view 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 ad49b267c6ac9..98d8a53b83c8a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/carrier/ShadeCarrierGroupController.java @@ -16,6 +16,7 @@ package com.android.systemui.shade.carrier; +import static android.telephony.SubscriptionManager.INVALID_SIM_SLOT_INDEX; import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES; import android.annotation.MainThread; @@ -46,8 +47,17 @@ import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.connectivity.MobileDataIndicators; import com.android.systemui.statusbar.connectivity.NetworkController; import com.android.systemui.statusbar.connectivity.SignalCallback; +import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider; +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.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.ShadeCarrierGroupMobileIconViewModel; import com.android.systemui.util.CarrierConfigTracker; +import java.util.List; import java.util.function.Consumer; import javax.inject.Inject; @@ -62,12 +72,16 @@ public class ShadeCarrierGroupController { private final ActivityStarter mActivityStarter; private final Handler mBgHandler; + private final Context mContext; private final NetworkController mNetworkController; private final CarrierTextManager mCarrierTextManager; private final TextView mNoSimTextView; // Non final for testing private H mMainHandler; private final Callback mCallback; + private final MobileIconsViewModel mMobileIconsViewModel; + private final MobileContextProvider mMobileContextProvider; + private final StatusBarPipelineFlags mStatusBarPipelineFlags; private boolean mListening; private final CellSignalState[] mInfos = new CellSignalState[SIM_SLOTS]; @@ -91,7 +105,7 @@ public class ShadeCarrierGroupController { Log.w(TAG, "setMobileDataIndicators - slot: " + slotIndex); return; } - if (slotIndex == SubscriptionManager.INVALID_SIM_SLOT_INDEX) { + if (slotIndex == INVALID_SIM_SLOT_INDEX) { Log.e(TAG, "Invalid SIM slot index for subscription: " + indicators.subId); return; } @@ -129,15 +143,25 @@ public class ShadeCarrierGroupController { } } - private ShadeCarrierGroupController(ShadeCarrierGroup view, ActivityStarter activityStarter, - @Background Handler bgHandler, @Main Looper mainLooper, + private ShadeCarrierGroupController( + ShadeCarrierGroup view, + ActivityStarter activityStarter, + @Background Handler bgHandler, + @Main Looper mainLooper, NetworkController networkController, - CarrierTextManager.Builder carrierTextManagerBuilder, Context context, - CarrierConfigTracker carrierConfigTracker, SlotIndexResolver slotIndexResolver) { - + CarrierTextManager.Builder carrierTextManagerBuilder, + Context context, + CarrierConfigTracker carrierConfigTracker, + SlotIndexResolver slotIndexResolver, + MobileUiAdapter mobileUiAdapter, + MobileContextProvider mobileContextProvider, + StatusBarPipelineFlags statusBarPipelineFlags + ) { + mContext = context; mActivityStarter = activityStarter; mBgHandler = bgHandler; mNetworkController = networkController; + mStatusBarPipelineFlags = statusBarPipelineFlags; mCarrierTextManager = carrierTextManagerBuilder .setShowAirplaneMode(false) .setShowMissingSim(false) @@ -162,6 +186,14 @@ public class ShadeCarrierGroupController { mCarrierGroups[1] = view.getCarrier2View(); mCarrierGroups[2] = view.getCarrier3View(); + mMobileContextProvider = mobileContextProvider; + mMobileIconsViewModel = mobileUiAdapter.getMobileIconsViewModel(); + + if (mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) { + mobileUiAdapter.setShadeCarrierGroupController(this); + MobileIconsBinder.bind(view, mMobileIconsViewModel); + } + mCarrierDividers[0] = view.getCarrierDivider1(); mCarrierDividers[1] = view.getCarrierDivider2(); @@ -193,6 +225,50 @@ public class ShadeCarrierGroupController { }); } + /** Updates the number of visible mobile icons using the new pipeline. */ + public void updateModernMobileIcons(List subIds) { + if (!mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) { + Log.d(TAG, "ignoring new pipeline callback because new mobile icon is disabled"); + return; + } + + for (ShadeCarrier carrier : mCarrierGroups) { + carrier.removeModernMobileView(); + } + + List iconDataList = processSubIdList(subIds); + + 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); + } + } + + @VisibleForTesting + List processSubIdList(List subIds) { + return subIds + .stream() + .limit(SIM_SLOTS) + .map(subId -> new IconData(subId, getSlotIndex(subId))) + .filter(iconData -> + iconData.slotIndex < SIM_SLOTS + && iconData.slotIndex != INVALID_SIM_SLOT_INDEX + ) + .toList(); + } + @VisibleForTesting protected int getSlotIndex(int subscriptionId) { return mSlotIndexResolver.getSlotIndex(subscriptionId); @@ -269,8 +345,12 @@ public class ShadeCarrierGroupController { } } - for (int i = 0; i < SIM_SLOTS; i++) { - mCarrierGroups[i].updateState(mInfos[i], singleCarrier); + if (mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()) { + Log.d(TAG, "ignoring old pipeline callback because new mobile icon is enabled"); + } else { + for (int i = 0; i < SIM_SLOTS; i++) { + mCarrierGroups[i].updateState(mInfos[i], singleCarrier); + } } mCarrierDividers[0].setVisibility( @@ -306,7 +386,7 @@ public class ShadeCarrierGroupController { Log.w(TAG, "updateInfoCarrier - slot: " + slot); continue; } - if (slot == SubscriptionManager.INVALID_SIM_SLOT_INDEX) { + if (slot == INVALID_SIM_SLOT_INDEX) { Log.e(TAG, "Invalid SIM slot index for subscription: " + info.subscriptionIds[i]); @@ -385,12 +465,24 @@ public class ShadeCarrierGroupController { private final Context mContext; private final CarrierConfigTracker mCarrierConfigTracker; private final SlotIndexResolver mSlotIndexResolver; + private final MobileUiAdapter mMobileUiAdapter; + private final MobileContextProvider mMobileContextProvider; + private final StatusBarPipelineFlags mStatusBarPipelineFlags; @Inject - public Builder(ActivityStarter activityStarter, @Background Handler handler, - @Main Looper looper, NetworkController networkController, - CarrierTextManager.Builder carrierTextControllerBuilder, Context context, - CarrierConfigTracker carrierConfigTracker, SlotIndexResolver slotIndexResolver) { + public Builder( + ActivityStarter activityStarter, + @Background Handler handler, + @Main Looper looper, + NetworkController networkController, + CarrierTextManager.Builder carrierTextControllerBuilder, + Context context, + CarrierConfigTracker carrierConfigTracker, + SlotIndexResolver slotIndexResolver, + MobileUiAdapter mobileUiAdapter, + MobileContextProvider mobileContextProvider, + StatusBarPipelineFlags statusBarPipelineFlags + ) { mActivityStarter = activityStarter; mHandler = handler; mLooper = looper; @@ -399,6 +491,9 @@ public class ShadeCarrierGroupController { mContext = context; mCarrierConfigTracker = carrierConfigTracker; mSlotIndexResolver = slotIndexResolver; + mMobileUiAdapter = mobileUiAdapter; + mMobileContextProvider = mobileContextProvider; + mStatusBarPipelineFlags = statusBarPipelineFlags; } public Builder setShadeCarrierGroup(ShadeCarrierGroup view) { @@ -407,9 +502,20 @@ public class ShadeCarrierGroupController { } public ShadeCarrierGroupController build() { - return new ShadeCarrierGroupController(mView, mActivityStarter, mHandler, mLooper, - mNetworkController, mCarrierTextControllerBuilder, mContext, - mCarrierConfigTracker, mSlotIndexResolver); + return new ShadeCarrierGroupController( + mView, + mActivityStarter, + mHandler, + mLooper, + mNetworkController, + mCarrierTextControllerBuilder, + mContext, + mCarrierConfigTracker, + mSlotIndexResolver, + mMobileUiAdapter, + mMobileContextProvider, + mStatusBarPipelineFlags + ); } } @@ -448,4 +554,15 @@ public class ShadeCarrierGroupController { return SubscriptionManager.getSlotIndex(subscriptionId); } } + + @VisibleForTesting + static class IconData { + public final int subId; + public final int slotIndex; + + IconData(int subId, int slotIndex) { + this.subId = subId; + this.slotIndex = slotIndex; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt index 5ace22695ec37..32e5c355889a2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt @@ -24,4 +24,6 @@ enum class StatusBarLocation { KEYGUARD, /** Quick settings (inside the shade). */ QS, + /** ShadeCarrierGroup (above QS status bar in expanded mode). */ + SHADE_CARRIER_GROUP, } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt index 6e51ed0eba378..c695773086082 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt @@ -18,6 +18,9 @@ package com.android.systemui.statusbar.pipeline import android.content.Context import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.shade.carrier.ShadeCarrierGroup import javax.inject.Inject /** All flagging methods related to the new status bar pipeline (see b/238425913). */ @@ -26,10 +29,18 @@ class StatusBarPipelineFlags @Inject constructor( context: Context, + private val featureFlags: FeatureFlags, ) { private val mobileSlot = context.getString(com.android.internal.R.string.status_bar_mobile) private val wifiSlot = context.getString(com.android.internal.R.string.status_bar_wifi) + /** + * True if we should display the mobile icons in the [ShadeCarrierGroup] using the new status + * bar Data pipeline. + */ + fun useNewShadeCarrierGroupMobileIcons(): Boolean = + featureFlags.isEnabled(Flags.NEW_SHADE_CARRIER_GROUP_MOBILE_ICONS) + /** * For convenience in the StatusBarIconController, we want to gate some actions based on slot * name and the flag together. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt index d7fcf4876c285..02e50a007f50d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shade.carrier.ShadeCarrierGroupController import com.android.systemui.statusbar.phone.StatusBarIconController import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel @@ -49,6 +50,8 @@ constructor( private var isCollecting: Boolean = false private var lastValue: List? = null + private var shadeCarrierGroupController: ShadeCarrierGroupController? = null + override fun start() { // Start notifying the icon controller of subscriptions scope.launch { @@ -57,10 +60,16 @@ constructor( logger.logUiAdapterSubIdsSentToIconController(it) lastValue = it iconController.setNewMobileIconSubIds(it) + shadeCarrierGroupController?.updateModernMobileIcons(it) } } } + /** Set the [ShadeCarrierGroupController] to notify of subscription updates */ + fun setShadeCarrierGroupController(controller: ShadeCarrierGroupController) { + shadeCarrierGroupController = controller + } + override fun dump(pw: PrintWriter, args: Array) { 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 cea6654a48de0..2af6795b39c41 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 @@ -57,7 +57,7 @@ constructor( { str1 = view.getIdForLogging() str2 = viewModel.getIdForLogging() - str3 = viewModel.locationName + str3 = viewModel.location.name }, { "New view binding. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, ) @@ -71,7 +71,7 @@ constructor( { str1 = view.getIdForLogging() str2 = viewModel.getIdForLogging() - str3 = viewModel.locationName + str3 = viewModel.location.name }, { "Collection started. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, ) @@ -85,7 +85,7 @@ constructor( { str1 = view.getIdForLogging() str2 = viewModel.getIdForLogging() - str3 = viewModel.locationName + str3 = viewModel.location.name }, { "Collection stopped. viewId=$str1, viewModelId=$str2, viewModelLocation=$str3" }, ) 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 55bc8d58be23e..4b2fb4373957a 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,6 +50,7 @@ object MobileIconBinder { fun bind( view: ViewGroup, viewModel: LocationBasedMobileViewModel, + @StatusBarIconView.VisibleState initialVisibilityState: Int = STATE_HIDDEN, logger: MobileViewLogger, ): ModernStatusBarViewBinding { val mobileGroupView = view.requireViewById(R.id.mobile_group) @@ -68,12 +69,12 @@ object MobileIconBinder { // TODO(b/238425913): We should log this visibility state. @StatusBarIconView.VisibleState - val visibilityState: MutableStateFlow = MutableStateFlow(STATE_HIDDEN) + val visibilityState: MutableStateFlow = MutableStateFlow(initialVisibilityState) val iconTint: MutableStateFlow = MutableStateFlow(viewModel.defaultColor) val decorTint: MutableStateFlow = MutableStateFlow(viewModel.defaultColor) - var isCollecting: Boolean = false + var isCollecting = false view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { 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 new file mode 100644 index 0000000000000..081e1015e26e6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/ShadeCarrierBinder.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.binder + +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel +import com.android.systemui.util.AutoMarqueeTextView +import kotlinx.coroutines.launch + +object ShadeCarrierBinder { + /** Binds the view to the view-model, continuing to update the former based on the latter */ + @JvmStatic + fun bind( + carrierTextView: AutoMarqueeTextView, + viewModel: ShadeCarrierGroupMobileIconViewModel, + ) { + carrierTextView.isVisible = true + + carrierTextView.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { viewModel.carrierName.collect { carrierTextView.text = it } } + } + } + } +} 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 new file mode 100644 index 0000000000000..f407127f3161e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernShadeCarrierGroupMobileView.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.view + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import com.android.systemui.R +import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON +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.ShadeCarrierBinder +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel +import com.android.systemui.util.AutoMarqueeTextView + +/** + * 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) { + + var subId: Int = -1 + + override fun toString(): String { + return "ModernShadeCarrierGroupMobileView(" + + "subId=$subId, " + + "viewString=${super.toString()}" + } + + companion object { + + /** + * Inflates a new instance of [ModernShadeCarrierGroupMobileView], binds it to [viewModel], + * and returns it. + */ + @JvmStatic + fun constructAndBind( + context: Context, + logger: MobileViewLogger, + slot: String, + viewModel: ShadeCarrierGroupMobileIconViewModel, + ): ModernShadeCarrierGroupMobileView { + return (LayoutInflater.from(context).inflate(R.layout.shade_carrier_new, null) + as ModernShadeCarrierGroupMobileView) + .also { + it.subId = viewModel.subscriptionId + + val iconView = it.requireViewById(R.id.mobile_combo) + iconView.initView(slot) { + MobileIconBinder.bind(iconView, viewModel, STATE_ICON, logger) + } + logger.logNewViewBinding(it, viewModel) + + val textView = it.requireViewById(R.id.mobile_carrier_text) + ShadeCarrierBinder.bind(textView, viewModel) + } + } + } +} 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 4144293d5ccd4..68d02de51dc9f 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 @@ -60,7 +60,9 @@ class ModernStatusBarMobileView( as ModernStatusBarMobileView) .also { it.subId = viewModel.subscriptionId - it.initView(slot) { MobileIconBinder.bind(it, viewModel, logger) } + it.initView(slot) { + MobileIconBinder.bind(view = it, viewModel = viewModel, logger = logger) + } logger.logNewViewBinding(it, viewModel) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt index a51982c41255b..e7c311d7ec74d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt @@ -18,7 +18,13 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import android.graphics.Color import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn /** * A view model for an individual mobile icon that embeds the notion of a [StatusBarLocation]. This @@ -26,12 +32,12 @@ import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger * * @param commonImpl for convenience, this class wraps a base interface that can provides all of the * common implementations between locations. See [MobileIconViewModel] - * @property locationName the name of the location of this VM, used for logging. + * @property location the [StatusBarLocation] of this VM. * @property verboseLogger an optional logger to log extremely verbose view updates. */ abstract class LocationBasedMobileViewModel( val commonImpl: MobileIconViewModelCommon, - val locationName: String, + val location: StatusBarLocation, val verboseLogger: VerboseMobileViewLogger?, ) : MobileIconViewModelCommon by commonImpl { val defaultColor: Int = Color.WHITE @@ -39,10 +45,12 @@ abstract class LocationBasedMobileViewModel( companion object { fun viewModelForLocation( commonImpl: MobileIconViewModelCommon, + interactor: MobileIconInteractor, verboseMobileViewLogger: VerboseMobileViewLogger, - loc: StatusBarLocation, + location: StatusBarLocation, + scope: CoroutineScope, ): LocationBasedMobileViewModel = - when (loc) { + when (location) { StatusBarLocation.HOME -> HomeMobileIconViewModel( commonImpl, @@ -50,6 +58,12 @@ abstract class LocationBasedMobileViewModel( ) StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl) StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl) + StatusBarLocation.SHADE_CARRIER_GROUP -> + ShadeCarrierGroupMobileIconViewModel( + commonImpl, + interactor, + scope, + ) } } } @@ -61,7 +75,7 @@ class HomeMobileIconViewModel( MobileIconViewModelCommon, LocationBasedMobileViewModel( commonImpl, - locationName = "Home", + location = StatusBarLocation.HOME, verboseMobileViewLogger, ) @@ -71,18 +85,40 @@ class QsMobileIconViewModel( MobileIconViewModelCommon, LocationBasedMobileViewModel( commonImpl, - locationName = "QS", + location = StatusBarLocation.QS, // Only do verbose logging for the Home location. verboseLogger = null, ) +class ShadeCarrierGroupMobileIconViewModel( + commonImpl: MobileIconViewModelCommon, + interactor: MobileIconInteractor, + scope: CoroutineScope, +) : + MobileIconViewModelCommon, + LocationBasedMobileViewModel( + commonImpl, + location = StatusBarLocation.SHADE_CARRIER_GROUP, + // Only do verbose logging for the Home location. + verboseLogger = null, + ) { + private val isSingleCarrier = interactor.isSingleCarrier + val carrierName = interactor.carrierName + + override val isVisible: StateFlow = + combine(super.isVisible, isSingleCarrier) { isVisible, isSingleCarrier -> + if (isSingleCarrier) false else isVisible + } + .stateIn(scope, SharingStarted.WhileSubscribed(), super.isVisible.value) +} + class KeyguardMobileIconViewModel( commonImpl: MobileIconViewModelCommon, ) : MobileIconViewModelCommon, LocationBasedMobileViewModel( commonImpl, - locationName = "Keyguard", + location = StatusBarLocation.KEYGUARD, // Only do verbose logging for the Home location. verboseLogger = null, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt index 5cf887e4d41ac..216afb987f919 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger @@ -58,6 +59,8 @@ constructor( private val statusBarPipelineFlags: StatusBarPipelineFlags, ) { @VisibleForTesting val mobileIconSubIdCache = mutableMapOf() + @VisibleForTesting + val mobileIconInteractorSubIdCache = mutableMapOf() val subscriptionIdsFlow: StateFlow> = interactor.filteredSubscriptions @@ -91,15 +94,17 @@ constructor( .stateIn(scope, SharingStarted.WhileSubscribed(), false) init { - scope.launch { subscriptionIdsFlow.collect { removeInvalidModelsFromCache(it) } } + scope.launch { subscriptionIdsFlow.collect { invalidateCaches(it) } } } fun viewModelForSub(subId: Int, location: StatusBarLocation): LocationBasedMobileViewModel { val common = commonViewModelForSub(subId) return LocationBasedMobileViewModel.viewModelForLocation( common, + mobileIconInteractorForSub(subId), verboseLogger, location, + scope, ) } @@ -107,7 +112,7 @@ constructor( return mobileIconSubIdCache[subId] ?: MobileIconViewModel( subId, - interactor.createMobileConnectionInteractorForSubId(subId), + mobileIconInteractorForSub(subId), airplaneModeInteractor, constants, scope, @@ -115,8 +120,20 @@ constructor( .also { mobileIconSubIdCache[subId] = it } } - private fun removeInvalidModelsFromCache(subIds: List) { + @VisibleForTesting + fun mobileIconInteractorForSub(subId: Int): MobileIconInteractor { + return mobileIconInteractorSubIdCache[subId] + ?: interactor.createMobileConnectionInteractorForSubId(subId).also { + mobileIconInteractorSubIdCache[subId] = it + } + } + + private fun invalidateCaches(subIds: List) { val subIdsToRemove = mobileIconSubIdCache.keys.filter { !subIds.contains(it) } subIdsToRemove.forEach { mobileIconSubIdCache.remove(it) } + + mobileIconInteractorSubIdCache.keys + .filter { !subIds.contains(it) } + .forEach { subId -> mobileIconInteractorSubIdCache.remove(subId) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt index cd5b92cf24c74..00bd616e04eac 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel import android.graphics.Color import com.android.systemui.statusbar.phone.StatusBarLocation +import java.lang.IllegalArgumentException /** * A view model for a wifi icon in a specific location. This allows us to control parameters that @@ -43,6 +44,8 @@ abstract class LocationBasedWifiViewModel( StatusBarLocation.HOME -> HomeWifiViewModel(commonImpl) StatusBarLocation.KEYGUARD -> KeyguardWifiViewModel(commonImpl) StatusBarLocation.QS -> QsWifiViewModel(commonImpl) + StatusBarLocation.SHADE_CARRIER_GROUP -> + throw IllegalArgumentException("invalid location for WifiViewModel: $location") } } } @@ -64,3 +67,11 @@ class KeyguardWifiViewModel( class QsWifiViewModel( commonImpl: WifiViewModelCommon, ) : WifiViewModelCommon, LocationBasedWifiViewModel(commonImpl) + +/** + * A view model for the wifi icon in the shade carrier group (visible when quick settings is fully + * expanded, and in large screen shade). Currently unused. + */ +class ShadeCarrierGroupWifiViewModel( + commonImpl: WifiViewModelCommon, +) : WifiViewModelCommon, LocationBasedWifiViewModel(commonImpl) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java index 31bfa3fdf8cba..5fa6b3a15d351 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/carrier/ShadeCarrierGroupControllerTest.java @@ -29,6 +29,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @@ -50,6 +51,12 @@ import com.android.systemui.statusbar.connectivity.IconState; import com.android.systemui.statusbar.connectivity.MobileDataIndicators; import com.android.systemui.statusbar.connectivity.NetworkController; import com.android.systemui.statusbar.connectivity.SignalCallback; +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.MobileViewLogger; +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel; import com.android.systemui.util.CarrierConfigTracker; import com.android.systemui.utils.leaks.LeakCheckedTest; import com.android.systemui.utils.os.FakeHandler; @@ -61,6 +68,10 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper @SmallTest @@ -95,6 +106,18 @@ public class ShadeCarrierGroupControllerTest extends LeakCheckedTest { private TestableLooper mTestableLooper; @Mock private ShadeCarrierGroupController.OnSingleCarrierChangedListener mOnSingleCarrierChangedListener; + @Mock + private MobileUiAdapter mMobileUiAdapter; + @Mock + private MobileIconsViewModel mMobileIconsViewModel; + @Mock + private ShadeCarrierGroupMobileIconViewModel mShadeCarrierGroupMobileIconViewModel; + @Mock + private MobileViewLogger mMobileViewLogger; + @Mock + private MobileContextProvider mMobileContextProvider; + @Mock + private StatusBarPipelineFlags mStatusBarPipelineFlags; private FakeSlotIndexResolver mSlotIndexResolver; private ClickListenerTextView mNoCarrierTextView; @@ -133,16 +156,35 @@ public class ShadeCarrierGroupControllerTest extends LeakCheckedTest { mSlotIndexResolver = new FakeSlotIndexResolver(); + when(mMobileUiAdapter.getMobileIconsViewModel()).thenReturn(mMobileIconsViewModel); + mShadeCarrierGroupController = new ShadeCarrierGroupController.Builder( - mActivityStarter, handler, TestableLooper.get(this).getLooper(), - mNetworkController, mCarrierTextControllerBuilder, mContext, mCarrierConfigTracker, - mSlotIndexResolver) + mActivityStarter, + handler, + TestableLooper.get(this).getLooper(), + mNetworkController, + mCarrierTextControllerBuilder, + mContext, + mCarrierConfigTracker, + mSlotIndexResolver, + mMobileUiAdapter, + mMobileContextProvider, + mStatusBarPipelineFlags + ) .setShadeCarrierGroup(mShadeCarrierGroup) .build(); mShadeCarrierGroupController.setListening(true); } + private void setupWithNewPipeline() { + when(mStatusBarPipelineFlags.useNewShadeCarrierGroupMobileIcons()).thenReturn(true); + when(mMobileContextProvider.getMobileContextForSub(anyInt(), any())).thenReturn(mContext); + when(mMobileIconsViewModel.getLogger()).thenReturn(mMobileViewLogger); + when(mMobileIconsViewModel.viewModelForSub(anyInt(), any())) + .thenReturn(mShadeCarrierGroupMobileIconViewModel); + } + @Test public void testInitiallyMultiCarrier() { assertFalse(mShadeCarrierGroupController.isSingleCarrier()); @@ -406,6 +448,129 @@ public class ShadeCarrierGroupControllerTest extends LeakCheckedTest { verify(mOnSingleCarrierChangedListener, never()).onSingleCarrierChanged(anyBoolean()); } + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testUpdateModernMobileIcons_addSubscription() { + setupWithNewPipeline(); + + mShadeCarrier1.setVisibility(View.GONE); + mShadeCarrier2.setVisibility(View.GONE); + mShadeCarrier3.setVisibility(View.GONE); + + List subIds = new ArrayList<>(); + subIds.add(0); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2, never()).addModernMobileView(any()); + verify(mShadeCarrier3, never()).addModernMobileView(any()); + + resetShadeCarriers(); + + subIds.add(1); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1, times(1)).removeModernMobileView(); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2).addModernMobileView(any()); + verify(mShadeCarrier3, never()).addModernMobileView(any()); + } + + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testUpdateModernMobileIcons_removeSubscription() { + setupWithNewPipeline(); + + List subIds = new ArrayList<>(); + subIds.add(0); + subIds.add(1); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2).addModernMobileView(any()); + verify(mShadeCarrier3, never()).addModernMobileView(any()); + + resetShadeCarriers(); + + subIds.remove(1); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1, times(1)).removeModernMobileView(); + verify(mShadeCarrier2, times(1)).removeModernMobileView(); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2, never()).addModernMobileView(any()); + verify(mShadeCarrier3, never()).addModernMobileView(any()); + } + + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testUpdateModernMobileIcons_removeSubscriptionOutOfOrder() { + setupWithNewPipeline(); + + List subIds = new ArrayList<>(); + subIds.add(0); + subIds.add(1); + subIds.add(2); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2).addModernMobileView(any()); + verify(mShadeCarrier3).addModernMobileView(any()); + + resetShadeCarriers(); + + subIds.remove(1); + mShadeCarrierGroupController.updateModernMobileIcons(subIds); + + verify(mShadeCarrier1).removeModernMobileView(); + verify(mShadeCarrier2).removeModernMobileView(); + verify(mShadeCarrier3).removeModernMobileView(); + + verify(mShadeCarrier1).addModernMobileView(any()); + verify(mShadeCarrier2, never()).addModernMobileView(any()); + verify(mShadeCarrier3).addModernMobileView(any()); + } + + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testProcessSubIdList_moreSubsThanSimSlots_listLimitedToMax() { + setupWithNewPipeline(); + + List subIds = Arrays.asList(0, 1, 2, 2); + + assertThat(mShadeCarrierGroupController.processSubIdList(subIds).size()).isEqualTo(3); + } + + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testProcessSubIdList_invalidSimSlotIndexFilteredOut() { + setupWithNewPipeline(); + + List subIds = Arrays.asList(0, 1, -1); + + List processedSubs = + mShadeCarrierGroupController.processSubIdList(subIds); + assertThat(processedSubs).hasSize(2); + assertThat(processedSubs.get(0).subId).isNotEqualTo(-1); + assertThat(processedSubs.get(1).subId).isNotEqualTo(-1); + } + + @TestableLooper.RunWithLooper(setAsMainLooper = true) + @Test + public void testProcessSubIdList_indexGreaterThanSimSlotsFilteredOut() { + setupWithNewPipeline(); + + List subIds = Arrays.asList(0, 4); + + List processedSubs = + mShadeCarrierGroupController.processSubIdList(subIds); + assertThat(processedSubs).hasSize(1); + assertThat(processedSubs.get(0).subId).isNotEqualTo(4); + } + + @Test public void testOnlyInternalViewsHaveClickableListener() { ArgumentCaptor captor = @@ -447,6 +612,12 @@ public class ShadeCarrierGroupControllerTest extends LeakCheckedTest { .isEqualTo(Settings.ACTION_WIRELESS_SETTINGS); } + private void resetShadeCarriers() { + reset(mShadeCarrier1); + reset(mShadeCarrier2); + reset(mShadeCarrier3); + } + private class FakeSlotIndexResolver implements ShadeCarrierGroupController.SlotIndexResolver { public boolean overrideInvalid; diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt index e42515e5871de..065dfbabf051c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt @@ -153,6 +153,33 @@ class MobileIconsViewModelTest : SysuiTestCase() { assertThat(underTest.mobileIconSubIdCache).containsExactly(2, model2.commonImpl) } + @Test + fun caching_mobileIconInteractorIsReusedForSameSubId() = + testScope.runTest { + val interactor1 = underTest.mobileIconInteractorForSub(1) + val interactor2 = underTest.mobileIconInteractorForSub(1) + + assertThat(interactor1).isSameInstanceAs(interactor2) + } + + @Test + fun caching_invalidInteractorssAreRemovedFromCacheWhenSubDisappears() = + testScope.runTest { + // Retrieve interactors to trigger caching + val interactor1 = underTest.mobileIconInteractorForSub(1) + val interactor2 = underTest.mobileIconInteractorForSub(2) + + // Both impls are cached + assertThat(underTest.mobileIconInteractorSubIdCache) + .containsExactly(1, interactor1, 2, interactor2) + + // SUB_1 is removed from the list... + interactor.filteredSubscriptions.value = listOf(SUB_2) + + // ... and dropped from the cache + assertThat(underTest.mobileIconInteractorSubIdCache).containsExactly(2, interactor2) + } + @Test fun firstMobileSubShowingNetworkTypeIcon_noSubs_false() = testScope.runTest {