Merge "First pass at setting up a mobile connectivity pipeline" into tm-qpr-dev

This commit is contained in:
Evan Laird
2022-10-06 19:13:21 +00:00
committed by Android (Google) Code Review
33 changed files with 2252 additions and 105 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
@@ -83,6 +88,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.
*/
@@ -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) {

View File

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

View File

@@ -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) {
@@ -79,6 +104,18 @@ public class StatusBarIconHolder {
return holder;
}
/**
* 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.
*/
@@ -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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(
@@ -206,6 +208,22 @@ public class CarrierConfigTracker
return mDefaultShowOperatorNameConfig;
}
/**
* 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -69,6 +69,10 @@ public class FakeStatusBarIconController extends BaseLeakChecker<IconManager>
public void setMobileIcons(String slot, List<MobileIconState> states) {
}
@Override
public void setNewMobileIconSubIds(List<Integer> subIds) {
}
@Override
public void setCallStrengthIcons(String slot, List<CallIndicatorIconState> states) {
}