[2/2] Migrating ShadeCarrierGroup mobile icons to modern status bar data pipeline

Hooks up the ShadeCarrierGroup to depend on only the new data pipeline. The UI is left mostly as-is for now, with the change being the ShadeCarrier no longer having its own version of the mobile icon layout, and the text view for the carrier name has been moved inside the ModernStatusBarView.

Bug: 288631206
Test: Manually verified no visual change with flag enabled, with both 1 and 2 sims
Test: Added/updated unit tests
Change-Id: Iaf91e5606640c5fad6e51ce3b6607e65f14d3bcb
This commit is contained in:
Shawn Lee
2023-07-10 15:09:23 -07:00
parent 1685e6b931
commit 062fbc4578
17 changed files with 623 additions and 35 deletions

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
**
** Copyright 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.
*/
-->
<com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/carrier_combo"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal" >
<com.android.systemui.util.AutoMarqueeTextView
android:id="@+id/mobile_carrier_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="@dimen/qs_carrier_margin_width"
android:visibility="gone"
android:textDirection="locale"
android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true"
android:maxEms="7"/>
<include layout="@layout/status_bar_mobile_signal_group_new" />
</com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView>

View File

@@ -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")

View File

@@ -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

View File

@@ -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<Integer> 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<IconData> 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<IconData> processSubIdList(List<Integer> 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;
}
}
}

View File

@@ -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,
}

View File

@@ -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.

View File

@@ -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<Int>? = 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<out String>) {
pw.println("isCollecting=$isCollecting")
pw.println("Last values sent to icon controller: $lastValue")

View File

@@ -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" },
)

View File

@@ -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<ViewGroup>(R.id.mobile_group)
@@ -68,12 +69,12 @@ object MobileIconBinder {
// TODO(b/238425913): We should log this visibility state.
@StatusBarIconView.VisibleState
val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN)
val visibilityState: MutableStateFlow<Int> = MutableStateFlow(initialVisibilityState)
val iconTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor)
val decorTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor)
var isCollecting: Boolean = false
var isCollecting = false
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {

View File

@@ -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 } }
}
}
}
}

View File

@@ -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<ModernStatusBarMobileView>(R.id.mobile_combo)
iconView.initView(slot) {
MobileIconBinder.bind(iconView, viewModel, STATE_ICON, logger)
}
logger.logNewViewBinding(it, viewModel)
val textView = it.requireViewById<AutoMarqueeTextView>(R.id.mobile_carrier_text)
ShadeCarrierBinder.bind(textView, viewModel)
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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<Boolean> =
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,
)

View File

@@ -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<Int, MobileIconViewModel>()
@VisibleForTesting
val mobileIconInteractorSubIdCache = mutableMapOf<Int, MobileIconInteractor>()
val subscriptionIdsFlow: StateFlow<List<Int>> =
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<Int>) {
@VisibleForTesting
fun mobileIconInteractorForSub(subId: Int): MobileIconInteractor {
return mobileIconInteractorSubIdCache[subId]
?: interactor.createMobileConnectionInteractorForSubId(subId).also {
mobileIconInteractorSubIdCache[subId] = it
}
}
private fun invalidateCaches(subIds: List<Int>) {
val subIdsToRemove = mobileIconSubIdCache.keys.filter { !subIds.contains(it) }
subIdsToRemove.forEach { mobileIconSubIdCache.remove(it) }
mobileIconInteractorSubIdCache.keys
.filter { !subIds.contains(it) }
.forEach { subId -> mobileIconInteractorSubIdCache.remove(subId) }
}
}

View File

@@ -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)

View File

@@ -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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> subIds = Arrays.asList(0, 1, -1);
List<ShadeCarrierGroupController.IconData> 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<Integer> subIds = Arrays.asList(0, 4);
List<ShadeCarrierGroupController.IconData> processedSubs =
mShadeCarrierGroupController.processSubIdList(subIds);
assertThat(processedSubs).hasSize(1);
assertThat(processedSubs.get(0).subId).isNotEqualTo(4);
}
@Test
public void testOnlyInternalViewsHaveClickableListener() {
ArgumentCaptor<View.OnClickListener> 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;

View File

@@ -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 {