Add persistence for VcnConfig objects by Subscription Group
This commit adds the ability for the VcnManagementService to track/store VCN profiles by subscription groups, and saving/loading to/from disk. Bug: 163611304 Test: New tests added, passing Change-Id: Ifabf5e2be090d529cd29e2c68d55ece4858b2aad
This commit is contained in:
@@ -23,6 +23,9 @@ import android.annotation.SystemService;
|
||||
import android.content.Context;
|
||||
import android.os.ParcelUuid;
|
||||
import android.os.RemoteException;
|
||||
import android.os.ServiceSpecificException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* VcnManager publishes APIs for applications to configure and manage Virtual Carrier Networks.
|
||||
@@ -63,15 +66,20 @@ public final class VcnManager {
|
||||
* @param config the configuration parameters for the VCN
|
||||
* @throws SecurityException if the caller does not have carrier privileges, or is not running
|
||||
* as the primary user
|
||||
* @throws IOException if the configuration failed to be persisted. A caller encountering this
|
||||
* exception should attempt to retry (possibly after a delay).
|
||||
* @hide
|
||||
*/
|
||||
@RequiresPermission("carrier privileges") // TODO (b/72967236): Define a system-wide constant
|
||||
public void setVcnConfig(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) {
|
||||
public void setVcnConfig(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config)
|
||||
throws IOException {
|
||||
requireNonNull(subscriptionGroup, "subscriptionGroup was null");
|
||||
requireNonNull(config, "config was null");
|
||||
|
||||
try {
|
||||
mService.setVcnConfig(subscriptionGroup, config);
|
||||
} catch (ServiceSpecificException e) {
|
||||
throw new IOException(e);
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
@@ -88,14 +96,18 @@ public final class VcnManager {
|
||||
* @param subscriptionGroup the subscription group that the configuration should be applied to
|
||||
* @throws SecurityException if the caller does not have carrier privileges, or is not running
|
||||
* as the primary user
|
||||
* @throws IOException if the configuration failed to be cleared. A caller encountering this
|
||||
* exception should attempt to retry (possibly after a delay).
|
||||
* @hide
|
||||
*/
|
||||
@RequiresPermission("carrier privileges") // TODO (b/72967236): Define a system-wide constant
|
||||
public void clearVcnConfig(@NonNull ParcelUuid subscriptionGroup) {
|
||||
public void clearVcnConfig(@NonNull ParcelUuid subscriptionGroup) throws IOException {
|
||||
requireNonNull(subscriptionGroup, "subscriptionGroup was null");
|
||||
|
||||
try {
|
||||
mService.clearVcnConfig(subscriptionGroup);
|
||||
} catch (ServiceSpecificException e) {
|
||||
throw new IOException(e);
|
||||
} catch (RemoteException e) {
|
||||
throw e.rethrowFromSystemServer();
|
||||
}
|
||||
|
||||
@@ -26,20 +26,31 @@ import android.net.NetworkRequest;
|
||||
import android.net.vcn.IVcnManagementService;
|
||||
import android.net.vcn.VcnConfig;
|
||||
import android.os.Binder;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.os.ParcelUuid;
|
||||
import android.os.PersistableBundle;
|
||||
import android.os.Process;
|
||||
import android.os.ServiceSpecificException;
|
||||
import android.os.UserHandle;
|
||||
import android.telephony.SubscriptionInfo;
|
||||
import android.telephony.SubscriptionManager;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.ArrayMap;
|
||||
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.util.PersistableBundleUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
/**
|
||||
* VcnManagementService manages Virtual Carrier Network profiles and lifecycles.
|
||||
@@ -101,20 +112,72 @@ public class VcnManagementService extends IVcnManagementService.Stub {
|
||||
|
||||
public static final boolean VDBG = false; // STOPSHIP: if true
|
||||
|
||||
@VisibleForTesting(visibility = Visibility.PRIVATE)
|
||||
static final String VCN_CONFIG_FILE = "/data/system/vcn/configs.xml";
|
||||
|
||||
/* Binder context for this service */
|
||||
@NonNull private final Context mContext;
|
||||
@NonNull private final Dependencies mDeps;
|
||||
|
||||
@NonNull private final Looper mLooper;
|
||||
@NonNull private final Handler mHandler;
|
||||
@NonNull private final VcnNetworkProvider mNetworkProvider;
|
||||
|
||||
@GuardedBy("mLock")
|
||||
@NonNull
|
||||
private final Map<ParcelUuid, VcnConfig> mConfigs = new ArrayMap<>();
|
||||
|
||||
@NonNull private final Object mLock = new Object();
|
||||
|
||||
@NonNull private final PersistableBundleUtils.LockingReadWriteHelper mConfigDiskRwHelper;
|
||||
|
||||
@VisibleForTesting(visibility = Visibility.PRIVATE)
|
||||
VcnManagementService(@NonNull Context context, @NonNull Dependencies deps) {
|
||||
mContext = requireNonNull(context, "Missing context");
|
||||
mDeps = requireNonNull(deps, "Missing dependencies");
|
||||
|
||||
mLooper = mDeps.getLooper();
|
||||
mHandler = new Handler(mLooper);
|
||||
mNetworkProvider = new VcnNetworkProvider(mContext, mLooper);
|
||||
|
||||
mConfigDiskRwHelper = mDeps.newPersistableBundleLockingReadWriteHelper(VCN_CONFIG_FILE);
|
||||
|
||||
// Run on handler to ensure I/O does not block system server startup
|
||||
mHandler.post(() -> {
|
||||
PersistableBundle configBundle = null;
|
||||
try {
|
||||
configBundle = mConfigDiskRwHelper.readFromDisk();
|
||||
} catch (IOException e1) {
|
||||
Slog.e(TAG, "Failed to read configs from disk; retrying", e1);
|
||||
|
||||
// Retry immediately. The IOException may have been transient.
|
||||
try {
|
||||
configBundle = mConfigDiskRwHelper.readFromDisk();
|
||||
} catch (IOException e2) {
|
||||
Slog.wtf(TAG, "Failed to read configs from disk", e2);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (configBundle != null) {
|
||||
final Map<ParcelUuid, VcnConfig> configs =
|
||||
PersistableBundleUtils.toMap(
|
||||
configBundle,
|
||||
PersistableBundleUtils::toParcelUuid,
|
||||
VcnConfig::new);
|
||||
|
||||
synchronized (mLock) {
|
||||
for (Entry<ParcelUuid, VcnConfig> entry : configs.entrySet()) {
|
||||
// Ensure no new configs are overwritten; a carrier app may have added a new
|
||||
// config.
|
||||
if (!mConfigs.containsKey(entry.getKey())) {
|
||||
mConfigs.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
// TODO: Trigger re-evaluation of active VCNs; start/stop VCNs as needed.
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Package-visibility for SystemServer to create instances.
|
||||
@@ -151,12 +214,21 @@ public class VcnManagementService extends IVcnManagementService.Stub {
|
||||
public int getBinderCallingUid() {
|
||||
return Binder.getCallingUid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a new {@link PersistableBundle.LockingReadWriteHelper}
|
||||
*
|
||||
* @param path the file path to read/write from/to.
|
||||
* @return the {@link PersistableBundleUtils.LockingReadWriteHelper} instance
|
||||
*/
|
||||
public PersistableBundleUtils.LockingReadWriteHelper
|
||||
newPersistableBundleLockingReadWriteHelper(@NonNull String path) {
|
||||
return new PersistableBundleUtils.LockingReadWriteHelper(path);
|
||||
}
|
||||
}
|
||||
|
||||
/** Notifies the VcnManagementService that external dependencies can be set up. */
|
||||
public void systemReady() {
|
||||
// TODO: Retrieve existing profiles from KeyStore
|
||||
|
||||
mContext.getSystemService(ConnectivityManager.class)
|
||||
.registerNetworkProvider(mNetworkProvider);
|
||||
}
|
||||
@@ -217,9 +289,15 @@ public class VcnManagementService extends IVcnManagementService.Stub {
|
||||
|
||||
enforceCallingUserAndCarrierPrivilege(subscriptionGroup);
|
||||
|
||||
// TODO: Clear Binder calling identity
|
||||
synchronized (mLock) {
|
||||
mConfigs.put(subscriptionGroup, config);
|
||||
|
||||
// TODO: Store VCN configuration, trigger startup as necessary
|
||||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,9 +311,38 @@ public class VcnManagementService extends IVcnManagementService.Stub {
|
||||
|
||||
enforceCallingUserAndCarrierPrivilege(subscriptionGroup);
|
||||
|
||||
// TODO: Clear Binder calling identity
|
||||
synchronized (mLock) {
|
||||
mConfigs.remove(subscriptionGroup);
|
||||
|
||||
// TODO: Clear VCN configuration, trigger teardown as necessary
|
||||
// Must be done synchronously to ensure that writes do not happen out-of-order.
|
||||
writeConfigsToDiskLocked();
|
||||
}
|
||||
|
||||
// TODO: Clear Binder calling identity
|
||||
// TODO: Trigger teardown as necessary
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void writeConfigsToDiskLocked() {
|
||||
try {
|
||||
PersistableBundle bundle =
|
||||
PersistableBundleUtils.fromMap(
|
||||
mConfigs,
|
||||
PersistableBundleUtils::fromParcelUuid,
|
||||
VcnConfig::toPersistableBundle);
|
||||
mConfigDiskRwHelper.writeToDisk(bundle);
|
||||
} catch (IOException e) {
|
||||
Slog.e(TAG, "Failed to save configs to disk", e);
|
||||
throw new ServiceSpecificException(0, "Failed to save configs");
|
||||
}
|
||||
}
|
||||
|
||||
/** Get current configuration list for testing purposes */
|
||||
@VisibleForTesting(visibility = Visibility.PRIVATE)
|
||||
Map<ParcelUuid, VcnConfig> getConfigs() {
|
||||
synchronized (mLock) {
|
||||
return Collections.unmodifiableMap(mConfigs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package com.android.server;
|
||||
|
||||
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.doReturn;
|
||||
@@ -25,8 +28,10 @@ import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.vcn.VcnConfig;
|
||||
import android.net.vcn.VcnConfigTest;
|
||||
import android.os.ParcelUuid;
|
||||
import android.os.PersistableBundle;
|
||||
import android.os.Process;
|
||||
import android.os.UserHandle;
|
||||
import android.os.test.TestLooper;
|
||||
@@ -37,10 +42,14 @@ import android.telephony.TelephonyManager;
|
||||
import androidx.test.filters.SmallTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.android.server.vcn.util.PersistableBundleUtils;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Tests for {@link VcnManagementService}. */
|
||||
@@ -48,6 +57,11 @@ import java.util.UUID;
|
||||
@SmallTest
|
||||
public class VcnManagementServiceTest {
|
||||
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 Map<ParcelUuid, VcnConfig> TEST_VCN_CONFIG_MAP =
|
||||
Collections.unmodifiableMap(Collections.singletonMap(TEST_UUID_1, TEST_VCN_CONFIG));
|
||||
|
||||
private static final SubscriptionInfo TEST_SUBSCRIPTION_INFO =
|
||||
new SubscriptionInfo(
|
||||
1 /* id */,
|
||||
@@ -79,6 +93,8 @@ public class VcnManagementServiceTest {
|
||||
private final TelephonyManager mTelMgr = mock(TelephonyManager.class);
|
||||
private final SubscriptionManager mSubMgr = mock(SubscriptionManager.class);
|
||||
private final VcnManagementService mVcnMgmtSvc;
|
||||
private final PersistableBundleUtils.LockingReadWriteHelper mConfigReadWriteHelper =
|
||||
mock(PersistableBundleUtils.LockingReadWriteHelper.class);
|
||||
|
||||
public VcnManagementServiceTest() throws Exception {
|
||||
setupSystemService(mConnMgr, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class);
|
||||
@@ -88,6 +104,16 @@ public class VcnManagementServiceTest {
|
||||
|
||||
doReturn(mTestLooper.getLooper()).when(mMockDeps).getLooper();
|
||||
doReturn(Process.FIRST_APPLICATION_UID).when(mMockDeps).getBinderCallingUid();
|
||||
doReturn(mConfigReadWriteHelper)
|
||||
.when(mMockDeps)
|
||||
.newPersistableBundleLockingReadWriteHelper(any());
|
||||
|
||||
final PersistableBundle bundle =
|
||||
PersistableBundleUtils.fromMap(
|
||||
TEST_VCN_CONFIG_MAP,
|
||||
PersistableBundleUtils::fromParcelUuid,
|
||||
VcnConfig::toPersistableBundle);
|
||||
doReturn(bundle).when(mConfigReadWriteHelper).readFromDisk();
|
||||
|
||||
setupMockedCarrierPrivilege(true);
|
||||
mVcnMgmtSvc = new VcnManagementService(mMockContext, mMockDeps);
|
||||
@@ -115,6 +141,36 @@ public class VcnManagementServiceTest {
|
||||
.registerNetworkProvider(any(VcnManagementService.VcnNetworkProvider.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonSystemServerRealConfigFileAccessPermission() throws Exception {
|
||||
// Attempt to build a real instance of the dependencies, and verify we cannot write to the
|
||||
// file.
|
||||
VcnManagementService.Dependencies deps = new VcnManagementService.Dependencies();
|
||||
PersistableBundleUtils.LockingReadWriteHelper configReadWriteHelper =
|
||||
deps.newPersistableBundleLockingReadWriteHelper(
|
||||
VcnManagementService.VCN_CONFIG_FILE);
|
||||
|
||||
// Even tests should not be able to read/write configs from disk; SELinux policies restrict
|
||||
// it to only the system server.
|
||||
// Reading config should always return null since the file "does not exist", and writing
|
||||
// should throw an IOException.
|
||||
assertNull(configReadWriteHelper.readFromDisk());
|
||||
|
||||
try {
|
||||
configReadWriteHelper.writeToDisk(new PersistableBundle());
|
||||
fail("Expected IOException due to SELinux policy");
|
||||
} catch (FileNotFoundException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoadVcnConfigsOnStartup() throws Exception {
|
||||
mTestLooper.dispatchAll();
|
||||
|
||||
assertEquals(TEST_VCN_CONFIG_MAP, mVcnMgmtSvc.getConfigs());
|
||||
verify(mConfigReadWriteHelper).readFromDisk();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetVcnConfigRequiresNonSystemServer() throws Exception {
|
||||
doReturn(Process.SYSTEM_UID).when(mMockDeps).getBinderCallingUid();
|
||||
@@ -150,6 +206,14 @@ public class VcnManagementServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetVcnConfig() throws Exception {
|
||||
// Use a different UUID to simulate a new VCN config.
|
||||
mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG);
|
||||
assertEquals(TEST_VCN_CONFIG, mVcnMgmtSvc.getConfigs().get(TEST_UUID_2));
|
||||
verify(mConfigReadWriteHelper).writeToDisk(any(PersistableBundle.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClearVcnConfigRequiresNonSystemServer() throws Exception {
|
||||
doReturn(Process.SYSTEM_UID).when(mMockDeps).getBinderCallingUid();
|
||||
@@ -184,4 +248,11 @@ public class VcnManagementServiceTest {
|
||||
} catch (SecurityException expected) {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClearVcnConfig() throws Exception {
|
||||
mVcnMgmtSvc.clearVcnConfig(TEST_UUID_1);
|
||||
assertTrue(mVcnMgmtSvc.getConfigs().isEmpty());
|
||||
verify(mConfigReadWriteHelper).writeToDisk(any(PersistableBundle.class));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user