diff options
33 files changed, 2252 insertions, 105 deletions
diff --git a/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml new file mode 100644 index 000000000000..29832a081612 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_inner.xml @@ -0,0 +1,93 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +** +** Copyright 2022, 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. +*/ +--> + +<merge + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:systemui="http://schemas.android.com/apk/res-auto" > + + <com.android.keyguard.AlphaOptimizedLinearLayout + android:id="@+id/mobile_group" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center_vertical" + android:orientation="horizontal" > + + <FrameLayout + android:id="@+id/inout_container" + android:layout_height="17dp" + android:layout_width="wrap_content" + android:layout_gravity="center_vertical"> + <ImageView + android:id="@+id/mobile_in" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:src="@drawable/ic_activity_down" + android:visibility="gone" + android:paddingEnd="2dp" + /> + <ImageView + android:id="@+id/mobile_out" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:src="@drawable/ic_activity_up" + android:paddingEnd="2dp" + android:visibility="gone" + /> + </FrameLayout> + <ImageView + android:id="@+id/mobile_type" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:layout_gravity="center_vertical" + android:paddingStart="2.5dp" + android:paddingEnd="1dp" + android:visibility="gone" /> + <Space + android:id="@+id/mobile_roaming_space" + android:layout_height="match_parent" + android:layout_width="@dimen/roaming_icon_start_padding" + android:visibility="gone" + /> + <FrameLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical"> + <com.android.systemui.statusbar.AnimatedImageView + android:id="@+id/mobile_signal" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + systemui:hasOverlappingRendering="false" + /> + <ImageView + android:id="@+id/mobile_roaming" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/stat_sys_roaming" + android:contentDescription="@string/data_connection_roaming" + android:visibility="gone" /> + </FrameLayout> + <ImageView + android:id="@+id/mobile_roaming_large" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/stat_sys_roaming_large" + android:contentDescription="@string/data_connection_roaming" + android:visibility="gone" /> + </com.android.keyguard.AlphaOptimizedLinearLayout> +</merge> diff --git a/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_new.xml b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_new.xml new file mode 100644 index 000000000000..1b38fd2c4283 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/status_bar_mobile_signal_group_new.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +** +** Copyright 2022, 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.ModernStatusBarMobileView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/mobile_combo" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center_vertical" > + + <include layout="@layout/status_bar_mobile_signal_group_inner" /> + +</com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView> + diff --git a/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml b/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml index 10d49b38ae75..d6c63eb4feac 100644 --- a/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml +++ b/packages/SystemUI/res/layout/status_bar_mobile_signal_group.xml @@ -18,80 +18,12 @@ --> <com.android.systemui.statusbar.StatusBarMobileView xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:systemui="http://schemas.android.com/apk/res-auto" android:id="@+id/mobile_combo" android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center_vertical" > - <com.android.keyguard.AlphaOptimizedLinearLayout - android:id="@+id/mobile_group" - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:gravity="center_vertical" - android:orientation="horizontal" > + <include layout="@layout/status_bar_mobile_signal_group_inner" /> - <FrameLayout - android:id="@+id/inout_container" - android:layout_height="17dp" - android:layout_width="wrap_content" - android:layout_gravity="center_vertical"> - <ImageView - android:id="@+id/mobile_in" - android:layout_height="wrap_content" - android:layout_width="wrap_content" - android:src="@drawable/ic_activity_down" - android:visibility="gone" - android:paddingEnd="2dp" - /> - <ImageView - android:id="@+id/mobile_out" - android:layout_height="wrap_content" - android:layout_width="wrap_content" - android:src="@drawable/ic_activity_up" - android:paddingEnd="2dp" - android:visibility="gone" - /> - </FrameLayout> - <ImageView - android:id="@+id/mobile_type" - android:layout_height="wrap_content" - android:layout_width="wrap_content" - android:layout_gravity="center_vertical" - android:paddingStart="2.5dp" - android:paddingEnd="1dp" - android:visibility="gone" /> - <Space - android:id="@+id/mobile_roaming_space" - android:layout_height="match_parent" - android:layout_width="@dimen/roaming_icon_start_padding" - android:visibility="gone" - /> - <FrameLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical"> - <com.android.systemui.statusbar.AnimatedImageView - android:id="@+id/mobile_signal" - android:layout_height="wrap_content" - android:layout_width="wrap_content" - systemui:hasOverlappingRendering="false" - /> - <ImageView - android:id="@+id/mobile_roaming" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/stat_sys_roaming" - android:contentDescription="@string/data_connection_roaming" - android:visibility="gone" /> - </FrameLayout> - <ImageView - android:id="@+id/mobile_roaming_large" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/stat_sys_roaming_large" - android:contentDescription="@string/data_connection_roaming" - android:visibility="gone" /> - </com.android.keyguard.AlphaOptimizedLinearLayout> </com.android.systemui.statusbar.StatusBarMobileView> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarFrameLayout.kt index 4d53064d047d..ce730baeed0d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarWifiView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBarFrameLayout.kt @@ -20,14 +20,16 @@ import android.util.AttributeSet import android.widget.FrameLayout /** - * A temporary base class that's shared between our old status bar wifi view implementation - * ([StatusBarWifiView]) and our new status bar wifi view implementation - * ([ModernStatusBarWifiView]). + * A temporary base class that's shared between our old status bar connectivity view implementations + * ([StatusBarWifiView], [StatusBarMobileView]) and our new status bar implementations ( + * [ModernStatusBarWifiView], [ModernStatusBarMobileView]). * * Once our refactor is over, we should be able to delete this go-between class and the old view * class. */ -abstract class BaseStatusBarWifiView @JvmOverloads constructor( +abstract class BaseStatusBarFrameLayout +@JvmOverloads +constructor( context: Context, attrs: AttributeSet? = null, defStyleAttrs: Int = 0, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java index 48c6e273bbb4..fdad101ae0f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java @@ -29,7 +29,6 @@ import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; -import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; @@ -43,7 +42,10 @@ import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconStat import java.util.ArrayList; -public class StatusBarMobileView extends FrameLayout implements DarkReceiver, +/** + * View group for the mobile icon in the status bar + */ +public class StatusBarMobileView extends BaseStatusBarFrameLayout implements DarkReceiver, StatusIconDisplayable { private static final String TAG = "StatusBarMobileView"; @@ -101,11 +103,6 @@ public class StatusBarMobileView extends FrameLayout implements DarkReceiver, super(context, attrs, defStyleAttr); } - public StatusBarMobileView(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - @Override public void getDrawingRect(Rect outRect) { super.getDrawingRect(outRect); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java index f3e74d92fc8a..decc70d175b8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java @@ -40,7 +40,7 @@ import java.util.ArrayList; /** * Start small: StatusBarWifiView will be able to layout from a WifiIconState */ -public class StatusBarWifiView extends BaseStatusBarWifiView implements DarkReceiver { +public class StatusBarWifiView extends BaseStatusBarFrameLayout implements DarkReceiver { private static final String TAG = "StatusBarWifiView"; /// Used to show etc dots diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index d6d021ff2819..ece7ee0ec98a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone; import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_ICON; import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE; +import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE_NEW; import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI; import android.annotation.Nullable; @@ -38,7 +39,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.demomode.DemoModeCommandReceiver; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; -import com.android.systemui.statusbar.BaseStatusBarWifiView; +import com.android.systemui.statusbar.BaseStatusBarFrameLayout; import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.StatusBarMobileView; import com.android.systemui.statusbar.StatusBarWifiView; @@ -48,6 +49,10 @@ import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorI import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState; 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.ModernStatusBarMobileView; +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel; import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView; import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel; import com.android.systemui.util.Assert; @@ -84,6 +89,12 @@ public interface StatusBarIconController { void setMobileIcons(String slot, List<MobileIconState> states); /** + * This method completely replaces {@link #setMobileIcons} with the information from the new + * mobile data pipeline. Icons will automatically keep their state up to date, so we don't have + * to worry about funneling MobileIconState objects through anymore. + */ + void setNewMobileIconSubIds(List<Integer> subIds); + /** * Display the no calling & SMS icons. */ void setCallStrengthIcons(String slot, List<CallIndicatorIconState> states); @@ -141,12 +152,14 @@ public interface StatusBarIconController { StatusBarLocation location, StatusBarPipelineFlags statusBarPipelineFlags, WifiViewModel wifiViewModel, + MobileUiAdapter mobileUiAdapter, MobileContextProvider mobileContextProvider, DarkIconDispatcher darkIconDispatcher) { super(linearLayout, location, statusBarPipelineFlags, wifiViewModel, + mobileUiAdapter, mobileContextProvider); mIconHPadding = mContext.getResources().getDimensionPixelSize( R.dimen.status_bar_icon_padding); @@ -207,6 +220,7 @@ public interface StatusBarIconController { private final StatusBarPipelineFlags mStatusBarPipelineFlags; private final WifiViewModel mWifiViewModel; private final MobileContextProvider mMobileContextProvider; + private final MobileUiAdapter mMobileUiAdapter; private final DarkIconDispatcher mDarkIconDispatcher; @Inject @@ -214,10 +228,12 @@ public interface StatusBarIconController { StatusBarPipelineFlags statusBarPipelineFlags, WifiViewModel wifiViewModel, MobileContextProvider mobileContextProvider, + MobileUiAdapter mobileUiAdapter, DarkIconDispatcher darkIconDispatcher) { mStatusBarPipelineFlags = statusBarPipelineFlags; mWifiViewModel = wifiViewModel; mMobileContextProvider = mobileContextProvider; + mMobileUiAdapter = mobileUiAdapter; mDarkIconDispatcher = darkIconDispatcher; } @@ -227,6 +243,7 @@ public interface StatusBarIconController { location, mStatusBarPipelineFlags, mWifiViewModel, + mMobileUiAdapter, mMobileContextProvider, mDarkIconDispatcher); } @@ -244,11 +261,14 @@ public interface StatusBarIconController { StatusBarLocation location, StatusBarPipelineFlags statusBarPipelineFlags, WifiViewModel wifiViewModel, - MobileContextProvider mobileContextProvider) { + MobileUiAdapter mobileUiAdapter, + MobileContextProvider mobileContextProvider + ) { super(group, location, statusBarPipelineFlags, wifiViewModel, + mobileUiAdapter, mobileContextProvider); } @@ -284,14 +304,18 @@ public interface StatusBarIconController { private final StatusBarPipelineFlags mStatusBarPipelineFlags; private final WifiViewModel mWifiViewModel; private final MobileContextProvider mMobileContextProvider; + private final MobileUiAdapter mMobileUiAdapter; @Inject public Factory( StatusBarPipelineFlags statusBarPipelineFlags, WifiViewModel wifiViewModel, - MobileContextProvider mobileContextProvider) { + MobileUiAdapter mobileUiAdapter, + MobileContextProvider mobileContextProvider + ) { mStatusBarPipelineFlags = statusBarPipelineFlags; mWifiViewModel = wifiViewModel; + mMobileUiAdapter = mobileUiAdapter; mMobileContextProvider = mobileContextProvider; } @@ -301,6 +325,7 @@ public interface StatusBarIconController { location, mStatusBarPipelineFlags, mWifiViewModel, + mMobileUiAdapter, mMobileContextProvider); } } @@ -315,6 +340,8 @@ public interface StatusBarIconController { private final StatusBarPipelineFlags mStatusBarPipelineFlags; private final WifiViewModel mWifiViewModel; private final MobileContextProvider mMobileContextProvider; + private final MobileIconsViewModel mMobileIconsViewModel; + protected final Context mContext; protected final int mIconSize; // Whether or not these icons show up in dumpsys @@ -333,7 +360,9 @@ public interface StatusBarIconController { StatusBarLocation location, StatusBarPipelineFlags statusBarPipelineFlags, WifiViewModel wifiViewModel, - MobileContextProvider mobileContextProvider) { + MobileUiAdapter mobileUiAdapter, + MobileContextProvider mobileContextProvider + ) { mGroup = group; mLocation = location; mStatusBarPipelineFlags = statusBarPipelineFlags; @@ -342,6 +371,14 @@ public interface StatusBarIconController { mContext = group.getContext(); mIconSize = mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.status_bar_icon_size); + + if (statusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + // This starts the flow for the new pipeline, and will notify us of changes + mMobileIconsViewModel = mobileUiAdapter.createMobileIconsViewModel(); + MobileIconsBinder.bind(mGroup, mMobileIconsViewModel); + } else { + mMobileIconsViewModel = null; + } } public boolean isDemoable() { @@ -394,6 +431,9 @@ public interface StatusBarIconController { case TYPE_MOBILE: return addMobileIcon(index, slot, holder.getMobileState()); + + case TYPE_MOBILE_NEW: + return addNewMobileIcon(index, slot, holder.getTag()); } return null; @@ -410,7 +450,7 @@ public interface StatusBarIconController { @VisibleForTesting protected StatusIconDisplayable addWifiIcon(int index, String slot, WifiIconState state) { - final BaseStatusBarWifiView view; + final BaseStatusBarFrameLayout view; if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) { view = onCreateModernStatusBarWifiView(slot); // When [ModernStatusBarWifiView] is created, it will automatically apply the @@ -429,17 +469,47 @@ public interface StatusBarIconController { } @VisibleForTesting - protected StatusBarMobileView addMobileIcon(int index, String slot, MobileIconState state) { + protected StatusIconDisplayable addMobileIcon( + int index, + String slot, + MobileIconState state + ) { + if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + throw new IllegalStateException("Attempting to add a mobile icon while the new " + + "pipeline is enabled is not supported"); + } + // Use the `subId` field as a key to query for the correct context - StatusBarMobileView view = onCreateStatusBarMobileView(state.subId, slot); - view.applyMobileState(state); - mGroup.addView(view, index, onCreateLayoutParams()); + StatusBarMobileView mobileView = onCreateStatusBarMobileView(state.subId, slot); + mobileView.applyMobileState(state); + mGroup.addView(mobileView, index, onCreateLayoutParams()); if (mIsInDemoMode) { Context mobileContext = mMobileContextProvider .getMobileContextForSub(state.subId, mContext); mDemoStatusIcons.addMobileView(state, mobileContext); } + return mobileView; + } + + protected StatusIconDisplayable addNewMobileIcon( + int index, + String slot, + int subId + ) { + if (!mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + throw new IllegalStateException("Attempting to add a mobile icon using the new" + + "pipeline, but the enabled flag is false."); + } + + BaseStatusBarFrameLayout view = onCreateModernStatusBarMobileView(slot, subId); + mGroup.addView(view, index, onCreateLayoutParams()); + + if (mIsInDemoMode) { + // TODO (b/249790009): demo mode should be handled at the data layer in the + // new pipeline + } + return view; } @@ -464,6 +534,15 @@ public interface StatusBarIconController { return view; } + private ModernStatusBarMobileView onCreateModernStatusBarMobileView( + String slot, int subId) { + return ModernStatusBarMobileView + .constructAndBind( + mContext, + slot, + mMobileIconsViewModel.viewModelForSub(subId)); + } + protected LinearLayout.LayoutParams onCreateLayoutParams() { return new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, mIconSize); } @@ -519,6 +598,10 @@ public interface StatusBarIconController { return; case TYPE_MOBILE: onSetMobileIcon(viewIndex, holder.getMobileState()); + return; + case TYPE_MOBILE_NEW: + // Nothing, the icon updates itself now + return; default: break; } @@ -542,9 +625,13 @@ public interface StatusBarIconController { } public void onSetMobileIcon(int viewIndex, MobileIconState state) { - StatusBarMobileView view = (StatusBarMobileView) mGroup.getChildAt(viewIndex); - if (view != null) { - view.applyMobileState(state); + View view = mGroup.getChildAt(viewIndex); + if (view instanceof StatusBarMobileView) { + ((StatusBarMobileView) view).applyMobileState(state); + } else { + // ModernStatusBarMobileView automatically updates via the ViewModel + throw new IllegalStateException("Cannot update ModernStatusBarMobileView outside of" + + "the new pipeline"); } if (mIsInDemoMode) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java index 7c31366ba4f0..e106b9e327ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java @@ -40,6 +40,7 @@ import com.android.systemui.statusbar.StatusIconDisplayable; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState; +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; import com.android.systemui.tuner.TunerService; @@ -66,8 +67,8 @@ public class StatusBarIconControllerImpl implements Tunable, private final StatusBarIconList mStatusBarIconList; private final ArrayList<IconManager> mIconGroups = new ArrayList<>(); private final ArraySet<String> mIconHideList = new ArraySet<>(); - - private Context mContext; + private final StatusBarPipelineFlags mStatusBarPipelineFlags; + private final Context mContext; /** */ @Inject @@ -78,9 +79,12 @@ public class StatusBarIconControllerImpl implements Tunable, ConfigurationController configurationController, TunerService tunerService, DumpManager dumpManager, - StatusBarIconList statusBarIconList) { + StatusBarIconList statusBarIconList, + StatusBarPipelineFlags statusBarPipelineFlags + ) { mStatusBarIconList = statusBarIconList; mContext = context; + mStatusBarPipelineFlags = statusBarPipelineFlags; configurationController.addCallback(this); commandQueue.addCallback(this); @@ -220,6 +224,11 @@ public class StatusBarIconControllerImpl implements Tunable, */ @Override public void setMobileIcons(String slot, List<MobileIconState> iconStates) { + if (mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + Log.d(TAG, "ignoring old pipeline callbacks, because the new " + + "pipeline frontend is enabled"); + return; + } Slot mobileSlot = mStatusBarIconList.getSlot(slot); // Reverse the sort order to show icons with left to right([Slot1][Slot2]..). @@ -227,7 +236,6 @@ public class StatusBarIconControllerImpl implements Tunable, Collections.reverse(iconStates); for (MobileIconState state : iconStates) { - StatusBarIconHolder holder = mobileSlot.getHolderForTag(state.subId); if (holder == null) { holder = StatusBarIconHolder.fromMobileIconState(state); @@ -239,6 +247,28 @@ public class StatusBarIconControllerImpl implements Tunable, } } + @Override + public void setNewMobileIconSubIds(List<Integer> subIds) { + if (!mStatusBarPipelineFlags.isNewPipelineFrontendEnabled()) { + Log.d(TAG, "ignoring new pipeline callback, " + + "since the frontend is disabled"); + return; + } + Slot mobileSlot = mStatusBarIconList.getSlot("mobile"); + + Collections.reverse(subIds); + + for (Integer subId : subIds) { + StatusBarIconHolder holder = mobileSlot.getHolderForTag(subId); + if (holder == null) { + holder = StatusBarIconHolder.fromSubIdForModernMobileIcon(subId); + setIcon("mobile", holder); + } else { + // Don't have to do anything in the new world + } + } + } + /** * Accept a list of CallIndicatorIconStates, and show the call strength icons. * @param slot statusbar slot for the call strength icons @@ -384,8 +414,6 @@ public class StatusBarIconControllerImpl implements Tunable, } } - - private void handleSet(String slotName, StatusBarIconHolder holder) { int viewIndex = mStatusBarIconList.getViewIndex(slotName, holder.getTag()); mIconGroups.forEach(l -> l.onSetIconHolder(viewIndex, holder)); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java index af342dd31a76..68a203e30f98 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone; +import android.annotation.IntDef; import android.annotation.Nullable; import android.content.Context; import android.graphics.drawable.Icon; @@ -25,6 +26,10 @@ import com.android.internal.statusbar.StatusBarIcon; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.CallIndicatorIconState; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState; +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Wraps {@link com.android.internal.statusbar.StatusBarIcon} so we can still have a uniform list @@ -33,15 +38,35 @@ public class StatusBarIconHolder { public static final int TYPE_ICON = 0; public static final int TYPE_WIFI = 1; public static final int TYPE_MOBILE = 2; + /** + * TODO (b/249790733): address this once the new pipeline is in place + * This type exists so that the new pipeline (see {@link MobileIconViewModel}) can be used + * to inform the old view system about changes to the data set (the list of mobile icons). The + * design of the new pipeline should allow for removal of this icon holder type, and obsolete + * the need for this entire class. + * + * @deprecated This field only exists so the new status bar pipeline can interface with the + * view holder system. + */ + @Deprecated + public static final int TYPE_MOBILE_NEW = 3; + + @IntDef({ + TYPE_ICON, + TYPE_WIFI, + TYPE_MOBILE, + TYPE_MOBILE_NEW + }) + @Retention(RetentionPolicy.SOURCE) + @interface IconType {} private StatusBarIcon mIcon; private WifiIconState mWifiState; private MobileIconState mMobileState; - private int mType = TYPE_ICON; + private @IconType int mType = TYPE_ICON; private int mTag = 0; private StatusBarIconHolder() { - } public static StatusBarIconHolder fromIcon(StatusBarIcon icon) { @@ -80,6 +105,18 @@ public class StatusBarIconHolder { } /** + * ONLY for use with the new connectivity pipeline, where we only need a subscriptionID to + * determine icon ordering and building the correct view model + */ + public static StatusBarIconHolder fromSubIdForModernMobileIcon(int subId) { + StatusBarIconHolder holder = new StatusBarIconHolder(); + holder.mType = TYPE_MOBILE_NEW; + holder.mTag = subId; + + return holder; + } + + /** * Creates a new StatusBarIconHolder from a CallIndicatorIconState. */ public static StatusBarIconHolder fromCallIndicatorState( @@ -95,7 +132,7 @@ public class StatusBarIconHolder { return holder; } - public int getType() { + public @IconType int getType() { return mType; } @@ -134,8 +171,12 @@ public class StatusBarIconHolder { return mWifiState.visible; case TYPE_MOBILE: return mMobileState.visible; + case TYPE_MOBILE_NEW: + //TODO (b/249790733), the new pipeline can control visibility via the ViewModel + return true; - default: return true; + default: + return true; } } @@ -156,6 +197,10 @@ public class StatusBarIconHolder { case TYPE_MOBILE: mMobileState.visible = visible; break; + + case TYPE_MOBILE_NEW: + //TODO (b/249790733), the new pipeline can control visibility via the ViewModel + break; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 9a7c3fae780c..06d554232565 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -16,6 +16,10 @@ package com.android.systemui.statusbar.pipeline.dagger +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepositoryImpl +import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepositoryImpl import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository @@ -30,4 +34,12 @@ abstract class StatusBarPipelineModule { @Binds abstract fun wifiRepository(impl: WifiRepositoryImpl): WifiRepository + + @Binds + abstract fun mobileSubscriptionRepository( + impl: MobileSubscriptionRepositoryImpl + ): MobileSubscriptionRepository + + @Binds + abstract fun userSetupRepository(impl: UserSetupRepositoryImpl): UserSetupRepository } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt new file mode 100644 index 000000000000..46ccf32cc7f9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileSubscriptionModel.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 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.data.model + +import android.annotation.IntRange +import android.telephony.Annotation.DataActivityType +import android.telephony.CellSignalStrength +import android.telephony.TelephonyCallback.CarrierNetworkListener +import android.telephony.TelephonyCallback.DataActivityListener +import android.telephony.TelephonyCallback.DataConnectionStateListener +import android.telephony.TelephonyCallback.DisplayInfoListener +import android.telephony.TelephonyCallback.ServiceStateListener +import android.telephony.TelephonyCallback.SignalStrengthsListener +import android.telephony.TelephonyDisplayInfo +import android.telephony.TelephonyManager + +/** + * Data class containing all of the relevant information for a particular line of service, known as + * a Subscription in the telephony world. These models are the result of a single telephony listener + * which has many callbacks which each modify some particular field on this object. + * + * The design goal here is to de-normalize fields from the system into our model fields below. So + * any new field that needs to be tracked should be copied into this data class rather than + * threading complex system objects through the pipeline. + */ +data class MobileSubscriptionModel( + /** From [ServiceStateListener.onServiceStateChanged] */ + val isEmergencyOnly: Boolean = false, + + /** From [SignalStrengthsListener.onSignalStrengthsChanged] */ + val isGsm: Boolean = false, + @IntRange(from = 0, to = 4) + val cdmaLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN, + @IntRange(from = 0, to = 4) + val primaryLevel: Int = CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN, + + /** Comes directly from [DataConnectionStateListener.onDataConnectionStateChanged] */ + val dataConnectionState: Int? = null, + + /** From [DataActivityListener.onDataActivity]. See [TelephonyManager] for the values */ + @DataActivityType val dataActivityDirection: Int? = null, + + /** From [CarrierNetworkListener.onCarrierNetworkChange] */ + val carrierNetworkChangeActive: Boolean? = null, + + /** From [DisplayInfoListener.onDisplayInfoChanged] */ + val displayInfo: TelephonyDisplayInfo? = null +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt new file mode 100644 index 000000000000..36de2a254160 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepository.kt @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2022 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.data.repository + +import android.telephony.CellSignalStrength +import android.telephony.CellSignalStrengthCdma +import android.telephony.ServiceState +import android.telephony.SignalStrength +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyCallback +import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener +import android.telephony.TelephonyCallback.CarrierNetworkListener +import android.telephony.TelephonyCallback.DataActivityListener +import android.telephony.TelephonyCallback.DataConnectionStateListener +import android.telephony.TelephonyCallback.DisplayInfoListener +import android.telephony.TelephonyCallback.ServiceStateListener +import android.telephony.TelephonyCallback.SignalStrengthsListener +import android.telephony.TelephonyDisplayInfo +import android.telephony.TelephonyManager +import androidx.annotation.VisibleForTesting +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext + +/** + * Repo for monitoring the complete active subscription info list, to be consumed and filtered based + * on various policy + */ +interface MobileSubscriptionRepository { + /** Observable list of current mobile subscriptions */ + val subscriptionsFlow: Flow<List<SubscriptionInfo>> + + /** Observable for the subscriptionId of the current mobile data connection */ + val activeMobileDataSubscriptionId: Flow<Int> + + /** Get or create an observable for the given subscription ID */ + fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> +} + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class MobileSubscriptionRepositoryImpl +@Inject +constructor( + private val subscriptionManager: SubscriptionManager, + private val telephonyManager: TelephonyManager, + @Background private val bgDispatcher: CoroutineDispatcher, + @Application private val scope: CoroutineScope, +) : MobileSubscriptionRepository { + private val subIdFlowCache: MutableMap<Int, StateFlow<MobileSubscriptionModel>> = mutableMapOf() + + /** + * State flow that emits the set of mobile data subscriptions, each represented by its own + * [SubscriptionInfo]. We probably only need the [SubscriptionInfo.getSubscriptionId] of each + * info object, but for now we keep track of the infos themselves. + */ + override val subscriptionsFlow: StateFlow<List<SubscriptionInfo>> = + conflatedCallbackFlow { + val callback = + object : SubscriptionManager.OnSubscriptionsChangedListener() { + override fun onSubscriptionsChanged() { + trySend(Unit) + } + } + + subscriptionManager.addOnSubscriptionsChangedListener( + bgDispatcher.asExecutor(), + callback, + ) + + awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } + } + .mapLatest { fetchSubscriptionsList() } + .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf()) + + /** StateFlow that keeps track of the current active mobile data subscription */ + override val activeMobileDataSubscriptionId: StateFlow<Int> = + conflatedCallbackFlow { + val callback = + object : TelephonyCallback(), ActiveDataSubscriptionIdListener { + override fun onActiveDataSubscriptionIdChanged(subId: Int) { + trySend(subId) + } + } + + telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) + awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } + } + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + SubscriptionManager.INVALID_SUBSCRIPTION_ID + ) + + /** + * Each mobile subscription needs its own flow, which comes from registering listeners on the + * system. Use this method to create those flows and cache them for reuse + */ + override fun getFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> { + return subIdFlowCache[subId] + ?: createFlowForSubId(subId).also { subIdFlowCache[subId] = it } + } + + @VisibleForTesting fun getSubIdFlowCache() = subIdFlowCache + + private fun createFlowForSubId(subId: Int): StateFlow<MobileSubscriptionModel> = run { + var state = MobileSubscriptionModel() + conflatedCallbackFlow { + val phony = telephonyManager.createForSubscriptionId(subId) + // TODO (b/240569788): log all of these into the connectivity logger + val callback = + object : + TelephonyCallback(), + ServiceStateListener, + SignalStrengthsListener, + DataConnectionStateListener, + DataActivityListener, + CarrierNetworkListener, + DisplayInfoListener { + override fun onServiceStateChanged(serviceState: ServiceState) { + state = state.copy(isEmergencyOnly = serviceState.isEmergencyOnly) + trySend(state) + } + override fun onSignalStrengthsChanged(signalStrength: SignalStrength) { + val cdmaLevel = + signalStrength + .getCellSignalStrengths(CellSignalStrengthCdma::class.java) + .let { strengths -> + if (!strengths.isEmpty()) { + strengths[0].level + } else { + CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN + } + } + + val primaryLevel = signalStrength.level + + state = + state.copy( + cdmaLevel = cdmaLevel, + primaryLevel = primaryLevel, + isGsm = signalStrength.isGsm, + ) + trySend(state) + } + override fun onDataConnectionStateChanged( + dataState: Int, + networkType: Int + ) { + state = state.copy(dataConnectionState = dataState) + trySend(state) + } + override fun onDataActivity(direction: Int) { + state = state.copy(dataActivityDirection = direction) + trySend(state) + } + override fun onCarrierNetworkChange(active: Boolean) { + state = state.copy(carrierNetworkChangeActive = active) + trySend(state) + } + override fun onDisplayInfoChanged( + telephonyDisplayInfo: TelephonyDisplayInfo + ) { + state = state.copy(displayInfo = telephonyDisplayInfo) + trySend(state) + } + } + phony.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) + awaitClose { + phony.unregisterTelephonyCallback(callback) + // Release the cached flow + subIdFlowCache.remove(subId) + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), state) + } + + private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> = + withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt new file mode 100644 index 000000000000..77de849691db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 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.data.repository + +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext + +/** + * Repository to observe the state of [DeviceProvisionedController.isUserSetup]. This information + * can change some policy related to display + */ +interface UserSetupRepository { + /** Observable tracking [DeviceProvisionedController.isUserSetup] */ + val isUserSetupFlow: Flow<Boolean> +} + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class UserSetupRepositoryImpl +@Inject +constructor( + private val deviceProvisionedController: DeviceProvisionedController, + @Background private val bgDispatcher: CoroutineDispatcher, + @Application scope: CoroutineScope, +) : UserSetupRepository { + /** State flow that tracks [DeviceProvisionedController.isUserSetup] */ + override val isUserSetupFlow: StateFlow<Boolean> = + conflatedCallbackFlow { + val callback = + object : DeviceProvisionedController.DeviceProvisionedListener { + override fun onUserSetupChanged() { + trySend(Unit) + } + } + + deviceProvisionedController.addCallback(callback) + + awaitClose { deviceProvisionedController.removeCallback(callback) } + } + .onStart { emit(Unit) } + .mapLatest { fetchUserSetupState() } + .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) + + private suspend fun fetchUserSetupState(): Boolean = + withContext(bgDispatcher) { deviceProvisionedController.isCurrentUserSetup } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt new file mode 100644 index 000000000000..40fe0f3e8fe0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 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.domain.interactor + +import android.telephony.CarrierConfigManager +import com.android.settingslib.SignalIcon +import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.systemui.util.CarrierConfigTracker +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +interface MobileIconInteractor { + /** Identifier for RAT type indicator */ + val iconGroup: Flow<SignalIcon.MobileIconGroup> + /** True if this line of service is emergency-only */ + val isEmergencyOnly: Flow<Boolean> + /** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */ + val level: Flow<Int> + /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */ + val numberOfLevels: Flow<Int> + /** True when we want to draw an icon that makes room for the exclamation mark */ + val cutOut: Flow<Boolean> +} + +/** Interactor for a single mobile connection. This connection _should_ have one subscription ID */ +class MobileIconInteractorImpl( + mobileStatusInfo: Flow<MobileSubscriptionModel>, +) : MobileIconInteractor { + override val iconGroup: Flow<SignalIcon.MobileIconGroup> = flowOf(TelephonyIcons.THREE_G) + override val isEmergencyOnly: Flow<Boolean> = mobileStatusInfo.map { it.isEmergencyOnly } + + override val level: Flow<Int> = + mobileStatusInfo.map { mobileModel -> + // TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi] + if (mobileModel.isGsm) { + mobileModel.primaryLevel + } else { + mobileModel.cdmaLevel + } + } + + /** + * This will become variable based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL] + * once it's wired up inside of [CarrierConfigTracker] + */ + override val numberOfLevels: Flow<Int> = flowOf(4) + + /** Whether or not to draw the mobile triangle as "cut out", i.e., with the exclamation mark */ + // TODO: find a better name for this? + override val cutOut: Flow<Boolean> = flowOf(false) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt new file mode 100644 index 000000000000..8e67e19f3e35 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2022 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.domain.interactor + +import android.telephony.CarrierConfigManager +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileSubscriptionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository +import com.android.systemui.util.CarrierConfigTracker +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +/** + * Business layer logic for mobile subscription icons + * + * Mobile indicators represent the UI for the (potentially filtered) list of [SubscriptionInfo]s + * that the system knows about. They obey policy that depends on OEM, carrier, and locale configs + */ +@SysUISingleton +class MobileIconsInteractor +@Inject +constructor( + private val mobileSubscriptionRepo: MobileSubscriptionRepository, + private val carrierConfigTracker: CarrierConfigTracker, + userSetupRepo: UserSetupRepository, +) { + private val activeMobileDataSubscriptionId = + mobileSubscriptionRepo.activeMobileDataSubscriptionId + + private val unfilteredSubscriptions: Flow<List<SubscriptionInfo>> = + mobileSubscriptionRepo.subscriptionsFlow + + /** + * Generally, SystemUI wants to show iconography for each subscription that is listed by + * [SubscriptionManager]. However, in the case of opportunistic subscriptions, we want to only + * show a single representation of the pair of subscriptions. The docs define opportunistic as: + * + * "A subscription is opportunistic (if) the network it connects to has limited coverage" + * https://developer.android.com/reference/android/telephony/SubscriptionManager#setOpportunistic(boolean,%20int) + * + * In the case of opportunistic networks (typically CBRS), we will filter out one of the + * subscriptions based on + * [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN], + * and by checking which subscription is opportunistic, or which one is active. + */ + val filteredSubscriptions: Flow<List<SubscriptionInfo>> = + combine(unfilteredSubscriptions, activeMobileDataSubscriptionId) { unfilteredSubs, activeId + -> + // Based on the old logic, + if (unfilteredSubs.size != 2) { + return@combine unfilteredSubs + } + + val info1 = unfilteredSubs[0] + val info2 = unfilteredSubs[1] + // If both subscriptions are primary, show both + if (!info1.isOpportunistic && !info2.isOpportunistic) { + return@combine unfilteredSubs + } + + // NOTE: at this point, we are now returning a single SubscriptionInfo + + // If carrier required, always show the icon of the primary subscription. + // Otherwise, show whichever subscription is currently active for internet. + if (carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) { + // return the non-opportunistic info + return@combine if (info1.isOpportunistic) listOf(info2) else listOf(info1) + } else { + return@combine if (info1.subscriptionId == activeId) { + listOf(info1) + } else { + listOf(info2) + } + } + } + + val isUserSetup: Flow<Boolean> = userSetupRepo.isUserSetupFlow + + /** Vends out new [MobileIconInteractor] for a particular subId */ + fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = + MobileIconInteractorImpl(mobileSubscriptionFlowForSubId(subId)) + + /** + * Create a new flow for a given subscription ID, which usually maps 1:1 with mobile connections + */ + private fun mobileSubscriptionFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> = + mobileSubscriptionRepo.getFlowForSubId(subId) +} 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 new file mode 100644 index 000000000000..380017cd3418 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 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.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.phone.StatusBarIconController +import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn + +/** + * This class is intended to provide a context to collect on the + * [MobileIconsInteractor.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. + */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class MobileUiAdapter +@Inject +constructor( + interactor: MobileIconsInteractor, + private val iconController: StatusBarIconController, + private val iconsViewModelFactory: MobileIconsViewModel.Factory, + @Application scope: CoroutineScope, +) { + private val mobileSubIds: Flow<List<Int>> = + interactor.filteredSubscriptions.mapLatest { infos -> + infos.map { subscriptionInfo -> subscriptionInfo.subscriptionId } + } + + /** + * We expose the list of tracked subscriptions as a flow of a list of ints, where each int is + * the subscriptionId of the relevant subscriptions. These act as a key into the layouts which + * house the mobile infos. + * + * NOTE: this should go away as the view presenter learns more about this data pipeline + */ + private val mobileSubIdsState: StateFlow<List<Int>> = + mobileSubIds + .onEach { + // Notify the icon controller here so that it knows to add icons + iconController.setNewMobileIconSubIds(it) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) + + /** + * Create a MobileIconsViewModel for a given [IconManager], and bind it to to the manager's + * lifecycle. This will start collecting on [mobileSubIdsState] and link our new pipeline with + * the old view system. + */ + fun createMobileIconsViewModel(): MobileIconsViewModel = + iconsViewModelFactory.create(mobileSubIdsState) +} 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 new file mode 100644 index 000000000000..1405b050234b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2022 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.view.ViewGroup +import android.widget.ImageView +import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.settingslib.graph.SignalDrawable +import com.android.systemui.R +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch + +object MobileIconBinder { + /** Binds the view to the view-model, continuing to update the former based on the latter */ + @JvmStatic + fun bind( + view: ViewGroup, + viewModel: MobileIconViewModel, + ) { + val iconView = view.requireViewById<ImageView>(R.id.mobile_signal) + val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) } + + view.isVisible = true + iconView.isVisible = true + + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + // Set the icon for the triangle + launch { + viewModel.iconId.distinctUntilChanged().collect { iconId -> + mobileDrawable.level = iconId + } + } + + // Set the tint + launch { + viewModel.tint.collect { tint -> + iconView.imageTintList = ColorStateList.valueOf(tint) + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconsBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconsBinder.kt new file mode 100644 index 000000000000..e7d5ee264efe --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconsBinder.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2022 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.view.View +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch + +object MobileIconsBinder { + /** + * Start this ViewModel collecting on the list of mobile subscriptions in the scope of [view] + * which is passed in and managed by [IconManager]. Once the subscription list flow starts + * collecting, [MobileUiAdapter] will send updates to the icon manager. + */ + @JvmStatic + fun bind(view: View, viewModel: MobileIconsViewModel) { + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.subscriptionIdsFlow.collect { + // TODO(b/249790733): This is an empty collect, because [MobileUiAdapter] + // sets up a side-effect in this flow to trigger the methods on + // [StatusBarIconController] which allows for this pipeline to be a data + // source for the mobile icons. + } + } + } + } + } +} 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 new file mode 100644 index 000000000000..ec4fa9ca8128 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2022 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.graphics.Rect +import android.util.AttributeSet +import android.view.LayoutInflater +import com.android.systemui.R +import com.android.systemui.statusbar.BaseStatusBarFrameLayout +import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON +import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel +import java.util.ArrayList + +class ModernStatusBarMobileView( + context: Context, + attrs: AttributeSet?, +) : BaseStatusBarFrameLayout(context, attrs) { + + private lateinit var slot: String + override fun getSlot() = slot + + override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) { + // TODO + } + + override fun setStaticDrawableColor(color: Int) { + // TODO + } + + override fun setDecorColor(color: Int) { + // TODO + } + + override fun setVisibleState(state: Int, animate: Boolean) { + // TODO + } + + override fun getVisibleState(): Int { + return STATE_ICON + } + + override fun isIconVisible(): Boolean { + return true + } + + companion object { + + /** + * Inflates a new instance of [ModernStatusBarMobileView], binds it to [viewModel], and + * returns it. + */ + @JvmStatic + fun constructAndBind( + context: Context, + slot: String, + viewModel: MobileIconViewModel, + ): ModernStatusBarMobileView { + return (LayoutInflater.from(context) + .inflate(R.layout.status_bar_mobile_signal_group_new, null) + as ModernStatusBarMobileView) + .also { + it.slot = slot + MobileIconBinder.bind(it, viewModel) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt new file mode 100644 index 000000000000..cfabeba8432c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2022 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.viewmodel + +import android.graphics.Color +import com.android.settingslib.graph.SignalDrawable +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.shared.ConnectivityPipelineLogger +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf + +/** + * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over + * a single line of service via [MobileIconInteractor] and update the UI based on that + * subscription's information. + * + * There will be exactly one [MobileIconViewModel] per filtered subscription offered from + * [MobileIconsInteractor.filteredSubscriptions] + * + * TODO: figure out where carrier merged and VCN models go (probably here?) + */ +class MobileIconViewModel +constructor( + val subscriptionId: Int, + iconInteractor: MobileIconInteractor, + logger: ConnectivityPipelineLogger, +) { + /** An int consumable by [SignalDrawable] for display */ + var iconId: Flow<Int> = + combine(iconInteractor.level, iconInteractor.numberOfLevels, iconInteractor.cutOut) { + level, + numberOfLevels, + cutOut -> + SignalDrawable.getState(level, numberOfLevels, cutOut) + } + .distinctUntilChanged() + .logOutputChange(logger, "iconId($subscriptionId)") + + var tint: Flow<Int> = flowOf(Color.CYAN) +} 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 new file mode 100644 index 000000000000..24c1db995d50 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 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. + */ + +@file:OptIn(InternalCoroutinesApi::class) + +package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import javax.inject.Inject +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.flow.Flow + +/** + * 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 + * [ModernStatusBarMobileView] + */ +class MobileIconsViewModel +@Inject +constructor( + val subscriptionIdsFlow: Flow<List<Int>>, + private val interactor: MobileIconsInteractor, + private val logger: ConnectivityPipelineLogger, +) { + /** TODO: do we need to cache these? */ + fun viewModelForSub(subId: Int): MobileIconViewModel = + MobileIconViewModel( + subId, + interactor.createMobileConnectionInteractorForSubId(subId), + logger + ) + + class Factory + @Inject + constructor( + private val interactor: MobileIconsInteractor, + private val logger: ConnectivityPipelineLogger, + ) { + fun create(subscriptionIdsFlow: Flow<List<Int>>): MobileIconsViewModel { + return MobileIconsViewModel( + subscriptionIdsFlow, + interactor, + logger, + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt index 6c616ac7c3b8..0cd9bd7d97b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt @@ -22,7 +22,7 @@ import android.util.AttributeSet import android.view.Gravity import android.view.LayoutInflater import com.android.systemui.R -import com.android.systemui.statusbar.BaseStatusBarWifiView +import com.android.systemui.statusbar.BaseStatusBarFrameLayout import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN @@ -37,7 +37,7 @@ import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel class ModernStatusBarWifiView( context: Context, attrs: AttributeSet? -) : BaseStatusBarWifiView(context, attrs) { +) : BaseStatusBarFrameLayout(context, attrs) { private lateinit var slot: String private lateinit var binding: WifiViewBinder.Binding diff --git a/packages/SystemUI/src/com/android/systemui/util/CarrierConfigTracker.java b/packages/SystemUI/src/com/android/systemui/util/CarrierConfigTracker.java index 5f7d74542fff..a925e384d3be 100644 --- a/packages/SystemUI/src/com/android/systemui/util/CarrierConfigTracker.java +++ b/packages/SystemUI/src/com/android/systemui/util/CarrierConfigTracker.java @@ -67,6 +67,8 @@ public class CarrierConfigTracker private boolean mDefaultCarrierProvisionsWifiMergedNetworks; private boolean mDefaultShowOperatorNameConfigLoaded; private boolean mDefaultShowOperatorNameConfig; + private boolean mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfigLoaded; + private boolean mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfig; @Inject public CarrierConfigTracker( @@ -207,6 +209,22 @@ public class CarrierConfigTracker } /** + * Returns KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN value for + * the default carrier config. + */ + public boolean getAlwaysShowPrimarySignalBarInOpportunisticNetworkDefault() { + if (!mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfigLoaded) { + mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfig = CarrierConfigManager + .getDefaultConfig().getBoolean(CarrierConfigManager + .KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN + ); + mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfigLoaded = true; + } + + return mDefaultAlwaysShowPrimarySignalBarInOpportunisticNetworkConfig; + } + + /** * Returns the KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL value for the given subId, or the * default value if no override exists * diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java index 34399b80c9f7..9c56c2670c63 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java @@ -44,6 +44,7 @@ import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.MobileIconState; import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState; import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; +import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter; import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel; import com.android.systemui.utils.leaks.LeakCheckedTest; @@ -80,6 +81,7 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { StatusBarLocation.HOME, mock(StatusBarPipelineFlags.class), mock(WifiViewModel.class), + mock(MobileUiAdapter.class), mMobileContextProvider, mock(DarkIconDispatcher.class)); testCallOnAdd_forManager(manager); @@ -123,12 +125,14 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { StatusBarLocation location, StatusBarPipelineFlags statusBarPipelineFlags, WifiViewModel wifiViewModel, + MobileUiAdapter mobileUiAdapter, MobileContextProvider contextProvider, DarkIconDispatcher darkIconDispatcher) { super(group, location, statusBarPipelineFlags, wifiViewModel, + mobileUiAdapter, contextProvider, darkIconDispatcher); } @@ -169,6 +173,7 @@ public class StatusBarIconControllerTest extends LeakCheckedTest { StatusBarLocation.HOME, mock(StatusBarPipelineFlags.class), mock(WifiViewModel.class), + mock(MobileUiAdapter.class), contextProvider); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt new file mode 100644 index 000000000000..0d1526883023 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileSubscriptionRepository.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2022 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.data.repository + +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeMobileSubscriptionRepository : MobileSubscriptionRepository { + private val _subscriptionsFlow = MutableStateFlow<List<SubscriptionInfo>>(listOf()) + override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _subscriptionsFlow + + private val _activeMobileDataSubscriptionId = + MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID) + override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId + + private val subIdFlows = mutableMapOf<Int, MutableStateFlow<MobileSubscriptionModel>>() + override fun getFlowForSubId(subId: Int): Flow<MobileSubscriptionModel> { + return subIdFlows[subId] + ?: MutableStateFlow(MobileSubscriptionModel()).also { subIdFlows[subId] = it } + } + + fun setSubscriptions(subs: List<SubscriptionInfo>) { + _subscriptionsFlow.value = subs + } + + fun setActiveMobileDataSubscriptionId(subId: Int) { + _activeMobileDataSubscriptionId.value = subId + } + + fun setMobileSubscriptionModel(model: MobileSubscriptionModel, subId: Int) { + val subscription = subIdFlows[subId] ?: throw Exception("no flow exists for this subId yet") + subscription.value = model + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt new file mode 100644 index 000000000000..6c495c5c705a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 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.data.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +/** Defaults to `true` */ +class FakeUserSetupRepository : UserSetupRepository { + private val _isUserSetup: MutableStateFlow<Boolean> = MutableStateFlow(true) + override val isUserSetupFlow: Flow<Boolean> = _isUserSetup + + fun setUserSetup(setup: Boolean) { + _isUserSetup.value = setup + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt new file mode 100644 index 000000000000..316b795ac949 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileSubscriptionRepositoryTest.kt @@ -0,0 +1,360 @@ +/* + * Copyright (C) 2022 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.data.repository + +import android.telephony.CellSignalStrengthCdma +import android.telephony.ServiceState +import android.telephony.SignalStrength +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyCallback +import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener +import android.telephony.TelephonyCallback.CarrierNetworkListener +import android.telephony.TelephonyCallback.DataActivityListener +import android.telephony.TelephonyCallback.DataConnectionStateListener +import android.telephony.TelephonyCallback.DisplayInfoListener +import android.telephony.TelephonyCallback.ServiceStateListener +import android.telephony.TelephonyCallback.SignalStrengthsListener +import android.telephony.TelephonyDisplayInfo +import android.telephony.TelephonyManager +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class MobileSubscriptionRepositoryTest : SysuiTestCase() { + private lateinit var underTest: MobileSubscriptionRepositoryImpl + + @Mock private lateinit var subscriptionManager: SubscriptionManager + @Mock private lateinit var telephonyManager: TelephonyManager + private val scope = CoroutineScope(IMMEDIATE) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + underTest = + MobileSubscriptionRepositoryImpl( + subscriptionManager, + telephonyManager, + IMMEDIATE, + scope, + ) + } + + @After + fun tearDown() { + scope.cancel() + } + + @Test + fun testSubscriptions_initiallyEmpty() = + runBlocking(IMMEDIATE) { + assertThat(underTest.subscriptionsFlow.value).isEqualTo(listOf<SubscriptionInfo>()) + } + + @Test + fun testSubscriptions_listUpdates() = + runBlocking(IMMEDIATE) { + var latest: List<SubscriptionInfo>? = null + + val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) + + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2)) + + job.cancel() + } + + @Test + fun testSubscriptions_removingSub_updatesList() = + runBlocking(IMMEDIATE) { + var latest: List<SubscriptionInfo>? = null + + val job = underTest.subscriptionsFlow.onEach { latest = it }.launchIn(this) + + // WHEN 2 networks show up + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_1, SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + // WHEN one network is removed + whenever(subscriptionManager.completeActiveSubscriptionInfoList) + .thenReturn(listOf(SUB_2)) + getSubscriptionCallback().onSubscriptionsChanged() + + // THEN the subscriptions list represents the newest change + assertThat(latest).isEqualTo(listOf(SUB_2)) + + job.cancel() + } + + @Test + fun testActiveDataSubscriptionId_initialValueIsInvalidId() = + runBlocking(IMMEDIATE) { + assertThat(underTest.activeMobileDataSubscriptionId.value) + .isEqualTo(SubscriptionManager.INVALID_SUBSCRIPTION_ID) + } + + @Test + fun testActiveDataSubscriptionId_updates() = + runBlocking(IMMEDIATE) { + var active: Int? = null + + val job = underTest.activeMobileDataSubscriptionId.onEach { active = it }.launchIn(this) + + getActiveDataSubscriptionCallback().onActiveDataSubscriptionIdChanged(SUB_2_ID) + + assertThat(active).isEqualTo(SUB_2_ID) + + job.cancel() + } + + @Test + fun testFlowForSubId_default() = + runBlocking(IMMEDIATE) { + whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) + + var latest: MobileSubscriptionModel? = null + val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(MobileSubscriptionModel()) + + job.cancel() + } + + @Test + fun testFlowForSubId_emergencyOnly() = + runBlocking(IMMEDIATE) { + whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) + + var latest: MobileSubscriptionModel? = null + val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + + val serviceState = ServiceState() + serviceState.isEmergencyOnly = true + + getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState) + + assertThat(latest?.isEmergencyOnly).isEqualTo(true) + + job.cancel() + } + + @Test + fun testFlowForSubId_emergencyOnly_toggles() = + runBlocking(IMMEDIATE) { + whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) + + var latest: MobileSubscriptionModel? = null + val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<ServiceStateListener>() + val serviceState = ServiceState() + serviceState.isEmergencyOnly = true + callback.onServiceStateChanged(serviceState) + serviceState.isEmergencyOnly = false + callback.onServiceStateChanged(serviceState) + + assertThat(latest?.isEmergencyOnly).isEqualTo(false) + + job.cancel() + } + + @Test + fun testFlowForSubId_signalStrengths_levelsUpdate() = + runBlocking(IMMEDIATE) { + whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) + + var latest: MobileSubscriptionModel? = null + val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<SignalStrengthsListener>() + val strength = signalStrength(1, 2, true) + callback.onSignalStrengthsChanged(strength) + + assertThat(latest?.isGsm).isEqualTo(true) + assertThat(latest?.primaryLevel).isEqualTo(1) + assertThat(latest?.cdmaLevel).isEqualTo(2) + + job.cancel() + } + + @Test + fun testFlowForSubId_dataConnectionState() = + runBlocking(IMMEDIATE) { + whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) + + var latest: MobileSubscriptionModel? = null + val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<DataConnectionStateListener>() + callback.onDataConnectionStateChanged(100, 200 /* unused */) + + assertThat(latest?.dataConnectionState).isEqualTo(100) + + job.cancel() + } + + @Test + fun testFlowForSubId_dataActivity() = + runBlocking(IMMEDIATE) { + whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) + + var latest: MobileSubscriptionModel? = null + val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<DataActivityListener>() + callback.onDataActivity(3) + + assertThat(latest?.dataActivityDirection).isEqualTo(3) + + job.cancel() + } + + @Test + fun testFlowForSubId_carrierNetworkChange() = + runBlocking(IMMEDIATE) { + whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) + + var latest: MobileSubscriptionModel? = null + val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<CarrierNetworkListener>() + callback.onCarrierNetworkChange(true) + + assertThat(latest?.carrierNetworkChangeActive).isEqualTo(true) + + job.cancel() + } + + @Test + fun testFlowForSubId_displayInfo() = + runBlocking(IMMEDIATE) { + whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) + + var latest: MobileSubscriptionModel? = null + val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + + val callback = getTelephonyCallbackForType<DisplayInfoListener>() + val ti = mock<TelephonyDisplayInfo>() + callback.onDisplayInfoChanged(ti) + + assertThat(latest?.displayInfo).isEqualTo(ti) + + job.cancel() + } + + @Test + fun testFlowForSubId_isCached() = + runBlocking(IMMEDIATE) { + whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) + + val state1 = underTest.getFlowForSubId(SUB_1_ID) + val state2 = underTest.getFlowForSubId(SUB_1_ID) + + assertThat(state1).isEqualTo(state2) + } + + @Test + fun testFlowForSubId_isRemovedAfterFinish() = + runBlocking(IMMEDIATE) { + whenever(telephonyManager.createForSubscriptionId(any())).thenReturn(telephonyManager) + + var latest: MobileSubscriptionModel? = null + + // Start collecting on some flow + val job = underTest.getFlowForSubId(SUB_1_ID).onEach { latest = it }.launchIn(this) + + // There should be once cached flow now + assertThat(underTest.getSubIdFlowCache().size).isEqualTo(1) + + // When the job is canceled, the cache should be cleared + job.cancel() + + assertThat(underTest.getSubIdFlowCache().size).isEqualTo(0) + } + + private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener { + val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>() + verify(subscriptionManager) + .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture()) + return callbackCaptor.value!! + } + + private fun getActiveDataSubscriptionCallback(): ActiveDataSubscriptionIdListener = + getTelephonyCallbackForType() + + private fun getTelephonyCallbacks(): List<TelephonyCallback> { + val callbackCaptor = argumentCaptor<TelephonyCallback>() + verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) + return callbackCaptor.allValues + } + + private inline fun <reified T> getTelephonyCallbackForType(): T { + val cbs = getTelephonyCallbacks().filterIsInstance<T>() + assertThat(cbs.size).isEqualTo(1) + return cbs[0] + } + + /** Convenience constructor for SignalStrength */ + private fun signalStrength(gsmLevel: Int, cdmaLevel: Int, isGsm: Boolean): SignalStrength { + val signalStrength = mock<SignalStrength>() + whenever(signalStrength.isGsm).thenReturn(isGsm) + whenever(signalStrength.level).thenReturn(gsmLevel) + val cdmaStrength = + mock<CellSignalStrengthCdma>().also { whenever(it.level).thenReturn(cdmaLevel) } + whenever(signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java)) + .thenReturn(listOf(cdmaStrength)) + + return signalStrength + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private const val SUB_1_ID = 1 + private val SUB_1 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } + + private const val SUB_2_ID = 2 + private val SUB_2 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepositoryTest.kt new file mode 100644 index 000000000000..91c233a4177d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepositoryTest.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2022 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.data.repository + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +class UserSetupRepositoryTest : SysuiTestCase() { + private lateinit var underTest: UserSetupRepository + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + private val scope = CoroutineScope(IMMEDIATE) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + underTest = + UserSetupRepositoryImpl( + deviceProvisionedController, + IMMEDIATE, + scope, + ) + } + + @After + fun tearDown() { + scope.cancel() + } + + @Test + fun testUserSetup_defaultFalse() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + + val job = underTest.isUserSetupFlow.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun testUserSetup_updatesOnChange() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + + val job = underTest.isUserSetupFlow.onEach { latest = it }.launchIn(this) + + whenever(deviceProvisionedController.isCurrentUserSetup).thenReturn(true) + val callback = getDeviceProvisionedListener() + callback.onUserSetupChanged() + + assertThat(latest).isTrue() + + job.cancel() + } + + private fun getDeviceProvisionedListener(): DeviceProvisionedListener { + val captor = argumentCaptor<DeviceProvisionedListener>() + verify(deviceProvisionedController).addCallback(captor.capture()) + return captor.value!! + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt new file mode 100644 index 000000000000..8ec68f36a837 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 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.domain.interactor + +import android.telephony.CellSignalStrength +import com.android.settingslib.SignalIcon +import com.android.settingslib.mobile.TelephonyIcons +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeMobileIconInteractor : MobileIconInteractor { + private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.UNKNOWN) + override val iconGroup = _iconGroup + + private val _isEmergencyOnly = MutableStateFlow<Boolean>(false) + override val isEmergencyOnly = _isEmergencyOnly + + private val _level = MutableStateFlow<Int>(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + override val level = _level + + private val _numberOfLevels = MutableStateFlow<Int>(4) + override val numberOfLevels = _numberOfLevels + + private val _cutOut = MutableStateFlow<Boolean>(false) + override val cutOut = _cutOut + + fun setIconGroup(group: SignalIcon.MobileIconGroup) { + _iconGroup.value = group + } + + fun setIsEmergencyOnly(emergency: Boolean) { + _isEmergencyOnly.value = emergency + } + + fun setLevel(level: Int) { + _level.value = level + } + + fun setNumberOfLevels(num: Int) { + _numberOfLevels.value = num + } + + fun setCutOut(cutOut: Boolean) { + _cutOut.value = cutOut + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt new file mode 100644 index 000000000000..2f07d9cb3831 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2022 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.domain.interactor + +import android.telephony.CellSignalStrength +import android.telephony.SubscriptionInfo +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test + +@SmallTest +class MobileIconInteractorTest : SysuiTestCase() { + private lateinit var underTest: MobileIconInteractor + private val mobileSubscriptionRepository = FakeMobileSubscriptionRepository() + private val sub1Flow = mobileSubscriptionRepository.getFlowForSubId(SUB_1_ID) + + @Before + fun setUp() { + underTest = MobileIconInteractorImpl(sub1Flow) + } + + @Test + fun gsm_level_default_unknown() = + runBlocking(IMMEDIATE) { + mobileSubscriptionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(isGsm = true), + SUB_1_ID + ) + + var latest: Int? = null + val job = underTest.level.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + + job.cancel() + } + + @Test + fun gsm_usesGsmLevel() = + runBlocking(IMMEDIATE) { + mobileSubscriptionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel( + isGsm = true, + primaryLevel = GSM_LEVEL, + cdmaLevel = CDMA_LEVEL + ), + SUB_1_ID + ) + + var latest: Int? = null + val job = underTest.level.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(GSM_LEVEL) + + job.cancel() + } + + @Test + fun cdma_level_default_unknown() = + runBlocking(IMMEDIATE) { + mobileSubscriptionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(isGsm = false), + SUB_1_ID + ) + + var latest: Int? = null + val job = underTest.level.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + job.cancel() + } + + @Test + fun cdma_usesCdmaLevel() = + runBlocking(IMMEDIATE) { + mobileSubscriptionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel( + isGsm = false, + primaryLevel = GSM_LEVEL, + cdmaLevel = CDMA_LEVEL + ), + SUB_1_ID + ) + + var latest: Int? = null + val job = underTest.level.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(CDMA_LEVEL) + + job.cancel() + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + + private const val GSM_LEVEL = 1 + private const val CDMA_LEVEL = 2 + + private const val SUB_1_ID = 1 + private val SUB_1 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } + + private const val SUB_2_ID = 2 + private val SUB_2 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt new file mode 100644 index 000000000000..89ad9cb9e51e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2022 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.domain.interactor + +import android.telephony.SubscriptionInfo +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileSubscriptionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository +import com.android.systemui.util.CarrierConfigTracker +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +class MobileIconsInteractorTest : SysuiTestCase() { + private lateinit var underTest: MobileIconsInteractor + private val userSetupRepository = FakeUserSetupRepository() + private val subscriptionsRepository = FakeMobileSubscriptionRepository() + + @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + underTest = + MobileIconsInteractor( + subscriptionsRepository, + carrierConfigTracker, + userSetupRepository, + ) + } + + @After fun tearDown() {} + + @Test + fun filteredSubscriptions_default() = + runBlocking(IMMEDIATE) { + var latest: List<SubscriptionInfo>? = null + val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(listOf<SubscriptionInfo>()) + + job.cancel() + } + + @Test + fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() = + runBlocking(IMMEDIATE) { + subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_2)) + + var latest: List<SubscriptionInfo>? = null + val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(listOf(SUB_1, SUB_2)) + + job.cancel() + } + + @Test + fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_3() = + runBlocking(IMMEDIATE) { + subscriptionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP)) + subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) + whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) + .thenReturn(false) + + var latest: List<SubscriptionInfo>? = null + val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this) + + // Filtered subscriptions should show the active one when the config is false + assertThat(latest).isEqualTo(listOf(SUB_3_OPP)) + + job.cancel() + } + + @Test + fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_4() = + runBlocking(IMMEDIATE) { + subscriptionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP)) + subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID) + whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) + .thenReturn(false) + + var latest: List<SubscriptionInfo>? = null + val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this) + + // Filtered subscriptions should show the active one when the config is false + assertThat(latest).isEqualTo(listOf(SUB_4_OPP)) + + job.cancel() + } + + @Test + fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_active_1() = + runBlocking(IMMEDIATE) { + subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP)) + subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID) + whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) + .thenReturn(true) + + var latest: List<SubscriptionInfo>? = null + val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this) + + // Filtered subscriptions should show the primary (non-opportunistic) if the config is + // true + assertThat(latest).isEqualTo(listOf(SUB_1)) + + job.cancel() + } + + @Test + fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_nonActive_1() = + runBlocking(IMMEDIATE) { + subscriptionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP)) + subscriptionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID) + whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) + .thenReturn(true) + + var latest: List<SubscriptionInfo>? = null + val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this) + + // Filtered subscriptions should show the primary (non-opportunistic) if the config is + // true + assertThat(latest).isEqualTo(listOf(SUB_1)) + + job.cancel() + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + + private const val SUB_1_ID = 1 + private val SUB_1 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } + + private const val SUB_2_ID = 2 + private val SUB_2 = + mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } + + private const val SUB_3_ID = 3 + private val SUB_3_OPP = + mock<SubscriptionInfo>().also { + whenever(it.subscriptionId).thenReturn(SUB_3_ID) + whenever(it.isOpportunistic).thenReturn(true) + } + + private const val SUB_4_ID = 4 + private val SUB_4_OPP = + mock<SubscriptionInfo>().also { + whenever(it.subscriptionId).thenReturn(SUB_4_ID) + whenever(it.isOpportunistic).thenReturn(true) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt new file mode 100644 index 000000000000..b374abbd5082 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2022 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.viewmodel + +import androidx.test.filters.SmallTest +import com.android.settingslib.graph.SignalDrawable +import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +class MobileIconViewModelTest : SysuiTestCase() { + private lateinit var underTest: MobileIconViewModel + private val interactor = FakeMobileIconInteractor() + @Mock private lateinit var logger: ConnectivityPipelineLogger + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + interactor.apply { + setLevel(1) + setCutOut(false) + setIconGroup(TelephonyIcons.THREE_G) + setIsEmergencyOnly(false) + setNumberOfLevels(4) + } + underTest = MobileIconViewModel(SUB_1_ID, interactor, logger) + } + + @Test + fun iconId_correctLevel_notCutout() = + runBlocking(IMMEDIATE) { + var latest: Int? = null + val job = underTest.iconId.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(SignalDrawable.getState(1, 4, false)) + + job.cancel() + } + + companion object { + private val IMMEDIATE = Dispatchers.Main.immediate + private const val SUB_1_ID = 1 + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java index 2be67edfc946..23c7a6139de8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java @@ -70,6 +70,10 @@ public class FakeStatusBarIconController extends BaseLeakChecker<IconManager> } @Override + public void setNewMobileIconSubIds(List<Integer> subIds) { + } + + @Override public void setCallStrengthIcons(String slot, List<CallIndicatorIconState> states) { } |