Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ab1b484a authored by Benedict Wong's avatar Benedict Wong
Browse files

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
parent 26497366
Loading
Loading
Loading
Loading
+14 −2
Original line number Diff line number Diff line
@@ -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();
        }
+112 −5
Original line number Diff line number Diff line
@@ -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);

        synchronized (mLock) {
            mConfigs.remove(subscriptionGroup);

            // 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");
        }
    }

        // TODO: Clear VCN configuration, trigger teardown as necessary
    /** Get current configuration list for testing purposes */
    @VisibleForTesting(visibility = Visibility.PRIVATE)
    Map<ParcelUuid, VcnConfig> getConfigs() {
        synchronized (mLock) {
            return Collections.unmodifiableMap(mConfigs);
        }
    }

    /**
+71 −0
Original line number Diff line number Diff line
@@ -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));
    }
}