| /* |
| * 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 {} |
| } |