Merge "Custom network selection"

This commit is contained in:
Benedict Wong
2021-05-14 04:22:03 +00:00
committed by Gerrit Code Review
6 changed files with 520 additions and 149 deletions

View File

@@ -74,6 +74,36 @@ import java.util.concurrent.Executor;
public class VcnManager {
@NonNull private static final String TAG = VcnManager.class.getSimpleName();
/**
* Key for WiFi entry RSSI thresholds
*
* <p>The VCN will only migrate to a Carrier WiFi network that has a signal strength greater
* than, or equal to this threshold.
*
* <p>WARNING: The VCN does not listen for changes to this key made after VCN startup.
*
* @hide
*/
@NonNull
public static final String VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY =
"vcn_network_selection_wifi_entry_rssi_threshold";
/**
* Key for WiFi entry RSSI thresholds
*
* <p>If the VCN's selected Carrier WiFi network has a signal strength less than this threshold,
* the VCN will attempt to migrate away from the Carrier WiFi network.
*
* <p>WARNING: The VCN does not listen for changes to this key made after VCN startup.
*
* @hide
*/
@NonNull
public static final String VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY =
"vcn_network_selection_wifi_exit_rssi_threshold";
// TODO: Add separate signal strength thresholds for 2.4 GHz and 5GHz
private static final Map<
VcnNetworkPolicyChangeListener, VcnUnderlyingNetworkPolicyListenerBinder>
REGISTERED_POLICY_LISTENERS = new ConcurrentHashMap<>();

View File

@@ -145,7 +145,7 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
*/
public void handleSubscriptionsChanged() {
final Map<ParcelUuid, Set<String>> privilegedPackages = new HashMap<>();
final Map<Integer, ParcelUuid> newSubIdToGroupMap = new HashMap<>();
final Map<Integer, SubscriptionInfo> newSubIdToInfoMap = new HashMap<>();
final List<SubscriptionInfo> allSubs = mSubscriptionManager.getAllSubscriptionInfoList();
if (allSubs == null) {
@@ -160,7 +160,7 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
}
// Build subId -> subGrp cache
newSubIdToGroupMap.put(subInfo.getSubscriptionId(), subInfo.getGroupUuid());
newSubIdToInfoMap.put(subInfo.getSubscriptionId(), subInfo);
// Update subscription groups that are both ready, and active. For a group to be
// considered active, both of the following must be true:
@@ -186,7 +186,7 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
}
final TelephonySubscriptionSnapshot newSnapshot =
new TelephonySubscriptionSnapshot(newSubIdToGroupMap, privilegedPackages);
new TelephonySubscriptionSnapshot(newSubIdToInfoMap, privilegedPackages);
// If snapshot was meaningfully updated, fire the callback
if (!newSnapshot.equals(mCurrentSnapshot)) {
@@ -245,7 +245,7 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
/** TelephonySubscriptionSnapshot is a class containing info about active subscriptions */
public static class TelephonySubscriptionSnapshot {
private final Map<Integer, ParcelUuid> mSubIdToGroupMap;
private final Map<Integer, SubscriptionInfo> mSubIdToInfoMap;
private final Map<ParcelUuid, Set<String>> mPrivilegedPackages;
public static final TelephonySubscriptionSnapshot EMPTY_SNAPSHOT =
@@ -253,12 +253,12 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
@VisibleForTesting(visibility = Visibility.PRIVATE)
TelephonySubscriptionSnapshot(
@NonNull Map<Integer, ParcelUuid> subIdToGroupMap,
@NonNull Map<Integer, SubscriptionInfo> subIdToInfoMap,
@NonNull Map<ParcelUuid, Set<String>> privilegedPackages) {
Objects.requireNonNull(subIdToGroupMap, "subIdToGroupMap was null");
Objects.requireNonNull(subIdToInfoMap, "subIdToInfoMap was null");
Objects.requireNonNull(privilegedPackages, "privilegedPackages was null");
mSubIdToGroupMap = Collections.unmodifiableMap(subIdToGroupMap);
mSubIdToInfoMap = Collections.unmodifiableMap(subIdToInfoMap);
final Map<ParcelUuid, Set<String>> unmodifiableInnerSets = new ArrayMap<>();
for (Entry<ParcelUuid, Set<String>> entry : privilegedPackages.entrySet()) {
@@ -285,7 +285,9 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
/** Returns the Subscription Group for a given subId. */
@Nullable
public ParcelUuid getGroupForSubId(int subId) {
return mSubIdToGroupMap.get(subId);
return mSubIdToInfoMap.containsKey(subId)
? mSubIdToInfoMap.get(subId).getGroupUuid()
: null;
}
/**
@@ -295,8 +297,8 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
public Set<Integer> getAllSubIdsInGroup(ParcelUuid subGrp) {
final Set<Integer> subIds = new ArraySet<>();
for (Entry<Integer, ParcelUuid> entry : mSubIdToGroupMap.entrySet()) {
if (subGrp.equals(entry.getValue())) {
for (Entry<Integer, SubscriptionInfo> entry : mSubIdToInfoMap.entrySet()) {
if (subGrp.equals(entry.getValue().getGroupUuid())) {
subIds.add(entry.getKey());
}
}
@@ -304,9 +306,17 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
return subIds;
}
/** Checks if the requested subscription is opportunistic */
@NonNull
public boolean isOpportunistic(int subId) {
return mSubIdToInfoMap.containsKey(subId)
? mSubIdToInfoMap.get(subId).isOpportunistic()
: false;
}
@Override
public int hashCode() {
return Objects.hash(mSubIdToGroupMap, mPrivilegedPackages);
return Objects.hash(mSubIdToInfoMap, mPrivilegedPackages);
}
@Override
@@ -317,7 +327,7 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
final TelephonySubscriptionSnapshot other = (TelephonySubscriptionSnapshot) obj;
return mSubIdToGroupMap.equals(other.mSubIdToGroupMap)
return mSubIdToInfoMap.equals(other.mSubIdToInfoMap)
&& mPrivilegedPackages.equals(other.mPrivilegedPackages);
}
@@ -326,7 +336,7 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
pw.println("TelephonySubscriptionSnapshot:");
pw.increaseIndent();
pw.println("mSubIdToGroupMap: " + mSubIdToGroupMap);
pw.println("mSubIdToInfoMap: " + mSubIdToInfoMap);
pw.println("mPrivilegedPackages: " + mPrivilegedPackages);
pw.decreaseIndent();
@@ -335,7 +345,7 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
@Override
public String toString() {
return "TelephonySubscriptionSnapshot{ "
+ "mSubIdToGroupMap=" + mSubIdToGroupMap
+ "mSubIdToInfoMap=" + mSubIdToInfoMap
+ ", mPrivilegedPackages=" + mPrivilegedPackages
+ " }";
}

View File

@@ -16,6 +16,10 @@
package com.android.server.vcn;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.ConnectivityManager;
@@ -25,8 +29,16 @@ import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.TelephonyNetworkSpecifier;
import android.net.vcn.VcnManager;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.ParcelUuid;
import android.os.PersistableBundle;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyManager;
import android.util.ArrayMap;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
@@ -35,9 +47,13 @@ import com.android.internal.util.IndentingPrintWriter;
import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
/**
* Tracks a set of Networks underpinning a VcnGatewayConnection.
@@ -51,6 +67,45 @@ import java.util.Set;
public class UnderlyingNetworkTracker {
@NonNull private static final String TAG = UnderlyingNetworkTracker.class.getSimpleName();
/**
* Minimum signal strength for a WiFi network to be eligible for switching to
*
* <p>A network that satisfies this is eligible to become the selected underlying network with
* no additional conditions
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
static final int WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT = -70;
/**
* Minimum signal strength to continue using a WiFi network
*
* <p>A network that satisfies the conditions may ONLY continue to be used if it is already
* selected as the underlying network. A WiFi network satisfying this condition, but NOT the
* prospective-network RSSI threshold CANNOT be switched to.
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
static final int WIFI_EXIT_RSSI_THRESHOLD_DEFAULT = -74;
/** Priority for any cellular network for which the subscription is listed as opportunistic */
@VisibleForTesting(visibility = Visibility.PRIVATE)
static final int PRIORITY_OPPORTUNISTIC_CELLULAR = 0;
/** Priority for any WiFi network which is in use, and satisfies the in-use RSSI threshold */
@VisibleForTesting(visibility = Visibility.PRIVATE)
static final int PRIORITY_WIFI_IN_USE = 1;
/** Priority for any WiFi network which satisfies the prospective-network RSSI threshold */
@VisibleForTesting(visibility = Visibility.PRIVATE)
static final int PRIORITY_WIFI_PROSPECTIVE = 2;
/** Priority for any standard macro cellular network */
@VisibleForTesting(visibility = Visibility.PRIVATE)
static final int PRIORITY_MACRO_CELLULAR = 3;
/** Priority for any other networks (including unvalidated, etc) */
@VisibleForTesting(visibility = Visibility.PRIVATE)
static final int PRIORITY_ANY = Integer.MAX_VALUE;
@NonNull private final VcnContext mVcnContext;
@NonNull private final ParcelUuid mSubscriptionGroup;
@NonNull private final Set<Integer> mRequiredUnderlyingNetworkCapabilities;
@@ -58,12 +113,17 @@ public class UnderlyingNetworkTracker {
@NonNull private final Dependencies mDeps;
@NonNull private final Handler mHandler;
@NonNull private final ConnectivityManager mConnectivityManager;
@NonNull private final TelephonyCallback mActiveDataSubIdListener =
new VcnActiveDataSubscriptionIdListener();
@NonNull private final List<NetworkCallback> mCellBringupCallbacks = new ArrayList<>();
@Nullable private NetworkCallback mWifiBringupCallback;
@Nullable private NetworkCallback mRouteSelectionCallback;
@Nullable private NetworkCallback mWifiEntryRssiThresholdCallback;
@Nullable private NetworkCallback mWifiExitRssiThresholdCallback;
@Nullable private UnderlyingNetworkListener mRouteSelectionCallback;
@NonNull private TelephonySubscriptionSnapshot mLastSnapshot;
@Nullable private PersistableBundle mCarrierConfig;
private boolean mIsQuitting = false;
@Nullable private UnderlyingNetworkRecord mCurrentRecord;
@@ -104,6 +164,30 @@ public class UnderlyingNetworkTracker {
mHandler = new Handler(mVcnContext.getLooper());
mConnectivityManager = mVcnContext.getContext().getSystemService(ConnectivityManager.class);
mVcnContext
.getContext()
.getSystemService(TelephonyManager.class)
.registerTelephonyCallback(new HandlerExecutor(mHandler), mActiveDataSubIdListener);
// TODO: Listen for changes in carrier config that affect this.
for (int subId : mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup)) {
PersistableBundle config =
mVcnContext
.getContext()
.getSystemService(CarrierConfigManager.class)
.getConfigForSubId(subId);
if (config != null) {
mCarrierConfig = config;
// Attempt to use (any) non-opportunistic subscription. If this subscription is
// opportunistic, continue and try to find a non-opportunistic subscription, using
// the opportunistic ones as a last resort.
if (!isOpportunistic(mLastSnapshot, Collections.singleton(subId))) {
break;
}
}
}
registerOrUpdateNetworkRequests();
}
@@ -111,16 +195,30 @@ public class UnderlyingNetworkTracker {
private void registerOrUpdateNetworkRequests() {
NetworkCallback oldRouteSelectionCallback = mRouteSelectionCallback;
NetworkCallback oldWifiCallback = mWifiBringupCallback;
NetworkCallback oldWifiEntryRssiThresholdCallback = mWifiEntryRssiThresholdCallback;
NetworkCallback oldWifiExitRssiThresholdCallback = mWifiExitRssiThresholdCallback;
List<NetworkCallback> oldCellCallbacks = new ArrayList<>(mCellBringupCallbacks);
mCellBringupCallbacks.clear();
// Register new callbacks. Make-before-break; always register new callbacks before removal
// of old callbacks
if (!mIsQuitting) {
mRouteSelectionCallback = new RouteSelectionCallback();
mConnectivityManager.requestBackgroundNetwork(
mRouteSelectionCallback = new UnderlyingNetworkListener();
mConnectivityManager.registerNetworkCallback(
getRouteSelectionRequest(), mRouteSelectionCallback, mHandler);
mWifiEntryRssiThresholdCallback = new NetworkBringupCallback();
mConnectivityManager.registerNetworkCallback(
getWifiEntryRssiThresholdNetworkRequest(),
mWifiEntryRssiThresholdCallback,
mHandler);
mWifiExitRssiThresholdCallback = new NetworkBringupCallback();
mConnectivityManager.registerNetworkCallback(
getWifiExitRssiThresholdNetworkRequest(),
mWifiExitRssiThresholdCallback,
mHandler);
mWifiBringupCallback = new NetworkBringupCallback();
mConnectivityManager.requestBackgroundNetwork(
getWifiNetworkRequest(), mWifiBringupCallback, mHandler);
@@ -135,6 +233,8 @@ public class UnderlyingNetworkTracker {
} else {
mRouteSelectionCallback = null;
mWifiBringupCallback = null;
mWifiEntryRssiThresholdCallback = null;
mWifiExitRssiThresholdCallback = null;
// mCellBringupCallbacks already cleared above.
}
@@ -145,6 +245,12 @@ public class UnderlyingNetworkTracker {
if (oldWifiCallback != null) {
mConnectivityManager.unregisterNetworkCallback(oldWifiCallback);
}
if (oldWifiEntryRssiThresholdCallback != null) {
mConnectivityManager.unregisterNetworkCallback(oldWifiEntryRssiThresholdCallback);
}
if (oldWifiExitRssiThresholdCallback != null) {
mConnectivityManager.unregisterNetworkCallback(oldWifiExitRssiThresholdCallback);
}
for (NetworkCallback cellBringupCallback : oldCellCallbacks) {
mConnectivityManager.unregisterNetworkCallback(cellBringupCallback);
}
@@ -168,6 +274,8 @@ public class UnderlyingNetworkTracker {
}
return getBaseNetworkRequestBuilder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
.setSubscriptionIds(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))
.build();
}
@@ -188,6 +296,38 @@ public class UnderlyingNetworkTracker {
.build();
}
/**
* Builds the WiFi entry threshold signal strength request
*
* <p>This request ensures that WiFi reports the crossing of the wifi entry RSSI threshold.
* Without this request, WiFi rate-limits, and reports signal strength changes at too slow a
* pace to effectively select a short-lived WiFi offload network.
*/
private NetworkRequest getWifiEntryRssiThresholdNetworkRequest() {
return getBaseNetworkRequestBuilder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.setSubscriptionIds(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))
// Ensure wifi updates signal strengths when crossing this threshold.
.setSignalStrength(getWifiEntryRssiThreshold(mCarrierConfig))
.build();
}
/**
* Builds the WiFi exit threshold signal strength request
*
* <p>This request ensures that WiFi reports the crossing of the wifi exit RSSI threshold.
* Without this request, WiFi rate-limits, and reports signal strength changes at too slow a
* pace to effectively select away from a failing WiFi network.
*/
private NetworkRequest getWifiExitRssiThresholdNetworkRequest() {
return getBaseNetworkRequestBuilder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.setSubscriptionIds(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))
// Ensure wifi updates signal strengths when crossing this threshold.
.setSignalStrength(getWifiExitRssiThreshold(mCarrierConfig))
.build();
}
/**
* Builds a Cellular bringup request for a given subId
*
@@ -233,10 +373,18 @@ public class UnderlyingNetworkTracker {
* reevaluate its NetworkBringupCallbacks. This may result in NetworkRequests being registered
* or unregistered if the subIds mapped to the this Tracker's SubscriptionGroup change.
*/
public void updateSubscriptionSnapshot(@NonNull TelephonySubscriptionSnapshot snapshot) {
Objects.requireNonNull(snapshot, "Missing snapshot");
public void updateSubscriptionSnapshot(@NonNull TelephonySubscriptionSnapshot newSnapshot) {
Objects.requireNonNull(newSnapshot, "Missing newSnapshot");
mLastSnapshot = snapshot;
final TelephonySubscriptionSnapshot oldSnapshot = mLastSnapshot;
mLastSnapshot = newSnapshot;
// Only trigger re-registration if subIds in this group have changed
if (oldSnapshot
.getAllSubIdsInGroup(mSubscriptionGroup)
.equals(newSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))) {
return;
}
registerOrUpdateNetworkRequests();
}
@@ -247,88 +395,43 @@ public class UnderlyingNetworkTracker {
// Will unregister all existing callbacks, but not register new ones due to quitting flag.
registerOrUpdateNetworkRequests();
mVcnContext
.getContext()
.getSystemService(TelephonyManager.class)
.unregisterTelephonyCallback(mActiveDataSubIdListener);
}
/** Returns whether the currently selected Network matches the given network. */
private static boolean isSameNetwork(
@Nullable UnderlyingNetworkRecord.Builder recordInProgress, @NonNull Network network) {
return recordInProgress != null && recordInProgress.getNetwork().equals(network);
}
private void reevaluateNetworks() {
TreeSet<UnderlyingNetworkRecord> sorted =
new TreeSet<>(
UnderlyingNetworkRecord.getComparator(
mSubscriptionGroup, mLastSnapshot, mCurrentRecord, mCarrierConfig));
sorted.addAll(mRouteSelectionCallback.getUnderlyingNetworks());
/** Notify the Callback if a full UnderlyingNetworkRecord exists. */
private void maybeNotifyCallback() {
// Only forward this update if a complete record has been received
if (!mRecordInProgress.isValid()) {
UnderlyingNetworkRecord candidate = sorted.isEmpty() ? null : sorted.first();
if (Objects.equals(mCurrentRecord, candidate)) {
return;
}
// Only forward this update if the updated record differs form the current record
UnderlyingNetworkRecord updatedRecord = mRecordInProgress.build();
if (!updatedRecord.equals(mCurrentRecord)) {
mCurrentRecord = updatedRecord;
mCb.onSelectedUnderlyingNetworkChanged(mCurrentRecord);
}
mCurrentRecord = candidate;
mCb.onSelectedUnderlyingNetworkChanged(mCurrentRecord);
}
private void handleNetworkAvailable(@NonNull Network network) {
mVcnContext.ensureRunningOnLooperThread();
mRecordInProgress = new UnderlyingNetworkRecord.Builder(network);
}
private void handleNetworkLost(@NonNull Network network) {
mVcnContext.ensureRunningOnLooperThread();
if (!isSameNetwork(mRecordInProgress, network)) {
Slog.wtf(TAG, "Non-underlying Network lost");
return;
private static boolean isOpportunistic(
@NonNull TelephonySubscriptionSnapshot snapshot, Set<Integer> subIds) {
if (snapshot == null) {
Slog.wtf(TAG, "Got null snapshot");
return false;
}
mRecordInProgress = null;
mCurrentRecord = null;
mCb.onSelectedUnderlyingNetworkChanged(null /* underlyingNetworkRecord */);
}
private void handleCapabilitiesChanged(
@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) {
mVcnContext.ensureRunningOnLooperThread();
if (!isSameNetwork(mRecordInProgress, network)) {
Slog.wtf(TAG, "Invalid update to NetworkCapabilities");
return;
for (int subId : subIds) {
if (snapshot.isOpportunistic(subId)) {
return true;
}
}
mRecordInProgress.setNetworkCapabilities(networkCapabilities);
maybeNotifyCallback();
}
private void handlePropertiesChanged(
@NonNull Network network, @NonNull LinkProperties linkProperties) {
mVcnContext.ensureRunningOnLooperThread();
if (!isSameNetwork(mRecordInProgress, network)) {
Slog.wtf(TAG, "Invalid update to LinkProperties");
return;
}
mRecordInProgress.setLinkProperties(linkProperties);
maybeNotifyCallback();
}
private void handleNetworkBlocked(@NonNull Network network, boolean isBlocked) {
mVcnContext.ensureRunningOnLooperThread();
if (!isSameNetwork(mRecordInProgress, network)) {
Slog.wtf(TAG, "Invalid update to isBlocked");
return;
}
mRecordInProgress.setIsBlocked(isBlocked);
maybeNotifyCallback();
return false;
}
/**
@@ -347,36 +450,104 @@ public class UnderlyingNetworkTracker {
* truth.
*/
@VisibleForTesting
class RouteSelectionCallback extends NetworkCallback {
class UnderlyingNetworkListener extends NetworkCallback {
private final Map<Network, UnderlyingNetworkRecord.Builder>
mUnderlyingNetworkRecordBuilders = new ArrayMap<>();
private List<UnderlyingNetworkRecord> getUnderlyingNetworks() {
final List<UnderlyingNetworkRecord> records = new ArrayList<>();
for (UnderlyingNetworkRecord.Builder builder :
mUnderlyingNetworkRecordBuilders.values()) {
if (builder.isValid()) {
records.add(builder.build());
}
}
return records;
}
@Override
public void onAvailable(@NonNull Network network) {
handleNetworkAvailable(network);
mUnderlyingNetworkRecordBuilders.put(
network, new UnderlyingNetworkRecord.Builder(network));
}
@Override
public void onLost(@NonNull Network network) {
handleNetworkLost(network);
mUnderlyingNetworkRecordBuilders.remove(network);
reevaluateNetworks();
}
@Override
public void onCapabilitiesChanged(
@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) {
if (networkCapabilities.equals(mRecordInProgress.getNetworkCapabilities())) return;
handleCapabilitiesChanged(network, networkCapabilities);
final UnderlyingNetworkRecord.Builder builder =
mUnderlyingNetworkRecordBuilders.get(network);
if (builder == null) {
Slog.wtf(TAG, "Got capabilities change for unknown key: " + network);
return;
}
builder.setNetworkCapabilities(networkCapabilities);
if (builder.isValid()) {
reevaluateNetworks();
}
}
@Override
public void onLinkPropertiesChanged(
@NonNull Network network, @NonNull LinkProperties linkProperties) {
handlePropertiesChanged(network, linkProperties);
final UnderlyingNetworkRecord.Builder builder =
mUnderlyingNetworkRecordBuilders.get(network);
if (builder == null) {
Slog.wtf(TAG, "Got link properties change for unknown key: " + network);
return;
}
builder.setLinkProperties(linkProperties);
if (builder.isValid()) {
reevaluateNetworks();
}
}
@Override
public void onBlockedStatusChanged(@NonNull Network network, boolean isBlocked) {
handleNetworkBlocked(network, isBlocked);
final UnderlyingNetworkRecord.Builder builder =
mUnderlyingNetworkRecordBuilders.get(network);
if (builder == null) {
Slog.wtf(TAG, "Got blocked status change for unknown key: " + network);
return;
}
builder.setIsBlocked(isBlocked);
if (builder.isValid()) {
reevaluateNetworks();
}
}
}
private static int getWifiEntryRssiThreshold(@Nullable PersistableBundle carrierConfig) {
if (carrierConfig != null) {
return carrierConfig.getInt(
VcnManager.VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY,
WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT);
}
return WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT;
}
private static int getWifiExitRssiThreshold(@Nullable PersistableBundle carrierConfig) {
if (carrierConfig != null) {
return carrierConfig.getInt(
VcnManager.VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY,
WIFI_EXIT_RSSI_THRESHOLD_DEFAULT);
}
return WIFI_EXIT_RSSI_THRESHOLD_DEFAULT;
}
/** A record of a single underlying network, caching relevant fields. */
public static class UnderlyingNetworkRecord {
@NonNull public final Network network;
@@ -413,6 +584,89 @@ public class UnderlyingNetworkTracker {
return Objects.hash(network, networkCapabilities, linkProperties, isBlocked);
}
/**
* Gives networks a priority class, based on the following priorities:
*
* <ol>
* <li>Opportunistic cellular
* <li>Carrier WiFi, signal strength >= WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT
* <li>Carrier WiFi, active network + signal strength >= WIFI_EXIT_RSSI_THRESHOLD_DEFAULT
* <li>Macro cellular
* <li>Any others
* </ol>
*/
private int calculatePriorityClass(
ParcelUuid subscriptionGroup,
TelephonySubscriptionSnapshot snapshot,
UnderlyingNetworkRecord currentlySelected,
PersistableBundle carrierConfig) {
final NetworkCapabilities caps = networkCapabilities;
// mRouteSelectionNetworkRequest requires a network be both VALIDATED and NOT_SUSPENDED
if (isBlocked) {
Slog.wtf(TAG, "Network blocked for System Server: " + network);
return PRIORITY_ANY;
}
if (caps.hasTransport(TRANSPORT_CELLULAR)
&& isOpportunistic(snapshot, caps.getSubscriptionIds())) {
// If this carrier is the active data provider, ensure that opportunistic is only
// ever prioritized if it is also the active data subscription. This ensures that
// if an opportunistic subscription is still in the process of being switched to,
// or switched away from, the VCN does not attempt to continue using it against the
// decision made at the telephony layer. Failure to do so may result in the modem
// switching back and forth.
//
// Allow the following two cases:
// 1. Active subId is NOT in the group that this VCN is supporting
// 2. This opportunistic subscription is for the active subId
if (!snapshot.getAllSubIdsInGroup(subscriptionGroup)
.contains(SubscriptionManager.getActiveDataSubscriptionId())
|| caps.getSubscriptionIds()
.contains(SubscriptionManager.getActiveDataSubscriptionId())) {
return PRIORITY_OPPORTUNISTIC_CELLULAR;
}
}
if (caps.hasTransport(TRANSPORT_WIFI)) {
if (caps.getSignalStrength() >= getWifiExitRssiThreshold(carrierConfig)
&& currentlySelected != null
&& network.equals(currentlySelected.network)) {
return PRIORITY_WIFI_IN_USE;
}
if (caps.getSignalStrength() >= getWifiEntryRssiThreshold(carrierConfig)) {
return PRIORITY_WIFI_PROSPECTIVE;
}
}
// Disallow opportunistic subscriptions from matching PRIORITY_MACRO_CELLULAR, as might
// be the case when Default Data SubId (CBRS) != Active Data SubId (MACRO), as might be
// the case if the Default Data SubId does not support certain services (eg voice
// calling)
if (caps.hasTransport(TRANSPORT_CELLULAR)
&& !isOpportunistic(snapshot, caps.getSubscriptionIds())) {
return PRIORITY_MACRO_CELLULAR;
}
return PRIORITY_ANY;
}
private static Comparator<UnderlyingNetworkRecord> getComparator(
ParcelUuid subscriptionGroup,
TelephonySubscriptionSnapshot snapshot,
UnderlyingNetworkRecord currentlySelected,
PersistableBundle carrierConfig) {
return (left, right) -> {
return Integer.compare(
left.calculatePriorityClass(
subscriptionGroup, snapshot, currentlySelected, carrierConfig),
right.calculatePriorityClass(
subscriptionGroup, snapshot, currentlySelected, carrierConfig));
};
}
/** Dumps the state of this record for logging and debugging purposes. */
public void dump(IndentingPrintWriter pw) {
pw.println("UnderlyingNetworkRecord:");
@@ -434,6 +688,8 @@ public class UnderlyingNetworkTracker {
boolean mIsBlocked;
boolean mWasIsBlockedSet;
@Nullable private UnderlyingNetworkRecord mCached;
private Builder(@NonNull Network network) {
mNetwork = network;
}
@@ -445,6 +701,7 @@ public class UnderlyingNetworkTracker {
private void setNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
mNetworkCapabilities = networkCapabilities;
mCached = null;
}
@Nullable
@@ -454,11 +711,13 @@ public class UnderlyingNetworkTracker {
private void setLinkProperties(@NonNull LinkProperties linkProperties) {
mLinkProperties = linkProperties;
mCached = null;
}
private void setIsBlocked(boolean isBlocked) {
mIsBlocked = isBlocked;
mWasIsBlockedSet = true;
mCached = null;
}
private boolean isValid() {
@@ -466,12 +725,30 @@ public class UnderlyingNetworkTracker {
}
private UnderlyingNetworkRecord build() {
return new UnderlyingNetworkRecord(
mNetwork, mNetworkCapabilities, mLinkProperties, mIsBlocked);
if (!isValid()) {
throw new IllegalArgumentException(
"Called build before UnderlyingNetworkRecord was valid");
}
if (mCached == null) {
mCached =
new UnderlyingNetworkRecord(
mNetwork, mNetworkCapabilities, mLinkProperties, mIsBlocked);
}
return mCached;
}
}
}
private class VcnActiveDataSubscriptionIdListener extends TelephonyCallback
implements ActiveDataSubscriptionIdListener {
@Override
public void onActiveDataSubscriptionIdChanged(int subId) {
reevaluateNetworks();
}
}
/** Callbacks for being notified of the changes in, or to the selected underlying network. */
public interface UnderlyingNetworkTrackerCallback {
/**

View File

@@ -88,13 +88,13 @@ public class TelephonySubscriptionTrackerTest {
private static final SubscriptionInfo TEST_SUBINFO_2 = mock(SubscriptionInfo.class);
private static final Map<ParcelUuid, Set<String>> TEST_PRIVILEGED_PACKAGES =
Collections.singletonMap(TEST_PARCEL_UUID, Collections.singleton(PACKAGE_NAME));
private static final Map<Integer, ParcelUuid> TEST_SUBID_TO_GROUP_MAP;
private static final Map<Integer, SubscriptionInfo> TEST_SUBID_TO_INFO_MAP;
static {
final Map<Integer, ParcelUuid> subIdToGroupMap = new HashMap<>();
subIdToGroupMap.put(TEST_SUBSCRIPTION_ID_1, TEST_PARCEL_UUID);
subIdToGroupMap.put(TEST_SUBSCRIPTION_ID_2, TEST_PARCEL_UUID);
TEST_SUBID_TO_GROUP_MAP = Collections.unmodifiableMap(subIdToGroupMap);
final Map<Integer, SubscriptionInfo> subIdToGroupMap = new HashMap<>();
subIdToGroupMap.put(TEST_SUBSCRIPTION_ID_1, TEST_SUBINFO_1);
subIdToGroupMap.put(TEST_SUBSCRIPTION_ID_2, TEST_SUBINFO_2);
TEST_SUBID_TO_INFO_MAP = Collections.unmodifiableMap(subIdToGroupMap);
}
@NonNull private final Context mContext;
@@ -190,13 +190,13 @@ public class TelephonySubscriptionTrackerTest {
private TelephonySubscriptionSnapshot buildExpectedSnapshot(
Map<ParcelUuid, Set<String>> privilegedPackages) {
return buildExpectedSnapshot(TEST_SUBID_TO_GROUP_MAP, privilegedPackages);
return buildExpectedSnapshot(TEST_SUBID_TO_INFO_MAP, privilegedPackages);
}
private TelephonySubscriptionSnapshot buildExpectedSnapshot(
Map<Integer, ParcelUuid> subIdToGroupMap,
Map<Integer, SubscriptionInfo> subIdToInfoMap,
Map<ParcelUuid, Set<String>> privilegedPackages) {
return new TelephonySubscriptionSnapshot(subIdToGroupMap, privilegedPackages);
return new TelephonySubscriptionSnapshot(subIdToInfoMap, privilegedPackages);
}
private void verifyNoActiveSubscriptions() {
@@ -371,7 +371,7 @@ public class TelephonySubscriptionTrackerTest {
@Test
public void testTelephonySubscriptionSnapshotGetGroupForSubId() throws Exception {
final TelephonySubscriptionSnapshot snapshot =
new TelephonySubscriptionSnapshot(TEST_SUBID_TO_GROUP_MAP, emptyMap());
new TelephonySubscriptionSnapshot(TEST_SUBID_TO_INFO_MAP, emptyMap());
assertEquals(TEST_PARCEL_UUID, snapshot.getGroupForSubId(TEST_SUBSCRIPTION_ID_1));
assertEquals(TEST_PARCEL_UUID, snapshot.getGroupForSubId(TEST_SUBSCRIPTION_ID_2));
@@ -380,7 +380,7 @@ public class TelephonySubscriptionTrackerTest {
@Test
public void testTelephonySubscriptionSnapshotGetAllSubIdsInGroup() throws Exception {
final TelephonySubscriptionSnapshot snapshot =
new TelephonySubscriptionSnapshot(TEST_SUBID_TO_GROUP_MAP, emptyMap());
new TelephonySubscriptionSnapshot(TEST_SUBID_TO_INFO_MAP, emptyMap());
assertEquals(
new ArraySet<>(Arrays.asList(TEST_SUBSCRIPTION_ID_1, TEST_SUBSCRIPTION_ID_2)),

View File

@@ -42,12 +42,14 @@ import android.net.NetworkRequest;
import android.net.TelephonyNetworkSpecifier;
import android.os.ParcelUuid;
import android.os.test.TestLooper;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionInfo;
import android.telephony.TelephonyManager;
import android.util.ArraySet;
import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
import com.android.server.vcn.UnderlyingNetworkTracker.NetworkBringupCallback;
import com.android.server.vcn.UnderlyingNetworkTracker.RouteSelectionCallback;
import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkListener;
import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkRecord;
import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkTrackerCallback;
@@ -98,11 +100,13 @@ public class UnderlyingNetworkTrackerTest {
@Mock private Context mContext;
@Mock private VcnNetworkProvider mVcnNetworkProvider;
@Mock private ConnectivityManager mConnectivityManager;
@Mock private TelephonyManager mTelephonyManager;
@Mock private CarrierConfigManager mCarrierConfigManager;
@Mock private TelephonySubscriptionSnapshot mSubscriptionSnapshot;
@Mock private UnderlyingNetworkTrackerCallback mNetworkTrackerCb;
@Mock private Network mNetwork;
@Captor private ArgumentCaptor<RouteSelectionCallback> mRouteSelectionCallbackCaptor;
@Captor private ArgumentCaptor<UnderlyingNetworkListener> mUnderlyingNetworkListenerCaptor;
private TestLooper mTestLooper;
private VcnContext mVcnContext;
@@ -127,6 +131,13 @@ public class UnderlyingNetworkTrackerTest {
mConnectivityManager,
Context.CONNECTIVITY_SERVICE,
ConnectivityManager.class);
setupSystemService(
mContext, mTelephonyManager, Context.TELEPHONY_SERVICE, TelephonyManager.class);
setupSystemService(
mContext,
mCarrierConfigManager,
Context.CARRIER_CONFIG_SERVICE,
CarrierConfigManager.class);
when(mSubscriptionSnapshot.getAllSubIdsInGroup(eq(SUB_GROUP))).thenReturn(INITIAL_SUB_IDS);
@@ -163,26 +174,26 @@ public class UnderlyingNetworkTrackerTest {
@Test
public void testNetworkCallbacksRegisteredOnStartupForTestMode() {
final ConnectivityManager cm = mock(ConnectivityManager.class);
setupSystemService(mContext, cm, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class);
final VcnContext vcnContext =
spy(
new VcnContext(
mContext,
mTestLooper.getLooper(),
mVcnNetworkProvider,
true /* isInTestMode */));
new VcnContext(
mContext,
mTestLooper.getLooper(),
mVcnNetworkProvider,
true /* isInTestMode */);
mUnderlyingNetworkTracker =
new UnderlyingNetworkTracker(
vcnContext,
SUB_GROUP,
mSubscriptionSnapshot,
Collections.singleton(NetworkCapabilities.NET_CAPABILITY_INTERNET),
mNetworkTrackerCb);
new UnderlyingNetworkTracker(
vcnContext,
SUB_GROUP,
mSubscriptionSnapshot,
Collections.singleton(NetworkCapabilities.NET_CAPABILITY_INTERNET),
mNetworkTrackerCb);
verify(mConnectivityManager)
.requestBackgroundNetwork(
verify(cm)
.registerNetworkCallback(
eq(getTestNetworkRequest(INITIAL_SUB_IDS)),
any(RouteSelectionCallback.class),
any(UnderlyingNetworkListener.class),
any());
}
@@ -200,9 +211,19 @@ public class UnderlyingNetworkTrackerTest {
}
verify(mConnectivityManager)
.requestBackgroundNetwork(
.registerNetworkCallback(
eq(getRouteSelectionRequest(expectedSubIds)),
any(RouteSelectionCallback.class),
any(UnderlyingNetworkListener.class),
any());
verify(mConnectivityManager)
.registerNetworkCallback(
eq(getWifiEntryRssiThresholdRequest(expectedSubIds)),
any(NetworkBringupCallback.class),
any());
verify(mConnectivityManager)
.registerNetworkCallback(
eq(getWifiExitRssiThresholdRequest(expectedSubIds)),
any(NetworkBringupCallback.class),
any());
}
@@ -218,9 +239,10 @@ public class UnderlyingNetworkTrackerTest {
mUnderlyingNetworkTracker.updateSubscriptionSnapshot(subscriptionUpdate);
// verify that initially-filed bringup requests are unregistered (cell + wifi)
verify(mConnectivityManager, times(INITIAL_SUB_IDS.size() + 1))
verify(mConnectivityManager, times(INITIAL_SUB_IDS.size() + 3))
.unregisterNetworkCallback(any(NetworkBringupCallback.class));
verify(mConnectivityManager).unregisterNetworkCallback(any(RouteSelectionCallback.class));
verify(mConnectivityManager)
.unregisterNetworkCallback(any(UnderlyingNetworkListener.class));
verifyNetworkRequestsRegistered(UPDATED_SUB_IDS);
}
@@ -231,6 +253,24 @@ public class UnderlyingNetworkTrackerTest {
.build();
}
private NetworkRequest getWifiEntryRssiThresholdRequest(Set<Integer> netCapsSubIds) {
// TODO (b/187991063): Add tests for carrier-config based thresholds
return getExpectedRequestBase()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.setSubscriptionIds(netCapsSubIds)
.setSignalStrength(UnderlyingNetworkTracker.WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT)
.build();
}
private NetworkRequest getWifiExitRssiThresholdRequest(Set<Integer> netCapsSubIds) {
// TODO (b/187991063): Add tests for carrier-config based thresholds
return getExpectedRequestBase()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.setSubscriptionIds(netCapsSubIds)
.setSignalStrength(UnderlyingNetworkTracker.WIFI_EXIT_RSSI_THRESHOLD_DEFAULT)
.build();
}
private NetworkRequest getCellRequestForSubId(int subId) {
return getExpectedRequestBase()
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
@@ -239,7 +279,11 @@ public class UnderlyingNetworkTrackerTest {
}
private NetworkRequest getRouteSelectionRequest(Set<Integer> netCapsSubIds) {
return getExpectedRequestBase().setSubscriptionIds(netCapsSubIds).build();
return getExpectedRequestBase()
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
.setSubscriptionIds(netCapsSubIds)
.build();
}
private NetworkRequest getTestNetworkRequest(Set<Integer> netCapsSubIds) {
@@ -265,11 +309,12 @@ public class UnderlyingNetworkTrackerTest {
public void testTeardown() {
mUnderlyingNetworkTracker.teardown();
// Expect 3 NetworkBringupCallbacks to be unregistered: 1 for WiFi and 2 for Cellular (1x
// for each subId)
verify(mConnectivityManager, times(3))
// Expect 5 NetworkBringupCallbacks to be unregistered: 1 for WiFi, 2 for Cellular (1x for
// each subId), and 1 for each of the Wifi signal strength thresholds
verify(mConnectivityManager, times(5))
.unregisterNetworkCallback(any(NetworkBringupCallback.class));
verify(mConnectivityManager).unregisterNetworkCallback(any(RouteSelectionCallback.class));
verify(mConnectivityManager)
.unregisterNetworkCallback(any(UnderlyingNetworkListener.class));
}
@Test
@@ -302,19 +347,19 @@ public class UnderlyingNetworkTrackerTest {
verifyRegistrationOnAvailableAndGetCallback();
}
private RouteSelectionCallback verifyRegistrationOnAvailableAndGetCallback() {
private UnderlyingNetworkListener verifyRegistrationOnAvailableAndGetCallback() {
return verifyRegistrationOnAvailableAndGetCallback(INITIAL_NETWORK_CAPABILITIES);
}
private RouteSelectionCallback verifyRegistrationOnAvailableAndGetCallback(
private UnderlyingNetworkListener verifyRegistrationOnAvailableAndGetCallback(
NetworkCapabilities networkCapabilities) {
verify(mConnectivityManager)
.requestBackgroundNetwork(
.registerNetworkCallback(
eq(getRouteSelectionRequest(INITIAL_SUB_IDS)),
mRouteSelectionCallbackCaptor.capture(),
mUnderlyingNetworkListenerCaptor.capture(),
any());
RouteSelectionCallback cb = mRouteSelectionCallbackCaptor.getValue();
UnderlyingNetworkListener cb = mUnderlyingNetworkListenerCaptor.getValue();
cb.onAvailable(mNetwork);
cb.onCapabilitiesChanged(mNetwork, networkCapabilities);
cb.onLinkPropertiesChanged(mNetwork, INITIAL_LINK_PROPERTIES);
@@ -332,7 +377,7 @@ public class UnderlyingNetworkTrackerTest {
@Test
public void testRecordTrackerCallbackNotifiedForNetworkCapabilitiesChange() {
RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback();
UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
cb.onCapabilitiesChanged(mNetwork, UPDATED_NETWORK_CAPABILITIES);
@@ -347,7 +392,7 @@ public class UnderlyingNetworkTrackerTest {
@Test
public void testRecordTrackerCallbackNotifiedForLinkPropertiesChange() {
RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback();
UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
cb.onLinkPropertiesChanged(mNetwork, UPDATED_LINK_PROPERTIES);
@@ -362,7 +407,7 @@ public class UnderlyingNetworkTrackerTest {
@Test
public void testRecordTrackerCallbackNotifiedForNetworkSuspended() {
RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback();
UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
cb.onCapabilitiesChanged(mNetwork, SUSPENDED_NETWORK_CAPABILITIES);
@@ -381,7 +426,7 @@ public class UnderlyingNetworkTrackerTest {
@Test
public void testRecordTrackerCallbackNotifiedForNetworkResumed() {
RouteSelectionCallback cb =
UnderlyingNetworkListener cb =
verifyRegistrationOnAvailableAndGetCallback(SUSPENDED_NETWORK_CAPABILITIES);
cb.onCapabilitiesChanged(mNetwork, INITIAL_NETWORK_CAPABILITIES);
@@ -401,7 +446,7 @@ public class UnderlyingNetworkTrackerTest {
@Test
public void testRecordTrackerCallbackNotifiedForBlocked() {
RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback();
UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
cb.onBlockedStatusChanged(mNetwork, true /* isBlocked */);
@@ -416,7 +461,7 @@ public class UnderlyingNetworkTrackerTest {
@Test
public void testRecordTrackerCallbackNotifiedForNetworkLoss() {
RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback();
UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
cb.onLost(mNetwork);
@@ -425,7 +470,7 @@ public class UnderlyingNetworkTrackerTest {
@Test
public void testRecordTrackerCallbackIgnoresDuplicateRecord() {
RouteSelectionCallback cb = verifyRegistrationOnAvailableAndGetCallback();
UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback();
cb.onCapabilitiesChanged(mNetwork, INITIAL_NETWORK_CAPABILITIES);
@@ -433,4 +478,6 @@ public class UnderlyingNetworkTrackerTest {
// UnderlyingNetworkRecord does not actually change
verifyNoMoreInteractions(mNetworkTrackerCb);
}
// TODO (b/187991063): Add tests for network prioritization
}

View File

@@ -54,6 +54,7 @@ import android.net.vcn.VcnGatewayConnectionConfigTest;
import android.os.ParcelUuid;
import android.os.PowerManager;
import android.os.test.TestLooper;
import android.telephony.SubscriptionInfo;
import com.android.internal.util.State;
import com.android.internal.util.WakeupMessage;
@@ -73,6 +74,12 @@ import java.util.concurrent.TimeUnit;
public class VcnGatewayConnectionTestBase {
protected static final ParcelUuid TEST_SUB_GRP = new ParcelUuid(UUID.randomUUID());
protected static final SubscriptionInfo TEST_SUB_INFO = mock(SubscriptionInfo.class);
static {
doReturn(TEST_SUB_GRP).when(TEST_SUB_INFO).getGroupUuid();
}
protected static final InetAddress TEST_DNS_ADDR =
InetAddresses.parseNumericAddress("2001:DB8:0:1::");
protected static final InetAddress TEST_DNS_ADDR_2 =
@@ -116,7 +123,7 @@ public class VcnGatewayConnectionTestBase {
protected static final TelephonySubscriptionSnapshot TEST_SUBSCRIPTION_SNAPSHOT =
new TelephonySubscriptionSnapshot(
Collections.singletonMap(TEST_SUB_ID, TEST_SUB_GRP), Collections.EMPTY_MAP);
Collections.singletonMap(TEST_SUB_ID, TEST_SUB_INFO), Collections.EMPTY_MAP);
@NonNull protected final Context mContext;
@NonNull protected final TestLooper mTestLooper;