blob: 7ddd1355a2d6559b343a0007fc2219114712de55 [file] [log] [blame]
/*
* Copyright (C) 2020 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.server.vcn;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener;
import static com.android.server.VcnManagementService.LOCAL_LOG;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.LinkProperties;
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 android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;
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.
*
* <p>A single UnderlyingNetworkTracker is built to serve a SINGLE VCN Gateway Connection, and MUST
* be torn down with the VcnGatewayConnection in order to ensure underlying networks are allowed to
* be reaped.
*
* @hide
*/
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;
private static final SparseArray<String> PRIORITY_TO_STRING_MAP = new SparseArray<>();
static {
PRIORITY_TO_STRING_MAP.put(
PRIORITY_OPPORTUNISTIC_CELLULAR, "PRIORITY_OPPORTUNISTIC_CELLULAR");
PRIORITY_TO_STRING_MAP.put(PRIORITY_WIFI_IN_USE, "PRIORITY_WIFI_IN_USE");
PRIORITY_TO_STRING_MAP.put(PRIORITY_WIFI_PROSPECTIVE, "PRIORITY_WIFI_PROSPECTIVE");
PRIORITY_TO_STRING_MAP.put(PRIORITY_MACRO_CELLULAR, "PRIORITY_MACRO_CELLULAR");
PRIORITY_TO_STRING_MAP.put(PRIORITY_ANY, "PRIORITY_ANY");
}
@NonNull private final VcnContext mVcnContext;
@NonNull private final ParcelUuid mSubscriptionGroup;
@NonNull private final UnderlyingNetworkTrackerCallback mCb;
@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 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;
@Nullable private UnderlyingNetworkRecord.Builder mRecordInProgress;
public UnderlyingNetworkTracker(
@NonNull VcnContext vcnContext,
@NonNull ParcelUuid subscriptionGroup,
@NonNull TelephonySubscriptionSnapshot snapshot,
@NonNull UnderlyingNetworkTrackerCallback cb) {
this(
vcnContext,
subscriptionGroup,
snapshot,
cb,
new Dependencies());
}
private UnderlyingNetworkTracker(
@NonNull VcnContext vcnContext,
@NonNull ParcelUuid subscriptionGroup,
@NonNull TelephonySubscriptionSnapshot snapshot,
@NonNull UnderlyingNetworkTrackerCallback cb,
@NonNull Dependencies deps) {
mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext");
mSubscriptionGroup = Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup");
mLastSnapshot = Objects.requireNonNull(snapshot, "Missing snapshot");
mCb = Objects.requireNonNull(cb, "Missing cb");
mDeps = Objects.requireNonNull(deps, "Missing deps");
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();
}
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 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);
for (final int subId : mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup)) {
final NetworkBringupCallback cb = new NetworkBringupCallback();
mCellBringupCallbacks.add(cb);
mConnectivityManager.requestBackgroundNetwork(
getCellNetworkRequestForSubId(subId), cb, mHandler);
}
} else {
mRouteSelectionCallback = null;
mWifiBringupCallback = null;
mWifiEntryRssiThresholdCallback = null;
mWifiExitRssiThresholdCallback = null;
// mCellBringupCallbacks already cleared above.
}
// Unregister old callbacks (as necessary)
if (oldRouteSelectionCallback != null) {
mConnectivityManager.unregisterNetworkCallback(oldRouteSelectionCallback);
}
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);
}
}
/**
* Builds the Route selection request
*
* <p>This request is guaranteed to select carrier-owned, non-VCN underlying networks by virtue
* of a populated set of subIds as expressed in NetworkCapabilities#getSubscriptionIds(). Only
* carrier owned networks may be selected, as the request specifies only subIds in the VCN's
* subscription group, while the VCN networks are excluded by virtue of not having subIds set on
* the VCN-exposed networks.
*
* <p>If the VCN that this UnderlyingNetworkTracker belongs to is in test-mode, this will return
* a NetworkRequest that only matches Test Networks.
*/
private NetworkRequest getRouteSelectionRequest() {
if (mVcnContext.isInTestMode()) {
return getTestNetworkRequest(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup));
}
return getBaseNetworkRequestBuilder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)
.setSubscriptionIds(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))
.build();
}
/**
* Builds the WiFi bringup request
*
* <p>This request is built specifically to match only carrier-owned WiFi networks, but is also
* built to ONLY keep Carrier WiFi Networks alive (but never bring them up). This is a result of
* the WifiNetworkFactory not advertising a list of subIds, and therefore not accepting this
* request. As such, it will bind to a Carrier WiFi Network that has already been brought up,
* but will NEVER bring up a Carrier WiFi network itself.
*/
private NetworkRequest getWifiNetworkRequest() {
return getBaseNetworkRequestBuilder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.setSubscriptionIds(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))
.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
*
* <p>This request is filed in order to ensure that the Telephony stack always has a
* NetworkRequest to bring up a VCN underlying cellular network. It is required in order to
* ensure that even when a VCN (appears as Cellular) satisfies the default request, Telephony
* will bring up additional underlying Cellular networks.
*
* <p>Since this request MUST make it to the TelephonyNetworkFactory, subIds are not specified
* in the NetworkCapabilities, but rather in the TelephonyNetworkSpecifier.
*/
private NetworkRequest getCellNetworkRequestForSubId(int subId) {
return getBaseNetworkRequestBuilder()
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.setNetworkSpecifier(new TelephonyNetworkSpecifier(subId))
.build();
}
/**
* Builds and returns a NetworkRequest builder common to all Underlying Network requests
*/
private NetworkRequest.Builder getBaseNetworkRequestBuilder() {
return new NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
}
/** Builds and returns a NetworkRequest for the given subIds to match Test Networks. */
private NetworkRequest getTestNetworkRequest(@NonNull Set<Integer> subIds) {
return new NetworkRequest.Builder()
.clearCapabilities()
.addTransportType(NetworkCapabilities.TRANSPORT_TEST)
.setSubscriptionIds(subIds)
.build();
}
/**
* Update this UnderlyingNetworkTracker's TelephonySubscriptionSnapshot.
*
* <p>Updating the TelephonySubscriptionSnapshot will cause this UnderlyingNetworkTracker to
* 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 newSnapshot) {
Objects.requireNonNull(newSnapshot, "Missing newSnapshot");
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();
}
/** Tears down this Tracker, and releases all underlying network requests. */
public void teardown() {
mVcnContext.ensureRunningOnLooperThread();
mIsQuitting = true;
// Will unregister all existing callbacks, but not register new ones due to quitting flag.
registerOrUpdateNetworkRequests();
mVcnContext
.getContext()
.getSystemService(TelephonyManager.class)
.unregisterTelephonyCallback(mActiveDataSubIdListener);
}
private void reevaluateNetworks() {
if (mIsQuitting || mRouteSelectionCallback == null) {
return; // UnderlyingNetworkTracker has quit.
}
TreeSet<UnderlyingNetworkRecord> sorted =
mRouteSelectionCallback.getSortedUnderlyingNetworks();
UnderlyingNetworkRecord candidate = sorted.isEmpty() ? null : sorted.first();
if (Objects.equals(mCurrentRecord, candidate)) {
return;
}
mCurrentRecord = candidate;
mCb.onSelectedUnderlyingNetworkChanged(mCurrentRecord);
}
private static boolean isOpportunistic(
@NonNull TelephonySubscriptionSnapshot snapshot, Set<Integer> subIds) {
if (snapshot == null) {
logWtf("Got null snapshot");
return false;
}
for (int subId : subIds) {
if (snapshot.isOpportunistic(subId)) {
return true;
}
}
return false;
}
/**
* NetworkBringupCallback is used to keep background, VCN-managed Networks from being reaped.
*
* <p>NetworkBringupCallback only exists to prevent matching (VCN-managed) Networks from being
* reaped, and no action is taken on any events firing.
*/
@VisibleForTesting
class NetworkBringupCallback extends NetworkCallback {}
/**
* RouteSelectionCallback is used to select the "best" underlying Network.
*
* <p>The "best" network is determined by ConnectivityService, which is treated as a source of
* truth.
*/
@VisibleForTesting
class UnderlyingNetworkListener extends NetworkCallback {
private final Map<Network, UnderlyingNetworkRecord.Builder>
mUnderlyingNetworkRecordBuilders = new ArrayMap<>();
UnderlyingNetworkListener() {
super(NetworkCallback.FLAG_INCLUDE_LOCATION_INFO);
}
private TreeSet<UnderlyingNetworkRecord> getSortedUnderlyingNetworks() {
TreeSet<UnderlyingNetworkRecord> sorted =
new TreeSet<>(
UnderlyingNetworkRecord.getComparator(
mSubscriptionGroup,
mLastSnapshot,
mCurrentRecord,
mCarrierConfig));
for (UnderlyingNetworkRecord.Builder builder :
mUnderlyingNetworkRecordBuilders.values()) {
if (builder.isValid()) {
sorted.add(builder.build());
}
}
return sorted;
}
@Override
public void onAvailable(@NonNull Network network) {
mUnderlyingNetworkRecordBuilders.put(
network, new UnderlyingNetworkRecord.Builder(network));
}
@Override
public void onLost(@NonNull Network network) {
mUnderlyingNetworkRecordBuilders.remove(network);
reevaluateNetworks();
}
@Override
public void onCapabilitiesChanged(
@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) {
final UnderlyingNetworkRecord.Builder builder =
mUnderlyingNetworkRecordBuilders.get(network);
if (builder == null) {
logWtf("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) {
final UnderlyingNetworkRecord.Builder builder =
mUnderlyingNetworkRecordBuilders.get(network);
if (builder == null) {
logWtf("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) {
final UnderlyingNetworkRecord.Builder builder =
mUnderlyingNetworkRecordBuilders.get(network);
if (builder == null) {
logWtf("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;
@NonNull public final NetworkCapabilities networkCapabilities;
@NonNull public final LinkProperties linkProperties;
public final boolean isBlocked;
@VisibleForTesting(visibility = Visibility.PRIVATE)
UnderlyingNetworkRecord(
@NonNull Network network,
@NonNull NetworkCapabilities networkCapabilities,
@NonNull LinkProperties linkProperties,
boolean isBlocked) {
this.network = network;
this.networkCapabilities = networkCapabilities;
this.linkProperties = linkProperties;
this.isBlocked = isBlocked;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof UnderlyingNetworkRecord)) return false;
final UnderlyingNetworkRecord that = (UnderlyingNetworkRecord) o;
return network.equals(that.network)
&& networkCapabilities.equals(that.networkCapabilities)
&& linkProperties.equals(that.linkProperties)
&& isBlocked == that.isBlocked;
}
@Override
public int hashCode() {
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) {
logWtf("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. */
private void dump(
IndentingPrintWriter pw,
ParcelUuid subscriptionGroup,
TelephonySubscriptionSnapshot snapshot,
UnderlyingNetworkRecord currentlySelected,
PersistableBundle carrierConfig) {
pw.println("UnderlyingNetworkRecord:");
pw.increaseIndent();
final int priorityClass =
calculatePriorityClass(
subscriptionGroup, snapshot, currentlySelected, carrierConfig);
pw.println(
"Priority class: " + PRIORITY_TO_STRING_MAP.get(priorityClass) + " ("
+ priorityClass + ")");
pw.println("mNetwork: " + network);
pw.println("mNetworkCapabilities: " + networkCapabilities);
pw.println("mLinkProperties: " + linkProperties);
pw.decreaseIndent();
}
/** Builder to incrementally construct an UnderlyingNetworkRecord. */
private static class Builder {
@NonNull private final Network mNetwork;
@Nullable private NetworkCapabilities mNetworkCapabilities;
@Nullable private LinkProperties mLinkProperties;
boolean mIsBlocked;
boolean mWasIsBlockedSet;
@Nullable private UnderlyingNetworkRecord mCached;
private Builder(@NonNull Network network) {
mNetwork = network;
}
@NonNull
private Network getNetwork() {
return mNetwork;
}
private void setNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
mNetworkCapabilities = networkCapabilities;
mCached = null;
}
@Nullable
private NetworkCapabilities getNetworkCapabilities() {
return mNetworkCapabilities;
}
private void setLinkProperties(@NonNull LinkProperties linkProperties) {
mLinkProperties = linkProperties;
mCached = null;
}
private void setIsBlocked(boolean isBlocked) {
mIsBlocked = isBlocked;
mWasIsBlockedSet = true;
mCached = null;
}
private boolean isValid() {
return mNetworkCapabilities != null && mLinkProperties != null && mWasIsBlockedSet;
}
private UnderlyingNetworkRecord build() {
if (!isValid()) {
throw new IllegalArgumentException(
"Called build before UnderlyingNetworkRecord was valid");
}
if (mCached == null) {
mCached =
new UnderlyingNetworkRecord(
mNetwork, mNetworkCapabilities, mLinkProperties, mIsBlocked);
}
return mCached;
}
}
}
private static void logWtf(String msg) {
Slog.wtf(TAG, msg);
LOCAL_LOG.log(TAG + " WTF: " + msg);
}
private static void logWtf(String msg, Throwable tr) {
Slog.wtf(TAG, msg, tr);
LOCAL_LOG.log(TAG + " WTF: " + msg + tr);
}
/** Dumps the state of this record for logging and debugging purposes. */
public void dump(IndentingPrintWriter pw) {
pw.println("UnderlyingNetworkTracker:");
pw.increaseIndent();
pw.println("Carrier WiFi Entry Threshold: " + getWifiEntryRssiThreshold(mCarrierConfig));
pw.println("Carrier WiFi Exit Threshold: " + getWifiExitRssiThreshold(mCarrierConfig));
pw.println(
"Currently selected: " + (mCurrentRecord == null ? null : mCurrentRecord.network));
pw.println("Underlying networks:");
pw.increaseIndent();
if (mRouteSelectionCallback != null) {
for (UnderlyingNetworkRecord record :
mRouteSelectionCallback.getSortedUnderlyingNetworks()) {
record.dump(pw, mSubscriptionGroup, mLastSnapshot, mCurrentRecord, mCarrierConfig);
}
}
pw.decreaseIndent();
pw.println();
pw.decreaseIndent();
}
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 {
/**
* Fired when a new underlying network is selected, or properties have changed.
*
* <p>This callback does NOT signal a mobility event.
*
* @param underlyingNetworkRecord The details of the new underlying network
*/
void onSelectedUnderlyingNetworkChanged(
@Nullable UnderlyingNetworkRecord underlyingNetworkRecord);
}
private static class Dependencies {}
}