blob: 7b26fe0370c90b692c929aa9ad4ff7cd8792fc37 [file] [log] [blame]
/*
* Copyright (C) 2021 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.routeselection;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
import static android.net.vcn.VcnUnderlyingNetworkPriority.NETWORK_QUALITY_ANY;
import static android.net.vcn.VcnUnderlyingNetworkPriority.NETWORK_QUALITY_OK;
import static com.android.server.VcnManagementService.LOCAL_LOG;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.NetworkCapabilities;
import android.net.TelephonyNetworkSpecifier;
import android.net.vcn.VcnCellUnderlyingNetworkPriority;
import android.net.vcn.VcnManager;
import android.net.vcn.VcnUnderlyingNetworkPriority;
import android.net.vcn.VcnWifiUnderlyingNetworkPriority;
import android.os.ParcelUuid;
import android.os.PersistableBundle;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;
import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
import com.android.server.vcn.VcnContext;
import java.util.LinkedHashSet;
import java.util.Set;
/** @hide */
class NetworkPriorityClassifier {
@NonNull private static final String TAG = NetworkPriorityClassifier.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 other networks (including unvalidated, etc) */
@VisibleForTesting(visibility = Visibility.PRIVATE)
static final int PRIORITY_ANY = Integer.MAX_VALUE;
/** Gives networks a priority class, based on configured VcnGatewayConnectionConfig */
public static int calculatePriorityClass(
VcnContext vcnContext,
UnderlyingNetworkRecord networkRecord,
LinkedHashSet<VcnUnderlyingNetworkPriority> underlyingNetworkPriorities,
ParcelUuid subscriptionGroup,
TelephonySubscriptionSnapshot snapshot,
UnderlyingNetworkRecord currentlySelected,
PersistableBundle carrierConfig) {
// mRouteSelectionNetworkRequest requires a network be both VALIDATED and NOT_SUSPENDED
if (networkRecord.isBlocked) {
logWtf("Network blocked for System Server: " + networkRecord.network);
return PRIORITY_ANY;
}
if (snapshot == null) {
logWtf("Got null snapshot");
return PRIORITY_ANY;
}
int priorityIndex = 0;
for (VcnUnderlyingNetworkPriority nwPriority : underlyingNetworkPriorities) {
if (checkMatchesPriorityRule(
vcnContext,
nwPriority,
networkRecord,
subscriptionGroup,
snapshot,
currentlySelected,
carrierConfig)) {
return priorityIndex;
}
priorityIndex++;
}
return PRIORITY_ANY;
}
@VisibleForTesting(visibility = Visibility.PRIVATE)
public static boolean checkMatchesPriorityRule(
VcnContext vcnContext,
VcnUnderlyingNetworkPriority networkPriority,
UnderlyingNetworkRecord networkRecord,
ParcelUuid subscriptionGroup,
TelephonySubscriptionSnapshot snapshot,
UnderlyingNetworkRecord currentlySelected,
PersistableBundle carrierConfig) {
// TODO: Check Network Quality reported by metric monitors/probers.
final NetworkCapabilities caps = networkRecord.networkCapabilities;
if (!networkPriority.allowMetered() && !caps.hasCapability(NET_CAPABILITY_NOT_METERED)) {
return false;
}
if (vcnContext.isInTestMode() && caps.hasTransport(TRANSPORT_TEST)) {
return true;
}
if (networkPriority instanceof VcnWifiUnderlyingNetworkPriority) {
return checkMatchesWifiPriorityRule(
(VcnWifiUnderlyingNetworkPriority) networkPriority,
networkRecord,
currentlySelected,
carrierConfig);
}
if (networkPriority instanceof VcnCellUnderlyingNetworkPriority) {
return checkMatchesCellPriorityRule(
vcnContext,
(VcnCellUnderlyingNetworkPriority) networkPriority,
networkRecord,
subscriptionGroup,
snapshot);
}
logWtf(
"Got unknown VcnUnderlyingNetworkPriority class: "
+ networkPriority.getClass().getSimpleName());
return false;
}
@VisibleForTesting(visibility = Visibility.PRIVATE)
public static boolean checkMatchesWifiPriorityRule(
VcnWifiUnderlyingNetworkPriority networkPriority,
UnderlyingNetworkRecord networkRecord,
UnderlyingNetworkRecord currentlySelected,
PersistableBundle carrierConfig) {
final NetworkCapabilities caps = networkRecord.networkCapabilities;
if (!caps.hasTransport(TRANSPORT_WIFI)) {
return false;
}
// TODO: Move the Network Quality check to the network metric monitor framework.
if (networkPriority.getNetworkQuality()
> getWifiQuality(networkRecord, currentlySelected, carrierConfig)) {
return false;
}
if (networkPriority.getSsid() != null && networkPriority.getSsid() != caps.getSsid()) {
return false;
}
return true;
}
private static int getWifiQuality(
UnderlyingNetworkRecord networkRecord,
UnderlyingNetworkRecord currentlySelected,
PersistableBundle carrierConfig) {
final NetworkCapabilities caps = networkRecord.networkCapabilities;
final boolean isSelectedNetwork =
currentlySelected != null
&& networkRecord.network.equals(currentlySelected.network);
if (isSelectedNetwork
&& caps.getSignalStrength() >= getWifiExitRssiThreshold(carrierConfig)) {
return NETWORK_QUALITY_OK;
}
if (caps.getSignalStrength() >= getWifiEntryRssiThreshold(carrierConfig)) {
return NETWORK_QUALITY_OK;
}
return NETWORK_QUALITY_ANY;
}
@VisibleForTesting(visibility = Visibility.PRIVATE)
public static boolean checkMatchesCellPriorityRule(
VcnContext vcnContext,
VcnCellUnderlyingNetworkPriority networkPriority,
UnderlyingNetworkRecord networkRecord,
ParcelUuid subscriptionGroup,
TelephonySubscriptionSnapshot snapshot) {
final NetworkCapabilities caps = networkRecord.networkCapabilities;
if (!caps.hasTransport(TRANSPORT_CELLULAR)) {
return false;
}
final TelephonyNetworkSpecifier telephonyNetworkSpecifier =
((TelephonyNetworkSpecifier) caps.getNetworkSpecifier());
if (telephonyNetworkSpecifier == null) {
logWtf("Got null NetworkSpecifier");
return false;
}
final int subId = telephonyNetworkSpecifier.getSubscriptionId();
final TelephonyManager subIdSpecificTelephonyMgr =
vcnContext
.getContext()
.getSystemService(TelephonyManager.class)
.createForSubscriptionId(subId);
if (!networkPriority.getAllowedPlmnIds().isEmpty()) {
final String plmnId = subIdSpecificTelephonyMgr.getNetworkOperator();
if (!networkPriority.getAllowedPlmnIds().contains(plmnId)) {
return false;
}
}
if (!networkPriority.getAllowedSpecificCarrierIds().isEmpty()) {
final int carrierId = subIdSpecificTelephonyMgr.getSimSpecificCarrierId();
if (!networkPriority.getAllowedSpecificCarrierIds().contains(carrierId)) {
return false;
}
}
if (!networkPriority.allowRoaming() && !caps.hasCapability(NET_CAPABILITY_NOT_ROAMING)) {
return false;
}
if (networkPriority.requireOpportunistic()) {
if (!isOpportunistic(snapshot, caps.getSubscriptionIds())) {
return false;
}
// 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 false;
}
}
return true;
}
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;
}
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;
}
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;
}
private static void logWtf(String msg) {
Slog.wtf(TAG, msg);
LOCAL_LOG.log(TAG + " WTF: " + msg);
}
}