Merge changes I3b769562,I63203188,Ia9396d66,Ifec34fad

* changes:
  Pull VcnNetworkProvider out into a separate class
  Verify carrier privileges for VCN-providing packages
  Add TelephonySubscriptionTracker to VcnMgmtSvc
  Add basic VcnMgmtSvc --> Vcn signals (startup, teardown, NetworkReq)
This commit is contained in:
Benedict Wong
2021-01-08 22:29:40 +00:00
committed by Gerrit Code Review
13 changed files with 931 additions and 107 deletions

View File

@@ -23,6 +23,6 @@ import android.os.ParcelUuid;
* @hide
*/
interface IVcnManagementService {
void setVcnConfig(in ParcelUuid subscriptionGroup, in VcnConfig config);
void setVcnConfig(in ParcelUuid subscriptionGroup, in VcnConfig config, in String opPkgName);
void clearVcnConfig(in ParcelUuid subscriptionGroup);
}

View File

@@ -19,6 +19,7 @@ import static com.android.internal.annotations.VisibleForTesting.Visibility;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
@@ -45,11 +46,17 @@ import java.util.Set;
public final class VcnConfig implements Parcelable {
@NonNull private static final String TAG = VcnConfig.class.getSimpleName();
private static final String PACKAGE_NAME_KEY = "mPackageName";
@NonNull private final String mPackageName;
private static final String GATEWAY_CONNECTION_CONFIGS_KEY = "mGatewayConnectionConfigs";
@NonNull private final Set<VcnGatewayConnectionConfig> mGatewayConnectionConfigs;
private VcnConfig(@NonNull Set<VcnGatewayConnectionConfig> tunnelConfigs) {
mGatewayConnectionConfigs = Collections.unmodifiableSet(tunnelConfigs);
private VcnConfig(
@NonNull String packageName,
@NonNull Set<VcnGatewayConnectionConfig> gatewayConnectionConfigs) {
mPackageName = packageName;
mGatewayConnectionConfigs = Collections.unmodifiableSet(gatewayConnectionConfigs);
validate();
}
@@ -61,6 +68,8 @@ public final class VcnConfig implements Parcelable {
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
public VcnConfig(@NonNull PersistableBundle in) {
mPackageName = in.getString(PACKAGE_NAME_KEY);
final PersistableBundle gatewayConnectionConfigsBundle =
in.getPersistableBundle(GATEWAY_CONNECTION_CONFIGS_KEY);
mGatewayConnectionConfigs =
@@ -72,8 +81,19 @@ public final class VcnConfig implements Parcelable {
}
private void validate() {
Objects.requireNonNull(mPackageName, "packageName was null");
Preconditions.checkCollectionNotEmpty(
mGatewayConnectionConfigs, "gatewayConnectionConfigs");
mGatewayConnectionConfigs, "gatewayConnectionConfigs was empty");
}
/**
* Retrieve the package name of the provisioning app.
*
* @hide
*/
@NonNull
public String getProvisioningPackageName() {
return mPackageName;
}
/** Retrieves the set of configured tunnels. */
@@ -91,6 +111,8 @@ public final class VcnConfig implements Parcelable {
public PersistableBundle toPersistableBundle() {
final PersistableBundle result = new PersistableBundle();
result.putString(PACKAGE_NAME_KEY, mPackageName);
final PersistableBundle gatewayConnectionConfigsBundle =
PersistableBundleUtils.fromList(
new ArrayList<>(mGatewayConnectionConfigs),
@@ -102,7 +124,7 @@ public final class VcnConfig implements Parcelable {
@Override
public int hashCode() {
return Objects.hash(mGatewayConnectionConfigs);
return Objects.hash(mPackageName, mGatewayConnectionConfigs);
}
@Override
@@ -112,7 +134,8 @@ public final class VcnConfig implements Parcelable {
}
final VcnConfig rhs = (VcnConfig) other;
return mGatewayConnectionConfigs.equals(rhs.mGatewayConnectionConfigs);
return mPackageName.equals(rhs.mPackageName)
&& mGatewayConnectionConfigs.equals(rhs.mGatewayConnectionConfigs);
}
// Parcelable methods
@@ -143,9 +166,17 @@ public final class VcnConfig implements Parcelable {
/** This class is used to incrementally build {@link VcnConfig} objects. */
public static class Builder {
@NonNull private final String mPackageName;
@NonNull
private final Set<VcnGatewayConnectionConfig> mGatewayConnectionConfigs = new ArraySet<>();
public Builder(@NonNull Context context) {
Objects.requireNonNull(context, "context was null");
mPackageName = context.getOpPackageName();
}
/**
* Adds a configuration for an individual gateway connection.
*
@@ -168,7 +199,7 @@ public final class VcnConfig implements Parcelable {
*/
@NonNull
public VcnConfig build() {
return new VcnConfig(mGatewayConnectionConfigs);
return new VcnConfig(mPackageName, mGatewayConnectionConfigs);
}
}
}

View File

@@ -101,7 +101,7 @@ public final class VcnManager {
requireNonNull(config, "config was null");
try {
mService.setVcnConfig(subscriptionGroup, config);
mService.setVcnConfig(subscriptionGroup, config, mContext.getOpPackageName());
} catch (ServiceSpecificException e) {
throw new IOException(e);
} catch (RemoteException e) {

View File

@@ -16,13 +16,15 @@
package com.android.server;
import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionTrackerCallback;
import static java.util.Objects.requireNonNull;
import android.annotation.NonNull;
import android.app.AppOpsManager;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkProvider;
import android.net.NetworkRequest;
import android.net.vcn.IVcnManagementService;
import android.net.vcn.VcnConfig;
import android.os.Binder;
@@ -43,6 +45,10 @@ import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;
import com.android.server.vcn.TelephonySubscriptionTracker;
import com.android.server.vcn.Vcn;
import com.android.server.vcn.VcnContext;
import com.android.server.vcn.VcnNetworkProvider;
import com.android.server.vcn.util.PersistableBundleUtils;
import java.io.IOException;
@@ -51,6 +57,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
/**
* VcnManagementService manages Virtual Carrier Network profiles and lifecycles.
@@ -115,6 +122,10 @@ public class VcnManagementService extends IVcnManagementService.Stub {
@VisibleForTesting(visibility = Visibility.PRIVATE)
static final String VCN_CONFIG_FILE = "/data/system/vcn/configs.xml";
// TODO(b/176956496): Directly use CarrierServiceBindHelper.UNBIND_DELAY_MILLIS
@VisibleForTesting(visibility = Visibility.PRIVATE)
static final long CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS = TimeUnit.SECONDS.toMillis(30);
/* Binder context for this service */
@NonNull private final Context mContext;
@NonNull private final Dependencies mDeps;
@@ -122,11 +133,23 @@ public class VcnManagementService extends IVcnManagementService.Stub {
@NonNull private final Looper mLooper;
@NonNull private final Handler mHandler;
@NonNull private final VcnNetworkProvider mNetworkProvider;
@NonNull private final TelephonySubscriptionTrackerCallback mTelephonySubscriptionTrackerCb;
@NonNull private final TelephonySubscriptionTracker mTelephonySubscriptionTracker;
@NonNull private final VcnContext mVcnContext;
@GuardedBy("mLock")
@NonNull
private final Map<ParcelUuid, VcnConfig> mConfigs = new ArrayMap<>();
@GuardedBy("mLock")
@NonNull
private final Map<ParcelUuid, Vcn> mVcns = new ArrayMap<>();
@GuardedBy("mLock")
@NonNull
private TelephonySubscriptionSnapshot mLastSnapshot =
TelephonySubscriptionSnapshot.EMPTY_SNAPSHOT;
@NonNull private final Object mLock = new Object();
@NonNull private final PersistableBundleUtils.LockingReadWriteHelper mConfigDiskRwHelper;
@@ -139,8 +162,12 @@ public class VcnManagementService extends IVcnManagementService.Stub {
mLooper = mDeps.getLooper();
mHandler = new Handler(mLooper);
mNetworkProvider = new VcnNetworkProvider(mContext, mLooper);
mTelephonySubscriptionTrackerCb = new VcnSubscriptionTrackerCallback();
mTelephonySubscriptionTracker = mDeps.newTelephonySubscriptionTracker(
mContext, mLooper, mTelephonySubscriptionTrackerCb);
mConfigDiskRwHelper = mDeps.newPersistableBundleLockingReadWriteHelper(VCN_CONFIG_FILE);
mVcnContext = mDeps.newVcnContext(mContext, mLooper, mNetworkProvider);
// Run on handler to ensure I/O does not block system server startup
mHandler.post(() -> {
@@ -174,7 +201,10 @@ public class VcnManagementService extends IVcnManagementService.Stub {
mConfigs.put(entry.getKey(), entry.getValue());
}
}
// TODO: Trigger re-evaluation of active VCNs; start/stop VCNs as needed.
// Re-evaluate subscriptions, and start/stop VCNs. This starts with an empty
// snapshot, and therefore safe even before telephony subscriptions are loaded.
mTelephonySubscriptionTrackerCb.onNewSnapshot(mLastSnapshot);
}
}
});
@@ -203,6 +233,14 @@ public class VcnManagementService extends IVcnManagementService.Stub {
return mHandlerThread.getLooper();
}
/** Creates a new VcnInstance using the provided configuration */
public TelephonySubscriptionTracker newTelephonySubscriptionTracker(
@NonNull Context context,
@NonNull Looper looper,
@NonNull TelephonySubscriptionTrackerCallback callback) {
return new TelephonySubscriptionTracker(context, new Handler(looper), callback);
}
/**
* Retrieves the caller's UID
*
@@ -225,12 +263,29 @@ public class VcnManagementService extends IVcnManagementService.Stub {
newPersistableBundleLockingReadWriteHelper(@NonNull String path) {
return new PersistableBundleUtils.LockingReadWriteHelper(path);
}
/** Creates a new VcnContext */
public VcnContext newVcnContext(
@NonNull Context context,
@NonNull Looper looper,
@NonNull VcnNetworkProvider vcnNetworkProvider) {
return new VcnContext(context, looper, vcnNetworkProvider);
}
/** Creates a new Vcn instance using the provided configuration */
public Vcn newVcn(
@NonNull VcnContext vcnContext,
@NonNull ParcelUuid subscriptionGroup,
@NonNull VcnConfig config) {
return new Vcn(vcnContext, subscriptionGroup, config);
}
}
/** Notifies the VcnManagementService that external dependencies can be set up. */
public void systemReady() {
mContext.getSystemService(ConnectivityManager.class)
.registerNetworkProvider(mNetworkProvider);
mTelephonySubscriptionTracker.register();
}
private void enforcePrimaryUser() {
@@ -277,27 +332,112 @@ public class VcnManagementService extends IVcnManagementService.Stub {
"Carrier privilege required for subscription group to set VCN Config");
}
private class VcnSubscriptionTrackerCallback implements TelephonySubscriptionTrackerCallback {
/**
* Handles subscription group changes, as notified by {@link TelephonySubscriptionTracker}
*
* <p>Start any unstarted VCN instances
*
* @hide
*/
public void onNewSnapshot(@NonNull TelephonySubscriptionSnapshot snapshot) {
// Startup VCN instances
synchronized (mLock) {
mLastSnapshot = snapshot;
// Start any VCN instances as necessary
for (Entry<ParcelUuid, VcnConfig> entry : mConfigs.entrySet()) {
if (snapshot.packageHasPermissionsForSubscriptionGroup(
entry.getKey(), entry.getValue().getProvisioningPackageName())) {
if (!mVcns.containsKey(entry.getKey())) {
startVcnLocked(entry.getKey(), entry.getValue());
}
// Cancel any scheduled teardowns for active subscriptions
mHandler.removeCallbacksAndMessages(mVcns.get(entry.getKey()));
}
}
// Schedule teardown of any VCN instances that have lost carrier privileges (after a
// delay)
for (Entry<ParcelUuid, Vcn> entry : mVcns.entrySet()) {
final VcnConfig config = mConfigs.get(entry.getKey());
if (config == null
|| !snapshot.packageHasPermissionsForSubscriptionGroup(
entry.getKey(), config.getProvisioningPackageName())) {
final ParcelUuid uuidToTeardown = entry.getKey();
final Vcn instanceToTeardown = entry.getValue();
mHandler.postDelayed(() -> {
synchronized (mLock) {
// Guard against case where this is run after a old instance was
// torn down, and a new instance was started. Verify to ensure
// correct instance is torn down. This could happen as a result of a
// Carrier App manually removing/adding a VcnConfig.
if (mVcns.get(uuidToTeardown) == instanceToTeardown) {
mVcns.remove(uuidToTeardown).teardownAsynchronously();
}
}
}, instanceToTeardown, CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS);
}
}
}
}
}
@GuardedBy("mLock")
private void startVcnLocked(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) {
Slog.v(TAG, "Starting VCN config for subGrp: " + subscriptionGroup);
// TODO(b/176939047): Support multiple VCNs active at the same time, or limit to one active
// VCN.
final Vcn newInstance = mDeps.newVcn(mVcnContext, subscriptionGroup, config);
mVcns.put(subscriptionGroup, newInstance);
}
@GuardedBy("mLock")
private void startOrUpdateVcnLocked(
@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) {
Slog.v(TAG, "Starting or updating VCN config for subGrp: " + subscriptionGroup);
if (mVcns.containsKey(subscriptionGroup)) {
mVcns.get(subscriptionGroup).updateConfig(config);
} else {
startVcnLocked(subscriptionGroup, config);
}
}
/**
* Sets a VCN config for a given subscription group.
*
* <p>Implements the IVcnManagementService Binder interface.
*/
@Override
public void setVcnConfig(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) {
public void setVcnConfig(
@NonNull ParcelUuid subscriptionGroup,
@NonNull VcnConfig config,
@NonNull String opPkgName) {
requireNonNull(subscriptionGroup, "subscriptionGroup was null");
requireNonNull(config, "config was null");
requireNonNull(opPkgName, "opPkgName was null");
if (!config.getProvisioningPackageName().equals(opPkgName)) {
throw new IllegalArgumentException("Mismatched caller and VcnConfig creator");
}
Slog.v(TAG, "VCN config updated for subGrp: " + subscriptionGroup);
mContext.getSystemService(AppOpsManager.class)
.checkPackage(mDeps.getBinderCallingUid(), config.getProvisioningPackageName());
enforceCallingUserAndCarrierPrivilege(subscriptionGroup);
synchronized (mLock) {
mConfigs.put(subscriptionGroup, config);
Binder.withCleanCallingIdentity(() -> {
synchronized (mLock) {
mConfigs.put(subscriptionGroup, config);
startOrUpdateVcnLocked(subscriptionGroup, config);
// Must be done synchronously to ensure that writes do not happen out-of-order.
writeConfigsToDiskLocked();
}
// TODO: Clear Binder calling identity
// TODO: Trigger startup as necessary
writeConfigsToDiskLocked();
}
});
}
/**
@@ -308,18 +448,21 @@ public class VcnManagementService extends IVcnManagementService.Stub {
@Override
public void clearVcnConfig(@NonNull ParcelUuid subscriptionGroup) {
requireNonNull(subscriptionGroup, "subscriptionGroup was null");
Slog.v(TAG, "VCN config cleared for subGrp: " + subscriptionGroup);
enforceCallingUserAndCarrierPrivilege(subscriptionGroup);
synchronized (mLock) {
mConfigs.remove(subscriptionGroup);
Binder.withCleanCallingIdentity(() -> {
synchronized (mLock) {
mConfigs.remove(subscriptionGroup);
// Must be done synchronously to ensure that writes do not happen out-of-order.
writeConfigsToDiskLocked();
}
if (mVcns.containsKey(subscriptionGroup)) {
mVcns.remove(subscriptionGroup).teardownAsynchronously();
}
// TODO: Clear Binder calling identity
// TODO: Trigger teardown as necessary
writeConfigsToDiskLocked();
}
});
}
@GuardedBy("mLock")
@@ -345,19 +488,11 @@ public class VcnManagementService extends IVcnManagementService.Stub {
}
}
/**
* Network provider for VCN networks.
*
* @hide
*/
public class VcnNetworkProvider extends NetworkProvider {
VcnNetworkProvider(Context context, Looper looper) {
super(context, looper, VcnNetworkProvider.class.getSimpleName());
}
@Override
public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) {
// TODO: Handle network requests - Ensure VCN started, and start appropriate tunnels.
/** Get current configuration list for testing purposes */
@VisibleForTesting(visibility = Visibility.PRIVATE)
public Map<ParcelUuid, Vcn> getAllVcns() {
synchronized (mLock) {
return Collections.unmodifiableMap(mVcns);
}
}
}

View File

@@ -36,6 +36,8 @@ import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
import android.telephony.TelephonyManager;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
@@ -79,6 +81,7 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
@NonNull private final TelephonySubscriptionTrackerCallback mCallback;
@NonNull private final Dependencies mDeps;
@NonNull private final TelephonyManager mTelephonyManager;
@NonNull private final SubscriptionManager mSubscriptionManager;
@NonNull private final CarrierConfigManager mCarrierConfigManager;
@@ -106,6 +109,7 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
mCallback = Objects.requireNonNull(callback, "Missing callback");
mDeps = Objects.requireNonNull(deps, "Missing deps");
mTelephonyManager = mContext.getSystemService(TelephonyManager.class);
mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class);
mCarrierConfigManager = mContext.getSystemService(CarrierConfigManager.class);
@@ -139,7 +143,7 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
* so callbacks & broadcasts are all serialized on mHandler, avoiding the need for locking.
*/
public void handleSubscriptionsChanged() {
final Set<ParcelUuid> activeSubGroups = new ArraySet<>();
final Map<ParcelUuid, Set<String>> privilegedPackages = new HashMap<>();
final Map<Integer, ParcelUuid> newSubIdToGroupMap = new HashMap<>();
final List<SubscriptionInfo> allSubs = mSubscriptionManager.getAllSubscriptionInfoList();
@@ -166,12 +170,22 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
// group.
if (subInfo.getSimSlotIndex() != INVALID_SIM_SLOT_INDEX
&& mReadySubIdsBySlotId.values().contains(subInfo.getSubscriptionId())) {
activeSubGroups.add(subInfo.getGroupUuid());
// TODO (b/172619301): Cache based on callbacks from CarrierPrivilegesTracker
final TelephonyManager subIdSpecificTelephonyManager =
mTelephonyManager.createForSubscriptionId(subInfo.getSubscriptionId());
final ParcelUuid subGroup = subInfo.getGroupUuid();
final Set<String> pkgs =
privilegedPackages.getOrDefault(subGroup, new ArraySet<>());
pkgs.addAll(subIdSpecificTelephonyManager.getPackagesWithCarrierPrivileges());
privilegedPackages.put(subGroup, pkgs);
}
}
final TelephonySubscriptionSnapshot newSnapshot =
new TelephonySubscriptionSnapshot(newSubIdToGroupMap, activeSubGroups);
new TelephonySubscriptionSnapshot(newSubIdToGroupMap, privilegedPackages);
// If snapshot was meaningfully updated, fire the callback
if (!newSnapshot.equals(mCurrentSnapshot)) {
@@ -231,22 +245,40 @@ 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 Set<ParcelUuid> mActiveGroups;
private final Map<ParcelUuid, Set<String>> mPrivilegedPackages;
public static final TelephonySubscriptionSnapshot EMPTY_SNAPSHOT =
new TelephonySubscriptionSnapshot(Collections.emptyMap(), Collections.emptyMap());
@VisibleForTesting(visibility = Visibility.PRIVATE)
TelephonySubscriptionSnapshot(
@NonNull Map<Integer, ParcelUuid> subIdToGroupMap,
@NonNull Set<ParcelUuid> activeGroups) {
mSubIdToGroupMap = Collections.unmodifiableMap(
Objects.requireNonNull(subIdToGroupMap, "subIdToGroupMap was null"));
mActiveGroups = Collections.unmodifiableSet(
Objects.requireNonNull(activeGroups, "activeGroups was null"));
@NonNull Map<ParcelUuid, Set<String>> privilegedPackages) {
Objects.requireNonNull(subIdToGroupMap, "subIdToGroupMap was null");
Objects.requireNonNull(privilegedPackages, "privilegedPackages was null");
mSubIdToGroupMap = Collections.unmodifiableMap(subIdToGroupMap);
final Map<ParcelUuid, Set<String>> unmodifiableInnerSets = new ArrayMap<>();
for (Entry<ParcelUuid, Set<String>> entry : privilegedPackages.entrySet()) {
unmodifiableInnerSets.put(
entry.getKey(), Collections.unmodifiableSet(entry.getValue()));
}
mPrivilegedPackages = Collections.unmodifiableMap(unmodifiableInnerSets);
}
/** Returns the active subscription groups */
@NonNull
public Set<ParcelUuid> getActiveSubscriptionGroups() {
return mActiveGroups;
return mPrivilegedPackages.keySet();
}
/** Checks if the provided package is carrier privileged for the specified sub group. */
public boolean packageHasPermissionsForSubscriptionGroup(
@NonNull ParcelUuid subGrp, @NonNull String packageName) {
final Set<String> privilegedPackages = mPrivilegedPackages.get(subGrp);
return privilegedPackages != null && privilegedPackages.contains(packageName);
}
/** Returns the Subscription Group for a given subId. */
@@ -273,7 +305,7 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
@Override
public int hashCode() {
return Objects.hash(mSubIdToGroupMap, mActiveGroups);
return Objects.hash(mSubIdToGroupMap, mPrivilegedPackages);
}
@Override
@@ -285,7 +317,15 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver {
final TelephonySubscriptionSnapshot other = (TelephonySubscriptionSnapshot) obj;
return mSubIdToGroupMap.equals(other.mSubIdToGroupMap)
&& mActiveGroups.equals(other.mActiveGroups);
&& mPrivilegedPackages.equals(other.mPrivilegedPackages);
}
@Override
public String toString() {
return "TelephonySubscriptionSnapshot{ "
+ "mSubIdToGroupMap=" + mSubIdToGroupMap
+ ", mPrivilegedPackages=" + mPrivilegedPackages
+ " }";
}
}

View File

@@ -16,32 +16,69 @@
package com.android.server.vcn;
import android.annotation.NonNull;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.vcn.VcnConfig;
import android.net.vcn.VcnGatewayConnectionConfig;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelUuid;
import android.util.Slog;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* Represents an single instance of a VCN.
*
* <p>Each Vcn instance manages all tunnels for a given subscription group, including per-capability
* networks, network selection, and multi-homing.
* <p>Each Vcn instance manages all {@link VcnGatewayConnection}(s) for a given subscription group,
* including per-capability networks, network selection, and multi-homing.
*
* @hide
*/
public class Vcn extends Handler {
private static final String TAG = Vcn.class.getSimpleName();
private static final int MSG_EVENT_BASE = 0;
private static final int MSG_CMD_BASE = 100;
/**
* A carrier app updated the configuration.
*
* <p>Triggers update of config, re-evaluating all active and underlying networks.
*
* @param obj VcnConfig
*/
private static final int MSG_EVENT_CONFIG_UPDATED = MSG_EVENT_BASE;
/**
* A NetworkRequest was added or updated.
*
* <p>Triggers an evaluation of all active networks, bringing up a new one if necessary.
*
* @param obj NetworkRequest
*/
private static final int MSG_EVENT_NETWORK_REQUESTED = MSG_EVENT_BASE + 1;
/** Triggers an immediate teardown of the entire Vcn, including GatewayConnections. */
private static final int MSG_CMD_TEARDOWN = MSG_CMD_BASE;
@NonNull private final VcnContext mVcnContext;
@NonNull private final ParcelUuid mSubscriptionGroup;
@NonNull private final Dependencies mDeps;
@NonNull private final VcnNetworkRequestListener mRequestListener;
@NonNull
private final Map<VcnGatewayConnectionConfig, VcnGatewayConnection> mVcnGatewayConnections =
new HashMap<>();
@NonNull private VcnConfig mConfig;
private boolean mIsRunning = true;
public Vcn(
@NonNull VcnContext vcnContext,
@NonNull ParcelUuid subscriptionGroup,
@@ -58,31 +95,123 @@ public class Vcn extends Handler {
mVcnContext = vcnContext;
mSubscriptionGroup = Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup");
mDeps = Objects.requireNonNull(deps, "Missing deps");
mRequestListener = new VcnNetworkRequestListener();
mConfig = Objects.requireNonNull(config, "Missing config");
// Register to receive cached and future NetworkRequests
mVcnContext.getVcnNetworkProvider().registerListener(mRequestListener);
}
/** Asynchronously updates the configuration and triggers a re-evaluation of Networks */
public void updateConfig(@NonNull VcnConfig config) {
Objects.requireNonNull(config, "Missing config");
// TODO: Proxy to handler, and make config there.
sendMessage(obtainMessage(MSG_EVENT_CONFIG_UPDATED, config));
}
/** Asynchronously tears down this Vcn instance, along with all tunnels and Networks */
public void teardown() {
// TODO: Proxy to handler, and teardown there.
/** Asynchronously tears down this Vcn instance, including VcnGatewayConnection(s) */
public void teardownAsynchronously() {
sendMessageAtFrontOfQueue(obtainMessage(MSG_CMD_TEARDOWN));
}
/** Notifies this Vcn instance of a new NetworkRequest */
public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) {
Objects.requireNonNull(request, "Missing request");
private class VcnNetworkRequestListener implements VcnNetworkProvider.NetworkRequestListener {
@Override
public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) {
Objects.requireNonNull(request, "Missing request");
// TODO: Proxy to handler, and handle there.
sendMessage(obtainMessage(MSG_EVENT_NETWORK_REQUESTED, score, providerId, request));
}
}
@Override
public void handleMessage(@NonNull Message msg) {
// TODO: Do something
if (!mIsRunning) {
return;
}
switch (msg.what) {
case MSG_EVENT_CONFIG_UPDATED:
handleConfigUpdated((VcnConfig) msg.obj);
break;
case MSG_EVENT_NETWORK_REQUESTED:
handleNetworkRequested((NetworkRequest) msg.obj, msg.arg1, msg.arg2);
break;
case MSG_CMD_TEARDOWN:
handleTeardown();
break;
default:
Slog.wtf(getLogTag(), "Unknown msg.what: " + msg.what);
}
}
private void handleConfigUpdated(@NonNull VcnConfig config) {
// TODO: Add a dump function in VcnConfig that omits PII. Until then, use hashCode()
Slog.v(getLogTag(), String.format("Config updated: config = %s", config.hashCode()));
mConfig = config;
// TODO: Reevaluate active VcnGatewayConnection(s)
}
private void handleTeardown() {
mVcnContext.getVcnNetworkProvider().unregisterListener(mRequestListener);
for (VcnGatewayConnection gatewayConnection : mVcnGatewayConnections.values()) {
gatewayConnection.teardownAsynchronously();
}
mIsRunning = false;
}
private void handleNetworkRequested(
@NonNull NetworkRequest request, int score, int providerId) {
if (score > getNetworkScore()) {
Slog.v(getLogTag(),
"Request " + request.requestId + " already satisfied by higher-scoring ("
+ score + ") network from provider " + providerId);
return;
}
// If preexisting VcnGatewayConnection(s) satisfy request, return
for (VcnGatewayConnectionConfig gatewayConnectionConfig : mVcnGatewayConnections.keySet()) {
if (requestSatisfiedByGatewayConnectionConfig(request, gatewayConnectionConfig)) {
Slog.v(getLogTag(),
"Request " + request.requestId
+ " satisfied by existing VcnGatewayConnection");
return;
}
}
// If any supported (but not running) VcnGatewayConnection(s) can satisfy request, bring it
// up
for (VcnGatewayConnectionConfig gatewayConnectionConfig :
mConfig.getGatewayConnectionConfigs()) {
if (requestSatisfiedByGatewayConnectionConfig(request, gatewayConnectionConfig)) {
Slog.v(
getLogTag(),
"Bringing up new VcnGatewayConnection for request " + request.requestId);
final VcnGatewayConnection vcnGatewayConnection =
new VcnGatewayConnection(
mVcnContext, mSubscriptionGroup, gatewayConnectionConfig);
mVcnGatewayConnections.put(gatewayConnectionConfig, vcnGatewayConnection);
}
}
}
private boolean requestSatisfiedByGatewayConnectionConfig(
@NonNull NetworkRequest request, @NonNull VcnGatewayConnectionConfig config) {
final NetworkCapabilities configCaps = new NetworkCapabilities();
for (int cap : config.getAllExposedCapabilities()) {
configCaps.addCapability(cap);
}
return request.networkCapabilities.satisfiedByNetworkCapabilities(configCaps);
}
private String getLogTag() {
return String.format("%s [%d]", TAG, mSubscriptionGroup.hashCode());
}
/** Retrieves the network score for a VCN Network */

View File

@@ -20,8 +20,6 @@ import android.annotation.NonNull;
import android.content.Context;
import android.os.Looper;
import com.android.server.VcnManagementService.VcnNetworkProvider;
import java.util.Objects;
/**

View File

@@ -65,8 +65,8 @@ public class VcnGatewayConnection extends Handler implements UnderlyingNetworkTr
mDeps.newUnderlyingNetworkTracker(mVcnContext, subscriptionGroup, this);
}
/** Tears down this GatewayConnection, and any resources used */
public void teardown() {
/** Asynchronously tears down this GatewayConnection, and any resources used */
public void teardownAsynchronously() {
mUnderlyingNetworkTracker.teardown();
}

View File

@@ -0,0 +1,108 @@
/*
* 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 android.annotation.NonNull;
import android.content.Context;
import android.net.NetworkProvider;
import android.net.NetworkRequest;
import android.os.Looper;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;
import java.util.Objects;
import java.util.Set;
/**
* VCN Network Provider routes NetworkRequests to listeners to bring up tunnels as needed.
*
* <p>The VcnNetworkProvider provides a caching layer to ensure that all listeners receive all
* active NetworkRequest(s), including ones that were filed prior to listener registration.
*
* @hide
*/
public class VcnNetworkProvider extends NetworkProvider {
private static final String TAG = VcnNetworkProvider.class.getSimpleName();
private final Set<NetworkRequestListener> mListeners = new ArraySet<>();
private final SparseArray<NetworkRequestEntry> mRequests = new SparseArray<>();
public VcnNetworkProvider(Context context, Looper looper) {
super(context, looper, VcnNetworkProvider.class.getSimpleName());
}
// Package-private
void registerListener(@NonNull NetworkRequestListener listener) {
mListeners.add(listener);
// Send listener all cached requests
for (int i = 0; i < mRequests.size(); i++) {
notifyListenerForEvent(listener, mRequests.valueAt(i));
}
}
// Package-private
void unregisterListener(@NonNull NetworkRequestListener listener) {
mListeners.remove(listener);
}
private void notifyListenerForEvent(
@NonNull NetworkRequestListener listener, @NonNull NetworkRequestEntry entry) {
listener.onNetworkRequested(entry.mRequest, entry.mScore, entry.mProviderId);
}
@Override
public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) {
Slog.v(
TAG,
String.format(
"Network requested: Request = %s, score = %d, providerId = %d",
request, score, providerId));
final NetworkRequestEntry entry = new NetworkRequestEntry(request, score, providerId);
mRequests.put(request.requestId, entry);
// TODO(b/176939047): Intelligently route requests to prioritized VcnInstances (based on
// Default Data Sub, or similar)
for (NetworkRequestListener listener : mListeners) {
notifyListenerForEvent(listener, entry);
}
}
@Override
public void onNetworkRequestWithdrawn(@NonNull NetworkRequest request) {
mRequests.remove(request.requestId);
}
private static class NetworkRequestEntry {
public final NetworkRequest mRequest;
public final int mScore;
public final int mProviderId;
private NetworkRequestEntry(@NonNull NetworkRequest request, int score, int providerId) {
mRequest = Objects.requireNonNull(request, "Missing request");
mScore = score;
mProviderId = providerId;
}
}
// package-private
interface NetworkRequestListener {
void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId);
}
}

View File

@@ -18,12 +18,17 @@ package android.net.vcn;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import android.annotation.NonNull;
import android.content.Context;
import android.os.Parcel;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -33,12 +38,15 @@ import java.util.Set;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class VcnConfigTest {
private static final String TEST_PACKAGE_NAME = VcnConfigTest.class.getPackage().getName();
private static final Set<VcnGatewayConnectionConfig> GATEWAY_CONNECTION_CONFIGS =
Collections.singleton(VcnGatewayConnectionConfigTest.buildTestConfig());
private final Context mContext = mock(Context.class);
// Public visibility for VcnManagementServiceTest
public static VcnConfig buildTestConfig() {
VcnConfig.Builder builder = new VcnConfig.Builder();
public static VcnConfig buildTestConfig(@NonNull Context context) {
VcnConfig.Builder builder = new VcnConfig.Builder(context);
for (VcnGatewayConnectionConfig gatewayConnectionConfig : GATEWAY_CONNECTION_CONFIGS) {
builder.addGatewayConnectionConfig(gatewayConnectionConfig);
@@ -47,10 +55,24 @@ public class VcnConfigTest {
return builder.build();
}
@Before
public void setUp() throws Exception {
doReturn(TEST_PACKAGE_NAME).when(mContext).getOpPackageName();
}
@Test
public void testBuilderConstructorRequiresContext() {
try {
new VcnConfig.Builder(null);
fail("Expected exception due to null context");
} catch (NullPointerException e) {
}
}
@Test
public void testBuilderRequiresGatewayConnectionConfig() {
try {
new VcnConfig.Builder().build();
new VcnConfig.Builder(mContext).build();
fail("Expected exception due to no VcnGatewayConnectionConfigs provided");
} catch (IllegalArgumentException e) {
}
@@ -58,21 +80,22 @@ public class VcnConfigTest {
@Test
public void testBuilderAndGetters() {
final VcnConfig config = buildTestConfig();
final VcnConfig config = buildTestConfig(mContext);
assertEquals(TEST_PACKAGE_NAME, config.getProvisioningPackageName());
assertEquals(GATEWAY_CONNECTION_CONFIGS, config.getGatewayConnectionConfigs());
}
@Test
public void testPersistableBundle() {
final VcnConfig config = buildTestConfig();
final VcnConfig config = buildTestConfig(mContext);
assertEquals(config, new VcnConfig(config.toPersistableBundle()));
}
@Test
public void testParceling() {
final VcnConfig config = buildTestConfig();
final VcnConfig config = buildTestConfig(mContext);
Parcel parcel = Parcel.obtain();
config.writeToParcel(parcel, 0);

View File

@@ -16,16 +16,23 @@
package com.android.server;
import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot;
import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionTrackerCallback;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.app.AppOpsManager;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.vcn.VcnConfig;
@@ -42,29 +49,47 @@ import android.telephony.TelephonyManager;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.vcn.TelephonySubscriptionTracker;
import com.android.server.vcn.Vcn;
import com.android.server.vcn.VcnContext;
import com.android.server.vcn.VcnNetworkProvider;
import com.android.server.vcn.util.PersistableBundleUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import java.io.FileNotFoundException;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/** Tests for {@link VcnManagementService}. */
@RunWith(AndroidJUnit4.class)
@SmallTest
public class VcnManagementServiceTest {
private static final String TEST_PACKAGE_NAME =
VcnManagementServiceTest.class.getPackage().getName();
private static final ParcelUuid TEST_UUID_1 = new ParcelUuid(new UUID(0, 0));
private static final ParcelUuid TEST_UUID_2 = new ParcelUuid(new UUID(1, 1));
private static final VcnConfig TEST_VCN_CONFIG = VcnConfigTest.buildTestConfig();
private static final VcnConfig TEST_VCN_CONFIG;
private static final int TEST_UID = Process.FIRST_APPLICATION_UID;
static {
final Context mockConfigContext = mock(Context.class);
doReturn(TEST_PACKAGE_NAME).when(mockConfigContext).getOpPackageName();
TEST_VCN_CONFIG = VcnConfigTest.buildTestConfig(mockConfigContext);
}
private static final Map<ParcelUuid, VcnConfig> TEST_VCN_CONFIG_MAP =
Collections.unmodifiableMap(Collections.singletonMap(TEST_UUID_1, TEST_VCN_CONFIG));
private static final int TEST_SUBSCRIPTION_ID = 1;
private static final SubscriptionInfo TEST_SUBSCRIPTION_INFO =
new SubscriptionInfo(
1 /* id */,
TEST_SUBSCRIPTION_ID /* id */,
"" /* iccId */,
0 /* simSlotIndex */,
"Carrier" /* displayName */,
@@ -92,22 +117,48 @@ public class VcnManagementServiceTest {
private final ConnectivityManager mConnMgr = mock(ConnectivityManager.class);
private final TelephonyManager mTelMgr = mock(TelephonyManager.class);
private final SubscriptionManager mSubMgr = mock(SubscriptionManager.class);
private final VcnManagementService mVcnMgmtSvc;
private final AppOpsManager mAppOpsMgr = mock(AppOpsManager.class);
private final VcnContext mVcnContext = mock(VcnContext.class);
private final PersistableBundleUtils.LockingReadWriteHelper mConfigReadWriteHelper =
mock(PersistableBundleUtils.LockingReadWriteHelper.class);
private final TelephonySubscriptionTracker mSubscriptionTracker =
mock(TelephonySubscriptionTracker.class);
private final VcnManagementService mVcnMgmtSvc;
public VcnManagementServiceTest() throws Exception {
setupSystemService(mConnMgr, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class);
setupSystemService(mTelMgr, Context.TELEPHONY_SERVICE, TelephonyManager.class);
setupSystemService(
mSubMgr, Context.TELEPHONY_SUBSCRIPTION_SERVICE, SubscriptionManager.class);
setupSystemService(mAppOpsMgr, Context.APP_OPS_SERVICE, AppOpsManager.class);
doReturn(TEST_PACKAGE_NAME).when(mMockContext).getOpPackageName();
doReturn(mTestLooper.getLooper()).when(mMockDeps).getLooper();
doReturn(Process.FIRST_APPLICATION_UID).when(mMockDeps).getBinderCallingUid();
doReturn(TEST_UID).when(mMockDeps).getBinderCallingUid();
doReturn(mVcnContext)
.when(mMockDeps)
.newVcnContext(
eq(mMockContext),
eq(mTestLooper.getLooper()),
any(VcnNetworkProvider.class));
doReturn(mSubscriptionTracker)
.when(mMockDeps)
.newTelephonySubscriptionTracker(
eq(mMockContext),
eq(mTestLooper.getLooper()),
any(TelephonySubscriptionTrackerCallback.class));
doReturn(mConfigReadWriteHelper)
.when(mMockDeps)
.newPersistableBundleLockingReadWriteHelper(any());
// Setup VCN instance generation
doAnswer((invocation) -> {
// Mock-within a doAnswer is safe, because it doesn't actually run nested.
return mock(Vcn.class);
}).when(mMockDeps).newVcn(any(), any(), any());
final PersistableBundle bundle =
PersistableBundleUtils.fromMap(
TEST_VCN_CONFIG_MAP,
@@ -117,6 +168,9 @@ public class VcnManagementServiceTest {
setupMockedCarrierPrivilege(true);
mVcnMgmtSvc = new VcnManagementService(mMockContext, mMockDeps);
// Make sure the profiles are loaded.
mTestLooper.dispatchAll();
}
private void setupSystemService(Object service, String name, Class<?> serviceClass) {
@@ -137,8 +191,8 @@ public class VcnManagementServiceTest {
public void testSystemReady() throws Exception {
mVcnMgmtSvc.systemReady();
verify(mConnMgr)
.registerNetworkProvider(any(VcnManagementService.VcnNetworkProvider.class));
verify(mConnMgr).registerNetworkProvider(any(VcnNetworkProvider.class));
verify(mSubscriptionTracker).register();
}
@Test
@@ -171,12 +225,110 @@ public class VcnManagementServiceTest {
verify(mConfigReadWriteHelper).readFromDisk();
}
private void triggerSubscriptionTrackerCallback(Set<ParcelUuid> activeSubscriptionGroups) {
final TelephonySubscriptionSnapshot snapshot = mock(TelephonySubscriptionSnapshot.class);
doReturn(activeSubscriptionGroups).when(snapshot).getActiveSubscriptionGroups();
final Set<String> privilegedPackages =
(activeSubscriptionGroups == null || activeSubscriptionGroups.isEmpty())
? Collections.emptySet()
: Collections.singleton(TEST_PACKAGE_NAME);
doReturn(true)
.when(snapshot)
.packageHasPermissionsForSubscriptionGroup(
argThat(val -> activeSubscriptionGroups.contains(val)),
eq(TEST_PACKAGE_NAME));
final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback();
cb.onNewSnapshot(snapshot);
}
private TelephonySubscriptionTrackerCallback getTelephonySubscriptionTrackerCallback() {
final ArgumentCaptor<TelephonySubscriptionTrackerCallback> captor =
ArgumentCaptor.forClass(TelephonySubscriptionTrackerCallback.class);
verify(mMockDeps)
.newTelephonySubscriptionTracker(
eq(mMockContext), eq(mTestLooper.getLooper()), captor.capture());
return captor.getValue();
}
private Vcn startAndGetVcnInstance(ParcelUuid uuid) {
mVcnMgmtSvc.setVcnConfig(uuid, TEST_VCN_CONFIG, TEST_PACKAGE_NAME);
return mVcnMgmtSvc.getAllVcns().get(uuid);
}
@Test
public void testTelephonyNetworkTrackerCallbackStartsInstances() throws Exception {
triggerSubscriptionTrackerCallback(Collections.singleton(TEST_UUID_1));
verify(mMockDeps).newVcn(eq(mVcnContext), eq(TEST_UUID_1), eq(TEST_VCN_CONFIG));
}
@Test
public void testTelephonyNetworkTrackerCallbackStopsInstances() throws Exception {
final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback();
final Vcn vcn = startAndGetVcnInstance(TEST_UUID_2);
triggerSubscriptionTrackerCallback(Collections.emptySet());
// Verify teardown after delay
mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS);
mTestLooper.dispatchAll();
verify(vcn).teardownAsynchronously();
}
@Test
public void testTelephonyNetworkTrackerCallbackSimSwitchesDoNotKillVcnInstances()
throws Exception {
final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback();
final Vcn vcn = startAndGetVcnInstance(TEST_UUID_2);
// Simulate SIM unloaded
triggerSubscriptionTrackerCallback(Collections.emptySet());
// Simulate new SIM loaded right during teardown delay.
mTestLooper.moveTimeForward(
VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS / 2);
mTestLooper.dispatchAll();
triggerSubscriptionTrackerCallback(Collections.singleton(TEST_UUID_2));
// Verify that even after the full timeout duration, the VCN instance is not torn down
mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS);
mTestLooper.dispatchAll();
verify(vcn, never()).teardownAsynchronously();
}
@Test
public void testTelephonyNetworkTrackerCallbackDoesNotKillNewVcnInstances() throws Exception {
final TelephonySubscriptionTrackerCallback cb = getTelephonySubscriptionTrackerCallback();
final Vcn oldInstance = startAndGetVcnInstance(TEST_UUID_2);
// Simulate SIM unloaded
triggerSubscriptionTrackerCallback(Collections.emptySet());
// Config cleared, SIM reloaded & config re-added right before teardown delay, staring new
// vcnInstance.
mTestLooper.moveTimeForward(
VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS / 2);
mTestLooper.dispatchAll();
mVcnMgmtSvc.clearVcnConfig(TEST_UUID_2);
final Vcn newInstance = startAndGetVcnInstance(TEST_UUID_2);
// Verify that new instance was different, and the old one was torn down
assertTrue(oldInstance != newInstance);
verify(oldInstance).teardownAsynchronously();
// Verify that even after the full timeout duration, the new VCN instance is not torn down
mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS);
mTestLooper.dispatchAll();
verify(newInstance, never()).teardownAsynchronously();
}
@Test
public void testSetVcnConfigRequiresNonSystemServer() throws Exception {
doReturn(Process.SYSTEM_UID).when(mMockDeps).getBinderCallingUid();
try {
mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, VcnConfigTest.buildTestConfig());
mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, TEST_VCN_CONFIG, TEST_PACKAGE_NAME);
fail("Expected IllegalStateException exception for system server");
} catch (IllegalStateException expected) {
}
@@ -184,12 +336,12 @@ public class VcnManagementServiceTest {
@Test
public void testSetVcnConfigRequiresSystemUser() throws Exception {
doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, Process.FIRST_APPLICATION_UID))
doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, TEST_UID))
.when(mMockDeps)
.getBinderCallingUid();
try {
mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, VcnConfigTest.buildTestConfig());
mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, TEST_VCN_CONFIG, TEST_PACKAGE_NAME);
fail("Expected security exception for non system user");
} catch (SecurityException expected) {
}
@@ -200,16 +352,25 @@ public class VcnManagementServiceTest {
setupMockedCarrierPrivilege(false);
try {
mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, VcnConfigTest.buildTestConfig());
mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, TEST_VCN_CONFIG, TEST_PACKAGE_NAME);
fail("Expected security exception for missing carrier privileges");
} catch (SecurityException expected) {
}
}
@Test
public void testSetVcnConfigMismatchedPackages() throws Exception {
try {
mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, TEST_VCN_CONFIG, "IncorrectPackage");
fail("Expected exception due to mismatched packages in config and method call");
} catch (IllegalArgumentException expected) {
}
}
@Test
public void testSetVcnConfig() throws Exception {
// Use a different UUID to simulate a new VCN config.
mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG);
mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG, TEST_PACKAGE_NAME);
assertEquals(TEST_VCN_CONFIG, mVcnMgmtSvc.getConfigs().get(TEST_UUID_2));
verify(mConfigReadWriteHelper).writeToDisk(any(PersistableBundle.class));
}
@@ -227,7 +388,7 @@ public class VcnManagementServiceTest {
@Test
public void testClearVcnConfigRequiresSystemUser() throws Exception {
doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, Process.FIRST_APPLICATION_UID))
doReturn(UserHandle.getUid(UserHandle.MIN_SECONDARY_USER_ID, TEST_UID))
.when(mMockDeps)
.getBinderCallingUid();
@@ -255,4 +416,26 @@ public class VcnManagementServiceTest {
assertTrue(mVcnMgmtSvc.getConfigs().isEmpty());
verify(mConfigReadWriteHelper).writeToDisk(any(PersistableBundle.class));
}
@Test
public void testSetVcnConfigClearVcnConfigStartsUpdatesAndTeardsDownVcns() throws Exception {
// Use a different UUID to simulate a new VCN config.
mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG, TEST_PACKAGE_NAME);
final Map<ParcelUuid, Vcn> vcnInstances = mVcnMgmtSvc.getAllVcns();
final Vcn vcnInstance = vcnInstances.get(TEST_UUID_2);
assertEquals(1, vcnInstances.size());
assertEquals(TEST_VCN_CONFIG, mVcnMgmtSvc.getConfigs().get(TEST_UUID_2));
verify(mConfigReadWriteHelper).writeToDisk(any(PersistableBundle.class));
// Verify Vcn is started
verify(mMockDeps).newVcn(eq(mVcnContext), eq(TEST_UUID_2), eq(TEST_VCN_CONFIG));
// Verify Vcn is updated if it was previously started
mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG, TEST_PACKAGE_NAME);
verify(vcnInstance).updateConfig(TEST_VCN_CONFIG);
// Verify Vcn is stopped if it was already started
mVcnMgmtSvc.clearVcnConfig(TEST_UUID_2);
verify(vcnInstance).teardownAsynchronously();
}
}

View File

@@ -30,6 +30,7 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
@@ -37,6 +38,10 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static java.util.Collections.emptyMap;
import static java.util.Collections.emptySet;
import static java.util.Collections.singletonMap;
import android.annotation.NonNull;
import android.content.Context;
import android.content.Intent;
@@ -49,6 +54,7 @@ import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
import android.telephony.TelephonyManager;
import android.util.ArraySet;
import androidx.test.filters.SmallTest;
@@ -63,6 +69,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@@ -71,12 +78,16 @@ import java.util.UUID;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class TelephonySubscriptionTrackerTest {
private static final String PACKAGE_NAME =
TelephonySubscriptionTrackerTest.class.getPackage().getName();
private static final ParcelUuid TEST_PARCEL_UUID = new ParcelUuid(UUID.randomUUID());
private static final int TEST_SIM_SLOT_INDEX = 1;
private static final int TEST_SUBSCRIPTION_ID_1 = 2;
private static final SubscriptionInfo TEST_SUBINFO_1 = mock(SubscriptionInfo.class);
private static final int TEST_SUBSCRIPTION_ID_2 = 3;
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;
static {
@@ -91,6 +102,7 @@ public class TelephonySubscriptionTrackerTest {
@NonNull private final Handler mHandler;
@NonNull private final TelephonySubscriptionTracker.Dependencies mDeps;
@NonNull private final TelephonyManager mTelephonyManager;
@NonNull private final SubscriptionManager mSubscriptionManager;
@NonNull private final CarrierConfigManager mCarrierConfigManager;
@@ -103,9 +115,15 @@ public class TelephonySubscriptionTrackerTest {
mHandler = new Handler(mTestLooper.getLooper());
mDeps = mock(TelephonySubscriptionTracker.Dependencies.class);
mTelephonyManager = mock(TelephonyManager.class);
mSubscriptionManager = mock(SubscriptionManager.class);
mCarrierConfigManager = mock(CarrierConfigManager.class);
doReturn(Context.TELEPHONY_SERVICE)
.when(mContext)
.getSystemServiceName(TelephonyManager.class);
doReturn(mTelephonyManager).when(mContext).getSystemService(Context.TELEPHONY_SERVICE);
doReturn(Context.TELEPHONY_SUBSCRIPTION_SERVICE)
.when(mContext)
.getSystemServiceName(SubscriptionManager.class);
@@ -140,6 +158,9 @@ public class TelephonySubscriptionTrackerTest {
doReturn(Arrays.asList(TEST_SUBINFO_1, TEST_SUBINFO_2))
.when(mSubscriptionManager)
.getAllSubscriptionInfoList();
doReturn(mTelephonyManager).when(mTelephonyManager).createForSubscriptionId(anyInt());
setPrivilegedPackagesForMock(Collections.singletonList(PACKAGE_NAME));
}
private IntentFilter getIntentFilter() {
@@ -167,13 +188,15 @@ public class TelephonySubscriptionTrackerTest {
return intent;
}
private TelephonySubscriptionSnapshot buildExpectedSnapshot(Set<ParcelUuid> activeSubGroups) {
return buildExpectedSnapshot(TEST_SUBID_TO_GROUP_MAP, activeSubGroups);
private TelephonySubscriptionSnapshot buildExpectedSnapshot(
Map<ParcelUuid, Set<String>> privilegedPackages) {
return buildExpectedSnapshot(TEST_SUBID_TO_GROUP_MAP, privilegedPackages);
}
private TelephonySubscriptionSnapshot buildExpectedSnapshot(
Map<Integer, ParcelUuid> subIdToGroupMap, Set<ParcelUuid> activeSubGroups) {
return new TelephonySubscriptionSnapshot(subIdToGroupMap, activeSubGroups);
Map<Integer, ParcelUuid> subIdToGroupMap,
Map<ParcelUuid, Set<String>> privilegedPackages) {
return new TelephonySubscriptionSnapshot(subIdToGroupMap, privilegedPackages);
}
private void verifyNoActiveSubscriptions() {
@@ -186,6 +209,10 @@ public class TelephonySubscriptionTrackerTest {
Collections.singletonMap(TEST_SIM_SLOT_INDEX, TEST_SUBSCRIPTION_ID_1));
}
private void setPrivilegedPackagesForMock(@NonNull List<String> privilegedPackages) {
doReturn(privilegedPackages).when(mTelephonyManager).getPackagesWithCarrierPrivileges();
}
@Test
public void testRegister() throws Exception {
verify(mContext)
@@ -223,15 +250,30 @@ public class TelephonySubscriptionTrackerTest {
}
@Test
public void testOnSubscriptionsChangedFired_WithReadySubIds() throws Exception {
public void testOnSubscriptionsChangedFired_WithReadySubidsNoPrivilegedPackages()
throws Exception {
setupReadySubIds();
setPrivilegedPackagesForMock(Collections.emptyList());
final OnSubscriptionsChangedListener listener = getOnSubscriptionsChangedListener();
listener.onSubscriptionsChanged();
mTestLooper.dispatchAll();
final Map<ParcelUuid, Set<String>> privilegedPackages =
Collections.singletonMap(TEST_PARCEL_UUID, new ArraySet<>());
verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(privilegedPackages)));
}
@Test
public void testOnSubscriptionsChangedFired_WithReadySubidsAndPrivilegedPackages()
throws Exception {
setupReadySubIds();
final OnSubscriptionsChangedListener listener = getOnSubscriptionsChangedListener();
listener.onSubscriptionsChanged();
mTestLooper.dispatchAll();
final Set<ParcelUuid> activeSubGroups = Collections.singleton(TEST_PARCEL_UUID);
verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(activeSubGroups)));
verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(TEST_PRIVILEGED_PACKAGES)));
}
@Test
@@ -239,8 +281,7 @@ public class TelephonySubscriptionTrackerTest {
mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(true));
mTestLooper.dispatchAll();
final Set<ParcelUuid> activeSubGroups = Collections.singleton(TEST_PARCEL_UUID);
verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(activeSubGroups)));
verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(TEST_PRIVILEGED_PACKAGES)));
}
@Test
@@ -253,8 +294,7 @@ public class TelephonySubscriptionTrackerTest {
mTestLooper.dispatchAll();
// Expect an empty snapshot
verify(mCallback).onNewSnapshot(
eq(buildExpectedSnapshot(Collections.emptyMap(), Collections.emptySet())));
verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(emptyMap(), emptyMap())));
}
@Test
@@ -281,41 +321,57 @@ public class TelephonySubscriptionTrackerTest {
@Test
public void testSubscriptionsClearedAfterValidTriggersCallbacks() throws Exception {
final Set<ParcelUuid> activeSubGroups = Collections.singleton(TEST_PARCEL_UUID);
mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(true));
mTestLooper.dispatchAll();
verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(activeSubGroups)));
verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(TEST_PRIVILEGED_PACKAGES)));
assertNotNull(
mTelephonySubscriptionTracker.getReadySubIdsBySlotId().get(TEST_SIM_SLOT_INDEX));
doReturn(Collections.emptyList()).when(mSubscriptionManager).getAllSubscriptionInfoList();
mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(true));
mTestLooper.dispatchAll();
verify(mCallback).onNewSnapshot(
eq(buildExpectedSnapshot(Collections.emptyMap(), Collections.emptySet())));
verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(emptyMap(), emptyMap())));
}
@Test
public void testSlotClearedAfterValidTriggersCallbacks() throws Exception {
final Set<ParcelUuid> activeSubGroups = Collections.singleton(TEST_PARCEL_UUID);
mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(true));
mTestLooper.dispatchAll();
verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(activeSubGroups)));
verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(TEST_PRIVILEGED_PACKAGES)));
assertNotNull(
mTelephonySubscriptionTracker.getReadySubIdsBySlotId().get(TEST_SIM_SLOT_INDEX));
mTelephonySubscriptionTracker.onReceive(mContext, buildTestBroadcastIntent(false));
mTestLooper.dispatchAll();
verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(Collections.emptySet())));
verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(emptyMap())));
assertNull(mTelephonySubscriptionTracker.getReadySubIdsBySlotId().get(TEST_SIM_SLOT_INDEX));
}
@Test
public void testChangingPrivilegedPackagesAfterValidTriggersCallbacks() throws Exception {
setupReadySubIds();
// Setup initial "valid" state
final OnSubscriptionsChangedListener listener = getOnSubscriptionsChangedListener();
listener.onSubscriptionsChanged();
mTestLooper.dispatchAll();
verify(mCallback).onNewSnapshot(eq(buildExpectedSnapshot(TEST_PRIVILEGED_PACKAGES)));
// Simulate a loss of carrier privileges
setPrivilegedPackagesForMock(Collections.emptyList());
listener.onSubscriptionsChanged();
mTestLooper.dispatchAll();
verify(mCallback)
.onNewSnapshot(
eq(buildExpectedSnapshot(singletonMap(TEST_PARCEL_UUID, emptySet()))));
}
@Test
public void testTelephonySubscriptionSnapshotGetGroupForSubId() throws Exception {
final TelephonySubscriptionSnapshot snapshot =
new TelephonySubscriptionSnapshot(TEST_SUBID_TO_GROUP_MAP, Collections.emptySet());
new TelephonySubscriptionSnapshot(TEST_SUBID_TO_GROUP_MAP, emptyMap());
assertEquals(TEST_PARCEL_UUID, snapshot.getGroupForSubId(TEST_SUBSCRIPTION_ID_1));
assertEquals(TEST_PARCEL_UUID, snapshot.getGroupForSubId(TEST_SUBSCRIPTION_ID_2));
@@ -324,7 +380,7 @@ public class TelephonySubscriptionTrackerTest {
@Test
public void testTelephonySubscriptionSnapshotGetAllSubIdsInGroup() throws Exception {
final TelephonySubscriptionSnapshot snapshot =
new TelephonySubscriptionSnapshot(TEST_SUBID_TO_GROUP_MAP, Collections.emptySet());
new TelephonySubscriptionSnapshot(TEST_SUBID_TO_GROUP_MAP, emptyMap());
assertEquals(
new ArraySet<>(Arrays.asList(TEST_SUBSCRIPTION_ID_1, TEST_SUBSCRIPTION_ID_2)),

View File

@@ -0,0 +1,121 @@
/*
* 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 org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.annotation.NonNull;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.os.test.TestLooper;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.vcn.VcnNetworkProvider.NetworkRequestListener;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
/** Tests for TelephonySubscriptionTracker */
@RunWith(AndroidJUnit4.class)
@SmallTest
public class VcnNetworkProviderTest {
private static final int TEST_SCORE_UNSATISFIED = 0;
private static final int TEST_SCORE_HIGH = 100;
private static final int TEST_PROVIDER_ID = 1;
private static final int TEST_LEGACY_TYPE = ConnectivityManager.TYPE_MOBILE;
private static final NetworkRequest.Type TEST_REQUEST_TYPE = NetworkRequest.Type.REQUEST;
@NonNull private final Context mContext;
@NonNull private final TestLooper mTestLooper;
@NonNull private VcnNetworkProvider mVcnNetworkProvider;
@NonNull private NetworkRequestListener mListener;
public VcnNetworkProviderTest() {
mContext = mock(Context.class);
mTestLooper = new TestLooper();
}
@Before
public void setUp() throws Exception {
mVcnNetworkProvider = new VcnNetworkProvider(mContext, mTestLooper.getLooper());
mListener = mock(NetworkRequestListener.class);
}
@Test
public void testRequestsPassedToRegisteredListeners() throws Exception {
mVcnNetworkProvider.registerListener(mListener);
final NetworkRequest request = mock(NetworkRequest.class);
mVcnNetworkProvider.onNetworkRequested(request, TEST_SCORE_UNSATISFIED, TEST_PROVIDER_ID);
verify(mListener).onNetworkRequested(request, TEST_SCORE_UNSATISFIED, TEST_PROVIDER_ID);
}
@Test
public void testRequestsPassedToRegisteredListeners_satisfiedByHighScoringProvider()
throws Exception {
mVcnNetworkProvider.registerListener(mListener);
final NetworkRequest request = mock(NetworkRequest.class);
mVcnNetworkProvider.onNetworkRequested(request, TEST_SCORE_HIGH, TEST_PROVIDER_ID);
verify(mListener).onNetworkRequested(request, TEST_SCORE_HIGH, TEST_PROVIDER_ID);
}
@Test
public void testUnregisterListener() throws Exception {
mVcnNetworkProvider.registerListener(mListener);
mVcnNetworkProvider.unregisterListener(mListener);
final NetworkRequest request = mock(NetworkRequest.class);
mVcnNetworkProvider.onNetworkRequested(request, TEST_SCORE_UNSATISFIED, TEST_PROVIDER_ID);
verifyNoMoreInteractions(mListener);
}
@Test
public void testCachedRequestsPassedOnRegister() throws Exception {
final List<NetworkRequest> requests = new ArrayList<>();
for (int i = 0; i < 10; i++) {
final NetworkRequest request =
new NetworkRequest(
new NetworkCapabilities(),
TEST_LEGACY_TYPE,
i /* requestId */,
TEST_REQUEST_TYPE);
requests.add(request);
mVcnNetworkProvider.onNetworkRequested(request, i, i + 1);
}
mVcnNetworkProvider.registerListener(mListener);
for (int i = 0; i < requests.size(); i++) {
final NetworkRequest request = requests.get(i);
verify(mListener).onNetworkRequested(request, i, i + 1);
}
verifyNoMoreInteractions(mListener);
}
}