Merge "proto: Add OWNERS file for camera.proto"
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 85186dd..2214ab4 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -1572,6 +1572,7 @@
     field @NonNull public static final android.os.ParcelUuid AVRCP_TARGET;
     field @NonNull public static final android.os.ParcelUuid BASE_UUID;
     field @NonNull public static final android.os.ParcelUuid BNEP;
+    field @NonNull public static final android.os.ParcelUuid DIP;
     field @NonNull public static final android.os.ParcelUuid HEARING_AID;
     field @NonNull public static final android.os.ParcelUuid HFP;
     field @NonNull public static final android.os.ParcelUuid HFP_AG;
diff --git a/core/java/android/bluetooth/BluetoothUuid.java b/core/java/android/bluetooth/BluetoothUuid.java
index 56c4824..c0736a6 100644
--- a/core/java/android/bluetooth/BluetoothUuid.java
+++ b/core/java/android/bluetooth/BluetoothUuid.java
@@ -162,6 +162,11 @@
     /** @hide */
     @NonNull
     @SystemApi
+    public static final ParcelUuid DIP =
+            ParcelUuid.fromString("00001200-0000-1000-8000-00805F9B34FB");
+    /** @hide */
+    @NonNull
+    @SystemApi
     public static final ParcelUuid BASE_UUID =
             ParcelUuid.fromString("00000000-0000-1000-8000-00805F9B34FB");
 
diff --git a/core/java/android/bluetooth/SdpDipRecord.java b/core/java/android/bluetooth/SdpDipRecord.java
new file mode 100644
index 0000000..84b0eef
--- /dev/null
+++ b/core/java/android/bluetooth/SdpDipRecord.java
@@ -0,0 +1,104 @@
+/*
+* Copyright (C) 2015 Samsung System LSI
+* 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 android.bluetooth;
+
+import java.util.Arrays;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Data representation of a Object Push Profile Server side SDP record.
+ */
+/** @hide */
+public class SdpDipRecord implements Parcelable {
+    private final int mSpecificationId;
+    private final int mVendorId;
+    private final int mVendorIdSource;
+    private final int mProductId;
+    private final int mVersion;
+    private final boolean mPrimaryRecord;
+
+    public SdpDipRecord(int specificationId,
+            int vendorId, int vendorIdSource,
+            int productId, int version,
+            boolean primaryRecord) {
+        super();
+        this.mSpecificationId = specificationId;
+        this.mVendorId = vendorId;
+        this.mVendorIdSource = vendorIdSource;
+        this.mProductId = productId;
+        this.mVersion = version;
+        this.mPrimaryRecord = primaryRecord;
+    }
+
+    public SdpDipRecord(Parcel in) {
+        this.mSpecificationId = in.readInt();
+        this.mVendorId = in.readInt();
+        this.mVendorIdSource = in.readInt();
+        this.mProductId = in.readInt();
+        this.mVersion = in.readInt();
+        this.mPrimaryRecord = in.readBoolean();
+    }
+
+    public int getSpecificationId() {
+        return mSpecificationId;
+    }
+
+    public int getVendorId() {
+        return mVendorId;
+    }
+
+    public int getVendorIdSource() {
+        return mVendorIdSource;
+    }
+
+    public int getProductId() {
+        return mProductId;
+    }
+
+    public int getVersion() {
+        return mVersion;
+    }
+
+    public boolean getPrimaryRecord() {
+        return mPrimaryRecord;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mSpecificationId);
+        dest.writeInt(mVendorId);
+        dest.writeInt(mVendorIdSource);
+        dest.writeInt(mProductId);
+        dest.writeInt(mVersion);
+        dest.writeBoolean(mPrimaryRecord);
+    }
+
+    @Override
+    public int describeContents() {
+        /* No special objects */
+        return 0;
+    }
+
+    public static  final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+        public SdpDipRecord createFromParcel(Parcel in) {
+            return new SdpDipRecord(in);
+        }
+        public SdpDipRecord[] newArray(int size) {
+            return new SdpDipRecord[size];
+        }
+    };
+}
diff --git a/core/java/android/net/vcn/IVcnManagementService.aidl b/core/java/android/net/vcn/IVcnManagementService.aidl
index 9dd0114..04b585c 100644
--- a/core/java/android/net/vcn/IVcnManagementService.aidl
+++ b/core/java/android/net/vcn/IVcnManagementService.aidl
@@ -23,6 +23,6 @@
  * @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);
 }
diff --git a/core/java/android/net/vcn/VcnConfig.java b/core/java/android/net/vcn/VcnConfig.java
index d4a3fa7..ede8faa 100644
--- a/core/java/android/net/vcn/VcnConfig.java
+++ b/core/java/android/net/vcn/VcnConfig.java
@@ -19,6 +19,7 @@
 
 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 @@
 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 @@
      */
     @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 @@
     }
 
     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 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 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(mGatewayConnectionConfigs);
+        return Objects.hash(mPackageName, mGatewayConnectionConfigs);
     }
 
     @Override
@@ -112,7 +134,8 @@
         }
 
         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 @@
 
     /** 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 @@
          */
         @NonNull
         public VcnConfig build() {
-            return new VcnConfig(mGatewayConnectionConfigs);
+            return new VcnConfig(mPackageName, mGatewayConnectionConfigs);
         }
     }
 }
diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java
index 19c183f..b881a339 100644
--- a/core/java/android/net/vcn/VcnManager.java
+++ b/core/java/android/net/vcn/VcnManager.java
@@ -101,7 +101,7 @@
         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) {
diff --git a/core/java/android/uwb/CloseReason.aidl b/core/java/android/uwb/CloseReason.aidl
deleted file mode 100644
index bef129e..0000000
--- a/core/java/android/uwb/CloseReason.aidl
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 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 android.uwb;
-
-/**
- * @hide
- */
-@Backing(type="int")
-enum CloseReason {
-  /**
-   * Unknown reason
-   */
-  UNKNOWN,
-
-  /**
-   * A local API call triggered the close, such as a call to
-   * IUwbAdapter.stopRanging.
-   */
-  LOCAL_API,
-
-  /**
-   * The maximum number of sessions has been reached. This error may be generated
-   * for an active session if a higher priority session begins.
-   */
-  MAX_SESSIONS_REACHED,
-
-  /**
-   * The system state has changed resulting in the session ending (e.g. the user
-   * disables UWB, or the user's locale changes and an active channel is no longer
-   * permitted to be used).
-   */
-  SYSTEM_POLICY,
-
-  /**
-   * The remote device has requested to terminate the session
-   */
-  REMOTE_REQUEST,
-
-  /**
-   * The session was closed for a protocol specific reason
-   */
-  PROTOCOL_SPECIFIC,
-}
-
diff --git a/core/java/android/uwb/IUwbAdapter.aidl b/core/java/android/uwb/IUwbAdapter.aidl
index 2c8b2e4..b9c5508 100644
--- a/core/java/android/uwb/IUwbAdapter.aidl
+++ b/core/java/android/uwb/IUwbAdapter.aidl
@@ -119,42 +119,96 @@
   PersistableBundle getSpecificationInfo();
 
   /**
-   * Request to start a new ranging session
+   * Request to open a new ranging session
    *
-   * This function must return before calling IUwbAdapterCallbacks
-   * #onRangingStarted, #onRangingClosed, or #onRangingResult.
+   * This function must return before calling any functions in
+   * IUwbAdapterCallbacks.
    *
-   * A ranging session does not need to be started before returning.
+   * This function does not start the ranging session, but all necessary
+   * components must be initialized and ready to start a new ranging
+   * session prior to calling IUwbAdapterCallback#onRangingOpened.
    *
-   * IUwbAdapterCallbacks#onRangingStarted must be called within
-   * RANGING_SESSION_START_THRESHOLD_MS milliseconds of #startRanging being called
-   * if the ranging session is scheduled to start successfully.
+   * IUwbAdapterCallbacks#onRangingOpened must be called within
+   * RANGING_SESSION_OPEN_THRESHOLD_MS milliseconds of #openRanging being
+   * called if the ranging session is opened successfully.
    *
-   * IUwbAdapterCallbacks#onRangingStartFailed must be called within
-   * RANGING_SESSION_START_THRESHOLD_MS milliseconds of #startRanging being called
-   * if the ranging session fails to be scheduled to start successfully.
+   * IUwbAdapterCallbacks#onRangingOpenFailed must be called within
+   * RANGING_SESSION_OPEN_THRESHOLD_MS milliseconds of #openRanging being called
+   * if the ranging session fails to be opened.
    *
    * @param rangingCallbacks the callbacks used to deliver ranging information
    * @param parameters the configuration to use for ranging
    * @return a SessionHandle used to identify this ranging request
    */
-  SessionHandle startRanging(in IUwbRangingCallbacks rangingCallbacks,
-                             in PersistableBundle parameters);
+  SessionHandle openRanging(in IUwbRangingCallbacks rangingCallbacks,
+                            in PersistableBundle parameters);
 
   /**
-   * Stop and close ranging for the session associated with the given handle
+   * Request to start ranging
+   *
+   * IUwbAdapterCallbacks#onRangingStarted must be called within
+   * RANGING_SESSION_START_THRESHOLD_MS milliseconds of #startRanging being
+   * called if the ranging session starts successfully.
+   *
+   * IUwbAdapterCallbacks#onRangingStartFailed must be called within
+   * RANGING_SESSION_START_THRESHOLD_MS milliseconds of #startRanging being
+   * called if the ranging session fails to be started.
+   *
+   * @param sessionHandle the session handle to start ranging for
+   * @param parameters additional configuration required to start ranging
+   */
+  void startRanging(in SessionHandle sessionHandle,
+                    in PersistableBundle parameters);
+
+  /**
+   * Request to reconfigure ranging
+   *
+   * IUwbAdapterCallbacks#onRangingReconfigured must be called after
+   * successfully reconfiguring the session.
+   *
+   * IUwbAdapterCallbacks#onRangingReconfigureFailed must be called after
+   * failing to reconfigure the session.
+   *
+   * A session must not be modified by a failed call to #reconfigureRanging.
+   *
+   * @param sessionHandle the session handle to start ranging for
+   * @param parameters the parameters to reconfigure and their new values
+   */
+  void reconfigureRanging(in SessionHandle sessionHandle,
+                          in PersistableBundle parameters);
+
+  /**
+   * Request to stop ranging
+   *
+   * IUwbAdapterCallbacks#onRangingStopped must be called after
+   * successfully stopping the session.
+   *
+   * IUwbAdapterCallbacks#onRangingStopFailed must be called after failing
+   * to stop the session.
+   *
+   * @param sessionHandle the session handle to stop ranging for
+   */
+  void stopRanging(in SessionHandle sessionHandle);
+
+  /**
+   * Close ranging for the session associated with the given handle
    *
    * Calling with an invalid handle or a handle that has already been closed
    * is a no-op.
    *
    * IUwbAdapterCallbacks#onRangingClosed must be called within
-   * RANGING_SESSION_CLOSE_THRESHOLD_MS of #stopRanging being called.
+   * RANGING_SESSION_CLOSE_THRESHOLD_MS of #closeRanging being called.
    *
-   * @param sessionHandle the session handle to stop ranging for
+   * @param sessionHandle the session handle to close ranging for
    */
   void closeRanging(in SessionHandle sessionHandle);
 
   /**
+   * The maximum allowed time to open a ranging session.
+   */
+  const int RANGING_SESSION_OPEN_THRESHOLD_MS = 3000; // Value TBD
+
+  /**
    * The maximum allowed time to start a ranging session.
    */
   const int RANGING_SESSION_START_THRESHOLD_MS = 3000; // Value TBD
diff --git a/core/java/android/uwb/IUwbRangingCallbacks.aidl b/core/java/android/uwb/IUwbRangingCallbacks.aidl
index 1fc3bfd..f71f3ff 100644
--- a/core/java/android/uwb/IUwbRangingCallbacks.aidl
+++ b/core/java/android/uwb/IUwbRangingCallbacks.aidl
@@ -17,16 +17,33 @@
 package android.uwb;
 
 import android.os.PersistableBundle;
-import android.uwb.CloseReason;
+import android.uwb.RangingChangeReason;
 import android.uwb.RangingReport;
 import android.uwb.SessionHandle;
-import android.uwb.StartFailureReason;
 
 /**
  * @hide
  */
 interface IUwbRangingCallbacks {
   /**
+   * Called when the ranging session has been opened
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   */
+  void onRangingOpened(in SessionHandle sessionHandle);
+
+  /**
+   * Called when a ranging session fails to start
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param reason the reason the session failed to start
+   * @param parameters protocol specific parameters
+   */
+  void onRangingOpenFailed(in SessionHandle sessionHandle,
+                           RangingChangeReason reason,
+                           in PersistableBundle parameters);
+
+  /**
    * Called when ranging has started
    *
    * May output parameters generated by the lower layers that must be sent to the
@@ -47,8 +64,49 @@
    * @param reason the reason the session failed to start
    * @param parameters protocol specific parameters
    */
-  void onRangingStartFailed(in SessionHandle sessionHandle, StartFailureReason reason,
+  void onRangingStartFailed(in SessionHandle sessionHandle,
+                            RangingChangeReason reason,
                             in PersistableBundle parameters);
+
+   /**
+   * Called when ranging has been reconfigured
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param parameters the updated ranging configuration
+   */
+  void onRangingReconfigured(in SessionHandle sessionHandle,
+                             in PersistableBundle parameters);
+
+  /**
+   * Called when a ranging session fails to be reconfigured
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param reason the reason the session failed to reconfigure
+   * @param parameters protocol specific parameters
+   */
+  void onRangingReconfigureFailed(in SessionHandle sessionHandle,
+                                  RangingChangeReason reason,
+                                  in PersistableBundle parameters);
+
+  /**
+   * Called when the ranging session has been stopped
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   */
+
+  void onRangingStopped(in SessionHandle sessionHandle);
+
+  /**
+   * Called when a ranging session fails to stop
+   *
+   * @param sessionHandle the session the callback is being invoked for
+   * @param reason the reason the session failed to stop
+   * @param parameters protocol specific parameters
+   */
+  void onRangingStopFailed(in SessionHandle sessionHandle,
+                           RangingChangeReason reason,
+                           in PersistableBundle parameters);
+
   /**
    * Called when a ranging session is closed
    *
@@ -56,7 +114,8 @@
    * @param reason the reason the session was closed
    * @param parameters protocol specific parameters
    */
-  void onRangingClosed(in SessionHandle sessionHandle, CloseReason reason,
+  void onRangingClosed(in SessionHandle sessionHandle,
+                       RangingChangeReason reason,
                        in PersistableBundle parameters);
 
   /**
diff --git a/core/java/android/uwb/RangingChangeReason.aidl b/core/java/android/uwb/RangingChangeReason.aidl
new file mode 100644
index 0000000..19d4b39
--- /dev/null
+++ b/core/java/android/uwb/RangingChangeReason.aidl
@@ -0,0 +1,63 @@
+/*
+ * Copyright 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 android.uwb;
+
+/**
+ * @hide
+ */
+@Backing(type="int")
+enum RangingChangeReason {
+  /**
+   * Unknown reason
+   */
+  UNKNOWN,
+
+  /**
+   * A local API call triggered the change, such as a call to
+   * IUwbAdapter.closeRanging.
+   */
+  LOCAL_API,
+
+  /**
+   * The maximum number of sessions has been reached. This may be generated for
+   * an active session if a higher priority session begins.
+   */
+  MAX_SESSIONS_REACHED,
+
+  /**
+   * The system state has changed resulting in the session changing (e.g. the
+   * user disables UWB, or the user's locale changes and an active channel is no
+   * longer permitted to be used).
+   */
+  SYSTEM_POLICY,
+
+  /**
+   * The remote device has requested to change the session
+   */
+  REMOTE_REQUEST,
+
+  /**
+   * The session changed for a protocol specific reason
+   */
+  PROTOCOL_SPECIFIC,
+
+  /**
+   * The provided parameters were invalid
+   */
+  BAD_PARAMETERS,
+}
+
diff --git a/core/java/android/uwb/RangingManager.java b/core/java/android/uwb/RangingManager.java
index a9bf4ab..5ac95d4 100644
--- a/core/java/android/uwb/RangingManager.java
+++ b/core/java/android/uwb/RangingManager.java
@@ -50,7 +50,7 @@
             @NonNull RangingSession.Callback callbacks) {
         SessionHandle sessionHandle;
         try {
-            sessionHandle = mAdapter.startRanging(this, params);
+            sessionHandle = mAdapter.openRanging(this, params);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -59,7 +59,7 @@
             if (hasSession(sessionHandle)) {
                 Log.w(TAG, "Newly created session unexpectedly reuses an active SessionHandle");
                 executor.execute(() -> callbacks.onClosed(
-                        RangingSession.Callback.CLOSE_REASON_LOCAL_GENERIC_ERROR,
+                        RangingSession.Callback.REASON_GENERIC_ERROR,
                         new PersistableBundle()));
             }
 
@@ -75,6 +75,67 @@
     }
 
     @Override
+    public void onRangingOpened(SessionHandle sessionHandle) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG,
+                        "onRangingOpened - received unexpected SessionHandle: " + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingOpened();
+        }
+    }
+
+    @Override
+    public void onRangingOpenFailed(SessionHandle sessionHandle, @RangingChangeReason int reason,
+            PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG,
+                        "onRangingOpened - received unexpected SessionHandle: " + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingOpenFailed(convertToReason(reason), parameters);
+            mRangingSessionTable.remove(sessionHandle);
+        }
+    }
+
+    @Override
+    public void onRangingReconfigured(SessionHandle sessionHandle, PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG,
+                        "onRangingReconfigured - received unexpected SessionHandle: "
+                                + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingReconfigured(parameters);
+        }
+    }
+
+    @Override
+    public void onRangingReconfigureFailed(SessionHandle sessionHandle,
+            @RangingChangeReason int reason, PersistableBundle params) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onRangingStartFailed - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingReconfigureFailed(convertToReason(reason), params);
+        }
+    }
+
+
+    @Override
     public void onRangingStarted(SessionHandle sessionHandle, PersistableBundle parameters) {
         synchronized (this) {
             if (!hasSession(sessionHandle)) {
@@ -89,7 +150,7 @@
     }
 
     @Override
-    public void onRangingStartFailed(SessionHandle sessionHandle, int reason,
+    public void onRangingStartFailed(SessionHandle sessionHandle, @RangingChangeReason int reason,
             PersistableBundle params) {
         synchronized (this) {
             if (!hasSession(sessionHandle)) {
@@ -99,13 +160,42 @@
             }
 
             RangingSession session = mRangingSessionTable.get(sessionHandle);
-            session.onRangingClosed(convertStartFailureToCloseReason(reason), params);
-            mRangingSessionTable.remove(sessionHandle);
+            session.onRangingStartFailed(convertToReason(reason), params);
         }
     }
 
     @Override
-    public void onRangingClosed(SessionHandle sessionHandle, int reason, PersistableBundle params) {
+    public void onRangingStopped(SessionHandle sessionHandle) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onRangingStopped - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingStopped();
+        }
+    }
+
+    @Override
+    public void onRangingStopFailed(SessionHandle sessionHandle, @RangingChangeReason int reason,
+            PersistableBundle parameters) {
+        synchronized (this) {
+            if (!hasSession(sessionHandle)) {
+                Log.w(TAG, "onRangingStopFailed - received unexpected SessionHandle: "
+                        + sessionHandle);
+                return;
+            }
+
+            RangingSession session = mRangingSessionTable.get(sessionHandle);
+            session.onRangingStopFailed(convertToReason(reason), parameters);
+        }
+    }
+
+    @Override
+    public void onRangingClosed(SessionHandle sessionHandle, @RangingChangeReason int reason,
+            PersistableBundle params) {
         synchronized (this) {
             if (!hasSession(sessionHandle)) {
                 Log.w(TAG, "onRangingClosed - received unexpected SessionHandle: " + sessionHandle);
@@ -113,7 +203,7 @@
             }
 
             RangingSession session = mRangingSessionTable.get(sessionHandle);
-            session.onRangingClosed(convertToCloseReason(reason), params);
+            session.onRangingClosed(convertToReason(reason), params);
             mRangingSessionTable.remove(sessionHandle);
         }
     }
@@ -131,48 +221,30 @@
         }
     }
 
-    @RangingSession.Callback.CloseReason
-    private static int convertToCloseReason(@CloseReason int reason) {
+    @RangingSession.Callback.Reason
+    private static int convertToReason(@RangingChangeReason int reason) {
         switch (reason) {
-            case CloseReason.LOCAL_API:
-                return RangingSession.Callback.CLOSE_REASON_LOCAL_CLOSE_API;
+            case RangingChangeReason.LOCAL_API:
+                return RangingSession.Callback.REASON_LOCAL_REQUEST;
 
-            case CloseReason.MAX_SESSIONS_REACHED:
-                return RangingSession.Callback.CLOSE_REASON_LOCAL_MAX_SESSIONS_REACHED;
+            case RangingChangeReason.MAX_SESSIONS_REACHED:
+                return RangingSession.Callback.REASON_MAX_SESSIONS_REACHED;
 
-            case CloseReason.SYSTEM_POLICY:
-                return RangingSession.Callback.CLOSE_REASON_LOCAL_SYSTEM_POLICY;
+            case RangingChangeReason.SYSTEM_POLICY:
+                return RangingSession.Callback.REASON_SYSTEM_POLICY;
 
-            case CloseReason.REMOTE_REQUEST:
-                return RangingSession.Callback.CLOSE_REASON_REMOTE_REQUEST;
+            case RangingChangeReason.REMOTE_REQUEST:
+                return RangingSession.Callback.REASON_REMOTE_REQUEST;
 
-            case CloseReason.PROTOCOL_SPECIFIC:
-                return RangingSession.Callback.CLOSE_REASON_PROTOCOL_SPECIFIC;
+            case RangingChangeReason.PROTOCOL_SPECIFIC:
+                return RangingSession.Callback.REASON_PROTOCOL_SPECIFIC_ERROR;
 
-            case CloseReason.UNKNOWN:
+            case RangingChangeReason.BAD_PARAMETERS:
+                return RangingSession.Callback.REASON_BAD_PARAMETERS;
+
+            case RangingChangeReason.UNKNOWN:
             default:
-                return RangingSession.Callback.CLOSE_REASON_UNKNOWN;
-        }
-    }
-
-    @RangingSession.Callback.CloseReason
-    private static int convertStartFailureToCloseReason(@StartFailureReason int reason) {
-        switch (reason) {
-            case StartFailureReason.BAD_PARAMETERS:
-                return RangingSession.Callback.CLOSE_REASON_LOCAL_BAD_PARAMETERS;
-
-            case StartFailureReason.MAX_SESSIONS_REACHED:
-                return RangingSession.Callback.CLOSE_REASON_LOCAL_MAX_SESSIONS_REACHED;
-
-            case StartFailureReason.SYSTEM_POLICY:
-                return RangingSession.Callback.CLOSE_REASON_LOCAL_SYSTEM_POLICY;
-
-            case StartFailureReason.PROTOCOL_SPECIFIC:
-                return RangingSession.Callback.CLOSE_REASON_PROTOCOL_SPECIFIC;
-
-            case StartFailureReason.UNKNOWN:
-            default:
-                return RangingSession.Callback.CLOSE_REASON_UNKNOWN;
+                return RangingSession.Callback.REASON_UNKNOWN;
         }
     }
 }
diff --git a/core/java/android/uwb/RangingSession.java b/core/java/android/uwb/RangingSession.java
index b0dbd85..0f87af4 100644
--- a/core/java/android/uwb/RangingSession.java
+++ b/core/java/android/uwb/RangingSession.java
@@ -36,9 +36,9 @@
  * <p>To get an instance of {@link RangingSession}, first use
  * {@link UwbManager#openRangingSession(PersistableBundle, Executor, Callback)} to request to open a
  * session. Once the session is opened, a {@link RangingSession} object is provided through
- * {@link RangingSession.Callback#onOpenSuccess(RangingSession, PersistableBundle)}. If opening a
- * session fails, the failure is reported through
- * {@link RangingSession.Callback#onClosed(int, PersistableBundle)} with the failure reason.
+ * {@link RangingSession.Callback#onOpened(RangingSession)}. If opening a session fails, the failure
+ * is reported through {@link RangingSession.Callback#onOpenFailed(int, PersistableBundle)} with the
+ * failure reason.
  *
  * @hide
  */
@@ -50,103 +50,162 @@
     private final Callback mCallback;
 
     private enum State {
+        /**
+         * The state of the {@link RangingSession} until
+         * {@link RangingSession.Callback#onOpened(RangingSession)} is invoked
+         */
         INIT,
-        OPEN,
-        CLOSED,
+
+        /**
+         * The {@link RangingSession} is initialized and ready to begin ranging
+         */
+        IDLE,
+
+        /**
+         * The {@link RangingSession} is actively ranging
+         */
+        ACTIVE,
+
+        /**
+         * The {@link RangingSession} is closed and may not be used for ranging.
+         */
+        CLOSED
     }
 
-    private State mState;
+    private State mState = State.INIT;
 
     /**
      * Interface for receiving {@link RangingSession} events
      */
     public interface Callback {
         /**
-         * Invoked when {@link UwbManager#openRangingSession(PersistableBundle, Executor, Callback)}
-         * is successful
-         *
-         * @param session the newly opened {@link RangingSession}
-         * @param sessionInfo session specific parameters from lower layers
-         */
-        void onOpenSuccess(@NonNull RangingSession session, @NonNull PersistableBundle sessionInfo);
-
-        /**
          * @hide
          */
         @Retention(RetentionPolicy.SOURCE)
         @IntDef(value = {
-                CLOSE_REASON_UNKNOWN,
-                CLOSE_REASON_LOCAL_CLOSE_API,
-                CLOSE_REASON_LOCAL_BAD_PARAMETERS,
-                CLOSE_REASON_LOCAL_GENERIC_ERROR,
-                CLOSE_REASON_LOCAL_MAX_SESSIONS_REACHED,
-                CLOSE_REASON_LOCAL_SYSTEM_POLICY,
-                CLOSE_REASON_REMOTE_GENERIC_ERROR,
-                CLOSE_REASON_REMOTE_REQUEST})
-        @interface CloseReason {}
+                REASON_UNKNOWN,
+                REASON_LOCAL_REQUEST,
+                REASON_REMOTE_REQUEST,
+                REASON_BAD_PARAMETERS,
+                REASON_GENERIC_ERROR,
+                REASON_MAX_SESSIONS_REACHED,
+                REASON_SYSTEM_POLICY,
+                REASON_PROTOCOL_SPECIFIC_ERROR})
+        @interface Reason {}
 
         /**
          * Indicates that the session was closed or failed to open due to an unknown reason
          */
-        int CLOSE_REASON_UNKNOWN = 0;
+        int REASON_UNKNOWN = 0;
 
         /**
          * Indicates that the session was closed or failed to open because
          * {@link AutoCloseable#close()} or {@link RangingSession#close()} was called
          */
-        int CLOSE_REASON_LOCAL_CLOSE_API = 1;
-
-        /**
-         * Indicates that the session failed to open due to erroneous parameters passed
-         * to {@link UwbManager#openRangingSession(PersistableBundle, Executor, Callback)}
-         */
-        int CLOSE_REASON_LOCAL_BAD_PARAMETERS = 2;
-
-        /**
-         * Indicates that the session was closed due to some local error on this device besides the
-         * error code already listed
-         */
-        int CLOSE_REASON_LOCAL_GENERIC_ERROR = 3;
-
-        /**
-         * Indicates that the session failed to open because the number of currently open sessions
-         * is equal to {@link UwbManager#getMaxSimultaneousSessions()}
-         */
-        int CLOSE_REASON_LOCAL_MAX_SESSIONS_REACHED = 4;
-
-        /**
-         * Indicates that the session was closed or failed to open due to local system policy, such
-         * as privacy policy, power management policy, permissions, and more.
-         */
-        int CLOSE_REASON_LOCAL_SYSTEM_POLICY = 5;
-
-        /**
-         * Indicates that the session was closed or failed to open due to an error with the remote
-         * device besides error codes already listed.
-         */
-        int CLOSE_REASON_REMOTE_GENERIC_ERROR = 6;
+        int REASON_LOCAL_REQUEST = 1;
 
         /**
          * Indicates that the session was closed or failed to open due to an explicit request from
          * the remote device.
          */
-        int CLOSE_REASON_REMOTE_REQUEST = 7;
+        int REASON_REMOTE_REQUEST = 2;
 
         /**
-         * Indicates that the session was closed for a protocol specific reason. The associated
-         * {@link PersistableBundle} should be consulted for additional information.
+         * Indicates that the session was closed or failed to open due to erroneous parameters
          */
-        int CLOSE_REASON_PROTOCOL_SPECIFIC = 8;
+        int REASON_BAD_PARAMETERS = 3;
 
         /**
+         * Indicates an error on this device besides the error code already listed
+         */
+        int REASON_GENERIC_ERROR = 4;
+
+        /**
+         * Indicates that the number of currently open sessions is equal to
+         * {@link UwbManager#getMaxSimultaneousSessions()} and additional sessions may not be
+         * opened.
+         */
+        int REASON_MAX_SESSIONS_REACHED = 5;
+
+        /**
+         * Indicates that the local system policy caused the change, such
+         * as privacy policy, power management policy, permissions, and more.
+         */
+        int REASON_SYSTEM_POLICY = 6;
+
+        /**
+         * Indicates a protocol specific error. The associated {@link PersistableBundle} should be
+         * consulted for additional information.
+         */
+        int REASON_PROTOCOL_SPECIFIC_ERROR = 7;
+
+        /**
+         * Invoked when {@link UwbManager#openRangingSession(PersistableBundle, Executor, Callback)}
+         * is successful
+         *
+         * @param session the newly opened {@link RangingSession}
+         */
+        void onOpened(@NonNull RangingSession session);
+
+        /**
+         * Invoked if {@link UwbManager#openRangingSession(PersistableBundle, Executor, Callback)}}
+         * fails
+         *
+         * @param reason the failure reason
+         * @param params protocol specific parameters
+         */
+        void onOpenFailed(@Reason int reason, @NonNull PersistableBundle params);
+
+        /**
+         * Invoked when {@link RangingSession#start(PersistableBundle)} is successful
+         * @param sessionInfo session specific parameters from the lower layers
+         */
+        void onStarted(@NonNull PersistableBundle sessionInfo);
+
+        /**
+         * Invoked when {@link RangingSession#start(PersistableBundle)} fails
+         *
+         * @param reason the failure reason
+         * @param params protocol specific parameters
+         */
+        void onStartFailed(@Reason int reason, @NonNull PersistableBundle params);
+
+        /**
+         * Invoked when a request to reconfigure the session succeeds
+         *
+         * @param params the updated ranging configuration
+         */
+        void onReconfigured(@NonNull PersistableBundle params);
+
+        /**
+         * Invoked when a request to reconfigure the session fails
+         *
+         * @param reason reason the session failed to be reconfigured
+         * @param params protocol specific failure reasons
+         */
+        void onReconfigureFailed(@Reason int reason, @NonNull PersistableBundle params);
+
+        /**
+         * Invoked when a request to stop the session succeeds
+         */
+        void onStopped();
+
+        /**
+         * Invoked when a request to stop the session fails
+         *
+         * @param reason reason the session failed to be stopped
+         * @param params protocol specific failure reasons
+         */
+        void onStopFailed(@Reason int reason, @NonNull PersistableBundle params);
+
+       /**
          * Invoked when session is either closed spontaneously, or per user request via
-         * {@link RangingSession#close()} or {@link AutoCloseable#close()}, or when session failed
-         * to open.
+         * {@link RangingSession#close()} or {@link AutoCloseable#close()}.
          *
          * @param reason reason for the session closure
          * @param parameters protocol specific parameters related to the close reason
          */
-        void onClosed(@CloseReason int reason, @NonNull PersistableBundle parameters);
+        void onClosed(@Reason int reason, @NonNull PersistableBundle parameters);
 
         /**
          * Called once per ranging interval even when a ranging measurement fails
@@ -172,12 +231,95 @@
      * @hide
      */
     public boolean isOpen() {
-        return mState == State.OPEN;
+        return mState == State.IDLE || mState == State.ACTIVE;
+    }
+
+    /**
+     * Begins ranging for the session.
+     *
+     * <p>On successfully starting a ranging session,
+     * {@link RangingSession.Callback#onStarted(PersistableBundle)} is invoked.
+     *
+     * <p>On failure to start the session,
+     * {@link RangingSession.Callback#onStartFailed(int, PersistableBundle)} is invoked.
+     *
+     * @param params configuration parameters for starting the session
+     */
+    public void start(@NonNull PersistableBundle params) {
+        if (mState != State.IDLE) {
+            throw new IllegalStateException();
+        }
+
+        try {
+            mAdapter.startRanging(mSessionHandle, params);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Attempts to reconfigure the session with the given parameters
+     * <p>This call may be made when the session is open.
+     *
+     * <p>On successfully reconfiguring the session
+     * {@link RangingSession.Callback#onReconfigured(PersistableBundle)} is invoked.
+     *
+     * <p>On failure to reconfigure the session,
+     * {@link RangingSession.Callback#onReconfigureFailed(int, PersistableBundle)} is invoked.
+     *
+     * @param params the parameters to reconfigure and their new values
+     */
+    public void reconfigure(@NonNull PersistableBundle params) {
+        if (mState != State.ACTIVE && mState != State.IDLE) {
+            throw new IllegalStateException();
+        }
+
+        try {
+            mAdapter.reconfigureRanging(mSessionHandle, params);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Stops actively ranging
+     *
+     * <p>A session that has been stopped may be resumed by calling
+     * {@link RangingSession#start(PersistableBundle)} without the need to open a new session.
+     *
+     * <p>Stopping a {@link RangingSession} is useful when the lower layers should not discard
+     * the parameters of the session, or when a session needs to be able to be resumed quickly.
+     *
+     * <p>If the {@link RangingSession} is no longer needed, use {@link RangingSession#close()} to
+     * completely close the session and allow lower layers of the stack to perform necessarily
+     * cleanup.
+     *
+     * <p>Stopped sessions may be closed by the system at any time. In such a case,
+     * {@link RangingSession.Callback#onClosed(int, PersistableBundle)} is invoked.
+     *
+     * <p>On failure to stop the session,
+     * {@link RangingSession.Callback#onStopFailed(int, PersistableBundle)} is invoked.
+     */
+    public void stop() {
+        if (mState != State.ACTIVE) {
+            throw new IllegalStateException();
+        }
+
+        try {
+            mAdapter.stopRanging(mSessionHandle);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
      * Close the ranging session
-     * <p>If this session is currently open, it will close and stop the session.
+     *
+     * <p>After calling this function, in order resume ranging, a new {@link RangingSession} must
+     * be opened by calling
+     * {@link UwbManager#openRangingSession(PersistableBundle, Executor, Callback)}.
+     *
+     * <p>If this session is currently ranging, it will stop and close the session.
      * <p>If the session is in the process of being opened, it will attempt to stop the session from
      * being opened.
      * <p>If the session is already closed, the registered
@@ -192,7 +334,7 @@
     public void close() {
         if (mState == State.CLOSED) {
             mExecutor.execute(() -> mCallback.onClosed(
-                    Callback.CLOSE_REASON_LOCAL_CLOSE_API, new PersistableBundle()));
+                    Callback.REASON_LOCAL_REQUEST, new PersistableBundle()));
             return;
         }
 
@@ -206,32 +348,114 @@
     /**
      * @hide
      */
+    public void onRangingOpened() {
+        if (mState == State.CLOSED) {
+            Log.w(TAG, "onRangingOpened invoked for a closed session");
+            return;
+        }
+
+        mState = State.IDLE;
+        executeCallback(() -> mCallback.onOpened(this));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingOpenFailed(@Callback.Reason int reason,
+            @NonNull PersistableBundle params) {
+        if (mState == State.CLOSED) {
+            Log.w(TAG, "onRangingOpenFailed invoked for a closed session");
+            return;
+        }
+
+        mState = State.CLOSED;
+        executeCallback(() -> mCallback.onOpenFailed(reason, params));
+    }
+
+    /**
+     * @hide
+     */
     public void onRangingStarted(@NonNull PersistableBundle parameters) {
         if (mState == State.CLOSED) {
             Log.w(TAG, "onRangingStarted invoked for a closed session");
             return;
         }
 
-        mState = State.OPEN;
-        final long identity = Binder.clearCallingIdentity();
-        try {
-            mExecutor.execute(() -> mCallback.onOpenSuccess(this, parameters));
-        } finally {
-            Binder.restoreCallingIdentity(identity);
-        }
+        mState = State.ACTIVE;
+        executeCallback(() -> mCallback.onStarted(parameters));
     }
 
     /**
      * @hide
      */
-    public void onRangingClosed(@Callback.CloseReason int reason, PersistableBundle parameters) {
-        mState = State.CLOSED;
-        final long identity = Binder.clearCallingIdentity();
-        try {
-            mExecutor.execute(() -> mCallback.onClosed(reason, parameters));
-        } finally {
-            Binder.restoreCallingIdentity(identity);
+    public void onRangingStartFailed(@Callback.Reason int reason,
+            @NonNull PersistableBundle params) {
+        if (mState == State.CLOSED) {
+            Log.w(TAG, "onRangingStartFailed invoked for a closed session");
+            return;
         }
+
+        executeCallback(() -> mCallback.onStartFailed(reason, params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingReconfigured(@NonNull PersistableBundle params) {
+        if (mState == State.CLOSED) {
+            Log.w(TAG, "onRangingReconfigured invoked for a closed session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onReconfigured(params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingReconfigureFailed(@Callback.Reason int reason,
+            @NonNull PersistableBundle params) {
+        if (mState == State.CLOSED) {
+            Log.w(TAG, "onRangingReconfigureFailed invoked for a closed session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onReconfigureFailed(reason, params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingStopped() {
+        if (mState == State.CLOSED) {
+            Log.w(TAG, "onRangingStopped invoked for a closed session");
+            return;
+        }
+
+        mState = State.IDLE;
+        executeCallback(() -> mCallback.onStopped());
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingStopFailed(@Callback.Reason int reason,
+            @NonNull PersistableBundle params) {
+        if (mState == State.CLOSED) {
+            Log.w(TAG, "onRangingStopFailed invoked for a closed session");
+            return;
+        }
+
+        executeCallback(() -> mCallback.onStopFailed(reason, params));
+    }
+
+    /**
+     * @hide
+     */
+    public void onRangingClosed(@Callback.Reason int reason,
+            @NonNull PersistableBundle parameters) {
+        mState = State.CLOSED;
+        executeCallback(() -> mCallback.onClosed(reason, parameters));
     }
 
     /**
@@ -243,9 +467,16 @@
             return;
         }
 
+        executeCallback(() -> mCallback.onReportReceived(report));
+    }
+
+    /**
+     * @hide
+     */
+    private void executeCallback(@NonNull Runnable runnable) {
         final long identity = Binder.clearCallingIdentity();
         try {
-            mExecutor.execute(() -> mCallback.onReportReceived(report));
+            mExecutor.execute(runnable);
         } finally {
             Binder.restoreCallingIdentity(identity);
         }
diff --git a/core/java/android/uwb/StartFailureReason.aidl b/core/java/android/uwb/StartFailureReason.aidl
deleted file mode 100644
index 4d9c962..0000000
--- a/core/java/android/uwb/StartFailureReason.aidl
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright 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 android.uwb;
-
-/**
- * @hide
- */
-@Backing(type="int")
-enum StartFailureReason {
-  /**
-   * Unknown start failure reason
-   */
-  UNKNOWN,
-
-  /**
-   * The provided parameters were invalid and ranging could not start
-   */
-  BAD_PARAMETERS,
-
-  /**
-   * The maximum number of sessions has been reached. This error may be generated
-   * for an active session if a higher priority session begins.
-   */
-  MAX_SESSIONS_REACHED,
-
-  /**
-   * The system state has changed resulting in the session ending (e.g. the user
-   * disables UWB, or the user's locale changes and an active channel is no longer
-   * permitted to be used).
-   */
-  SYSTEM_POLICY,
-
-  /**
-   * The session could not start because of a protocol specific reason.
-   */
-  PROTOCOL_SPECIFIC,
-}
-
diff --git a/core/java/android/uwb/UwbManager.java b/core/java/android/uwb/UwbManager.java
index f4d8018..15ee5b5 100644
--- a/core/java/android/uwb/UwbManager.java
+++ b/core/java/android/uwb/UwbManager.java
@@ -369,13 +369,13 @@
 
     /**
      * Open a {@link RangingSession} with the given parameters
-     * <p>This function is asynchronous and will return before ranging begins. The
-     * {@link RangingSession.Callback#onOpenSuccess(RangingSession, PersistableBundle)} function is
-     * called with a {@link RangingSession} object used to control ranging when the session is
-     * successfully opened.
+     * <p>The {@link RangingSession.Callback#onOpened(RangingSession)} function is called with a
+     * {@link RangingSession} object used to control ranging when the session is successfully
+     * opened.
      *
-     * <p>If a session cannot be opened, then {@link RangingSession.Callback#onClosed(int)} will be
-     * invoked with the appropriate {@link RangingSession.Callback.CloseReason}.
+     * <p>If a session cannot be opened, then
+     * {@link RangingSession.Callback#onClosed(int, PersistableBundle)} will be invoked with the
+     * appropriate {@link RangingSession.Callback.Reason}.
      *
      * <p>An open {@link RangingSession} will be automatically closed if client application process
      * dies.
@@ -391,7 +391,7 @@
      * @return an {@link AutoCloseable} that is able to be used to close or cancel the opening of a
      *         {@link RangingSession} that has been requested through {@link #openRangingSession}
      *         but has not yet been made available by
-     *         {@link RangingSession.Callback#onOpenSuccess}.
+     *         {@link RangingSession.Callback#onOpened(RangingSession)}.
      */
     @NonNull
     public AutoCloseable openRangingSession(@NonNull PersistableBundle parameters,
diff --git a/core/tests/uwbtests/src/android/uwb/RangingManagerTest.java b/core/tests/uwbtests/src/android/uwb/RangingManagerTest.java
index 6df1c3e..c01bb75 100644
--- a/core/tests/uwbtests/src/android/uwb/RangingManagerTest.java
+++ b/core/tests/uwbtests/src/android/uwb/RangingManagerTest.java
@@ -45,14 +45,14 @@
     private static final IUwbAdapter ADAPTER = mock(IUwbAdapter.class);
     private static final Executor EXECUTOR = UwbTestUtils.getExecutor();
     private static final PersistableBundle PARAMS = new PersistableBundle();
-    private static final @CloseReason int CLOSE_REASON = CloseReason.UNKNOWN;
+    private static final @RangingChangeReason int REASON = RangingChangeReason.UNKNOWN;
 
     @Test
-    public void testOpenSession_StartRangingInvoked() throws RemoteException {
+    public void testOpenSession_OpenRangingInvoked() throws RemoteException {
         RangingManager rangingManager = new RangingManager(ADAPTER);
         RangingSession.Callback callback = mock(RangingSession.Callback.class);
         rangingManager.openSession(PARAMS, EXECUTOR, callback);
-        verify(ADAPTER, times(1)).startRanging(eq(rangingManager), eq(PARAMS));
+        verify(ADAPTER, times(1)).openRanging(eq(rangingManager), eq(PARAMS));
     }
 
     @Test
@@ -60,7 +60,7 @@
         RangingManager rangingManager = new RangingManager(ADAPTER);
         RangingSession.Callback callback = mock(RangingSession.Callback.class);
         SessionHandle handle = new SessionHandle(1);
-        when(ADAPTER.startRanging(any(), any())).thenReturn(handle);
+        when(ADAPTER.openRanging(any(), any())).thenReturn(handle);
 
         rangingManager.openSession(PARAMS, EXECUTOR, callback);
 
@@ -73,34 +73,34 @@
     }
 
     @Test
-    public void testOnRangingStarted_ValidSessionHandle() throws RemoteException {
+    public void testOnRangingOpened_ValidSessionHandle() throws RemoteException {
         RangingManager rangingManager = new RangingManager(ADAPTER);
         RangingSession.Callback callback = mock(RangingSession.Callback.class);
         SessionHandle handle = new SessionHandle(1);
-        when(ADAPTER.startRanging(any(), any())).thenReturn(handle);
+        when(ADAPTER.openRanging(any(), any())).thenReturn(handle);
 
         rangingManager.openSession(PARAMS, EXECUTOR, callback);
-        rangingManager.onRangingStarted(handle, PARAMS);
-        verify(callback, times(1)).onOpenSuccess(any(), any());
+        rangingManager.onRangingOpened(handle);
+        verify(callback, times(1)).onOpened(any());
     }
 
     @Test
-    public void testOnRangingStarted_InvalidSessionHandle() throws RemoteException {
+    public void testOnRangingOpened_InvalidSessionHandle() throws RemoteException {
         RangingManager rangingManager = new RangingManager(ADAPTER);
         RangingSession.Callback callback = mock(RangingSession.Callback.class);
 
-        rangingManager.onRangingStarted(new SessionHandle(2), PARAMS);
-        verify(callback, times(0)).onOpenSuccess(any(), any());
+        rangingManager.onRangingOpened(new SessionHandle(2));
+        verify(callback, times(0)).onOpened(any());
     }
 
     @Test
-    public void testOnRangingStarted_MultipleSessionsRegistered() throws RemoteException {
+    public void testOnRangingOpened_MultipleSessionsRegistered() throws RemoteException {
         SessionHandle sessionHandle1 = new SessionHandle(1);
         SessionHandle sessionHandle2 = new SessionHandle(2);
         RangingSession.Callback callback1 = mock(RangingSession.Callback.class);
         RangingSession.Callback callback2 = mock(RangingSession.Callback.class);
 
-        when(ADAPTER.startRanging(any(), any()))
+        when(ADAPTER.openRanging(any(), any()))
                 .thenReturn(sessionHandle1)
                 .thenReturn(sessionHandle2);
 
@@ -108,25 +108,50 @@
         rangingManager.openSession(PARAMS, EXECUTOR, callback1);
         rangingManager.openSession(PARAMS, EXECUTOR, callback2);
 
-        rangingManager.onRangingStarted(sessionHandle1, PARAMS);
-        verify(callback1, times(1)).onOpenSuccess(any(), any());
-        verify(callback2, times(0)).onOpenSuccess(any(), any());
+        rangingManager.onRangingOpened(sessionHandle1);
+        verify(callback1, times(1)).onOpened(any());
+        verify(callback2, times(0)).onOpened(any());
 
-        rangingManager.onRangingStarted(sessionHandle2, PARAMS);
-        verify(callback1, times(1)).onOpenSuccess(any(), any());
-        verify(callback2, times(1)).onOpenSuccess(any(), any());
+        rangingManager.onRangingOpened(sessionHandle2);
+        verify(callback1, times(1)).onOpened(any());
+        verify(callback2, times(1)).onOpened(any());
     }
 
     @Test
-    public void testOnRangingClosed_OnRangingClosedCalled() throws RemoteException {
+    public void testCorrectCallbackInvoked() throws RemoteException {
         RangingManager rangingManager = new RangingManager(ADAPTER);
         RangingSession.Callback callback = mock(RangingSession.Callback.class);
         SessionHandle handle = new SessionHandle(1);
-        when(ADAPTER.startRanging(any(), any())).thenReturn(handle);
-        rangingManager.openSession(PARAMS, EXECUTOR, callback);
+        when(ADAPTER.openRanging(any(), any())).thenReturn(handle);
 
-        rangingManager.onRangingClosed(handle, CLOSE_REASON, PARAMS);
-        verify(callback, times(1)).onClosed(anyInt(), any());
+        rangingManager.openSession(PARAMS, EXECUTOR, callback);
+        rangingManager.onRangingOpened(handle);
+        verify(callback, times(1)).onOpened(any());
+
+        rangingManager.onRangingStarted(handle, PARAMS);
+        verify(callback, times(1)).onStarted(eq(PARAMS));
+
+        rangingManager.onRangingStartFailed(handle, REASON, PARAMS);
+        verify(callback, times(1)).onStartFailed(eq(REASON), eq(PARAMS));
+
+        RangingReport report = UwbTestUtils.getRangingReports(1);
+        rangingManager.onRangingResult(handle, report);
+        verify(callback, times(1)).onReportReceived(eq(report));
+
+        rangingManager.onRangingReconfigured(handle, PARAMS);
+        verify(callback, times(1)).onReconfigured(eq(PARAMS));
+
+        rangingManager.onRangingReconfigureFailed(handle, REASON, PARAMS);
+        verify(callback, times(1)).onReconfigureFailed(eq(REASON), eq(PARAMS));
+
+        rangingManager.onRangingStopped(handle);
+        verify(callback, times(1)).onStopped();
+
+        rangingManager.onRangingStopFailed(handle, REASON, PARAMS);
+        verify(callback, times(1)).onStopFailed(eq(REASON), eq(PARAMS));
+
+        rangingManager.onRangingClosed(handle, REASON, PARAMS);
+        verify(callback, times(1)).onClosed(eq(REASON), eq(PARAMS));
     }
 
     @Test
@@ -138,7 +163,7 @@
         RangingSession.Callback callback1 = mock(RangingSession.Callback.class);
         RangingSession.Callback callback2 = mock(RangingSession.Callback.class);
 
-        when(ADAPTER.startRanging(any(), any()))
+        when(ADAPTER.openRanging(any(), any()))
                 .thenReturn(sessionHandle1)
                 .thenReturn(sessionHandle2);
 
@@ -146,37 +171,23 @@
         rangingManager.openSession(PARAMS, EXECUTOR, callback1);
         rangingManager.openSession(PARAMS, EXECUTOR, callback2);
 
-        rangingManager.onRangingClosed(sessionHandle1, CLOSE_REASON, PARAMS);
+        rangingManager.onRangingClosed(sessionHandle1, REASON, PARAMS);
         verify(callback1, times(1)).onClosed(anyInt(), any());
         verify(callback2, times(0)).onClosed(anyInt(), any());
 
-        rangingManager.onRangingClosed(sessionHandle2, CLOSE_REASON, PARAMS);
+        rangingManager.onRangingClosed(sessionHandle2, REASON, PARAMS);
         verify(callback1, times(1)).onClosed(anyInt(), any());
         verify(callback2, times(1)).onClosed(anyInt(), any());
     }
 
     @Test
-    public void testOnRangingReport_OnReportReceived() throws RemoteException {
-        RangingManager rangingManager = new RangingManager(ADAPTER);
-        RangingSession.Callback callback = mock(RangingSession.Callback.class);
-        SessionHandle handle = new SessionHandle(1);
-        when(ADAPTER.startRanging(any(), any())).thenReturn(handle);
-        rangingManager.openSession(PARAMS, EXECUTOR, callback);
-        rangingManager.onRangingStarted(handle, PARAMS);
-
-        RangingReport report = UwbTestUtils.getRangingReports(1);
-        rangingManager.onRangingResult(handle, report);
-        verify(callback, times(1)).onReportReceived(eq(report));
-    }
-
-    @Test
     public void testOnRangingReport_MultipleSessionsRegistered() throws RemoteException {
         SessionHandle sessionHandle1 = new SessionHandle(1);
         SessionHandle sessionHandle2 = new SessionHandle(2);
         RangingSession.Callback callback1 = mock(RangingSession.Callback.class);
         RangingSession.Callback callback2 = mock(RangingSession.Callback.class);
 
-        when(ADAPTER.startRanging(any(), any()))
+        when(ADAPTER.openRanging(any(), any()))
                 .thenReturn(sessionHandle1)
                 .thenReturn(sessionHandle2);
 
@@ -196,65 +207,54 @@
     }
 
     @Test
-    public void testOnClose_Reasons() throws RemoteException {
-        runOnClose_Reason(CloseReason.LOCAL_API,
-                RangingSession.Callback.CLOSE_REASON_LOCAL_CLOSE_API);
+    public void testReasons() throws RemoteException {
+        runReason(RangingChangeReason.LOCAL_API,
+                RangingSession.Callback.REASON_LOCAL_REQUEST);
 
-        runOnClose_Reason(CloseReason.MAX_SESSIONS_REACHED,
-                RangingSession.Callback.CLOSE_REASON_LOCAL_MAX_SESSIONS_REACHED);
+        runReason(RangingChangeReason.MAX_SESSIONS_REACHED,
+                RangingSession.Callback.REASON_MAX_SESSIONS_REACHED);
 
-        runOnClose_Reason(CloseReason.PROTOCOL_SPECIFIC,
-                RangingSession.Callback.CLOSE_REASON_PROTOCOL_SPECIFIC);
+        runReason(RangingChangeReason.PROTOCOL_SPECIFIC,
+                RangingSession.Callback.REASON_PROTOCOL_SPECIFIC_ERROR);
 
-        runOnClose_Reason(CloseReason.REMOTE_REQUEST,
-                RangingSession.Callback.CLOSE_REASON_REMOTE_REQUEST);
+        runReason(RangingChangeReason.REMOTE_REQUEST,
+                RangingSession.Callback.REASON_REMOTE_REQUEST);
 
-        runOnClose_Reason(CloseReason.SYSTEM_POLICY,
-                RangingSession.Callback.CLOSE_REASON_LOCAL_SYSTEM_POLICY);
+        runReason(RangingChangeReason.SYSTEM_POLICY,
+                RangingSession.Callback.REASON_SYSTEM_POLICY);
 
-        runOnClose_Reason(CloseReason.UNKNOWN,
-                RangingSession.Callback.CLOSE_REASON_UNKNOWN);
+        runReason(RangingChangeReason.BAD_PARAMETERS,
+                RangingSession.Callback.REASON_BAD_PARAMETERS);
+
+        runReason(RangingChangeReason.UNKNOWN,
+                RangingSession.Callback.REASON_UNKNOWN);
     }
 
-    private void runOnClose_Reason(@CloseReason int reasonIn,
-            @RangingSession.Callback.CloseReason int reasonOut) throws RemoteException {
+    private void runReason(@RangingChangeReason int reasonIn,
+            @RangingSession.Callback.Reason int reasonOut) throws RemoteException {
         RangingManager rangingManager = new RangingManager(ADAPTER);
         RangingSession.Callback callback = mock(RangingSession.Callback.class);
         SessionHandle handle = new SessionHandle(1);
-        when(ADAPTER.startRanging(any(), any())).thenReturn(handle);
+        when(ADAPTER.openRanging(any(), any())).thenReturn(handle);
         rangingManager.openSession(PARAMS, EXECUTOR, callback);
 
-        rangingManager.onRangingClosed(handle, reasonIn, PARAMS);
-        verify(callback, times(1)).onClosed(eq(reasonOut), eq(PARAMS));
-    }
+        rangingManager.onRangingOpenFailed(handle, reasonIn, PARAMS);
+        verify(callback, times(1)).onOpenFailed(eq(reasonOut), eq(PARAMS));
 
-    @Test
-    public void testStartFailureReasons() throws RemoteException {
-        runOnRangingStartFailed_Reason(StartFailureReason.BAD_PARAMETERS,
-                RangingSession.Callback.CLOSE_REASON_LOCAL_BAD_PARAMETERS);
-
-        runOnRangingStartFailed_Reason(StartFailureReason.MAX_SESSIONS_REACHED,
-                RangingSession.Callback.CLOSE_REASON_LOCAL_MAX_SESSIONS_REACHED);
-
-        runOnRangingStartFailed_Reason(StartFailureReason.PROTOCOL_SPECIFIC,
-                RangingSession.Callback.CLOSE_REASON_PROTOCOL_SPECIFIC);
-
-        runOnRangingStartFailed_Reason(StartFailureReason.SYSTEM_POLICY,
-                RangingSession.Callback.CLOSE_REASON_LOCAL_SYSTEM_POLICY);
-
-        runOnRangingStartFailed_Reason(StartFailureReason.UNKNOWN,
-                RangingSession.Callback.CLOSE_REASON_UNKNOWN);
-    }
-
-    private void runOnRangingStartFailed_Reason(@StartFailureReason int reasonIn,
-            @RangingSession.Callback.CloseReason int reasonOut) throws RemoteException {
-        RangingManager rangingManager = new RangingManager(ADAPTER);
-        RangingSession.Callback callback = mock(RangingSession.Callback.class);
-        SessionHandle handle = new SessionHandle(1);
-        when(ADAPTER.startRanging(any(), any())).thenReturn(handle);
+        // Open a new session
         rangingManager.openSession(PARAMS, EXECUTOR, callback);
+        rangingManager.onRangingOpened(handle);
 
         rangingManager.onRangingStartFailed(handle, reasonIn, PARAMS);
+        verify(callback, times(1)).onStartFailed(eq(reasonOut), eq(PARAMS));
+
+        rangingManager.onRangingReconfigureFailed(handle, reasonIn, PARAMS);
+        verify(callback, times(1)).onReconfigureFailed(eq(reasonOut), eq(PARAMS));
+
+        rangingManager.onRangingStopFailed(handle, reasonIn, PARAMS);
+        verify(callback, times(1)).onStopFailed(eq(reasonOut), eq(PARAMS));
+
+        rangingManager.onRangingClosed(handle, reasonIn, PARAMS);
         verify(callback, times(1)).onClosed(eq(reasonOut), eq(PARAMS));
     }
 }
diff --git a/core/tests/uwbtests/src/android/uwb/RangingSessionTest.java b/core/tests/uwbtests/src/android/uwb/RangingSessionTest.java
index 702c68e..e5eea26 100644
--- a/core/tests/uwbtests/src/android/uwb/RangingSessionTest.java
+++ b/core/tests/uwbtests/src/android/uwb/RangingSessionTest.java
@@ -19,9 +19,11 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -34,6 +36,8 @@
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
 
 import java.util.concurrent.Executor;
 
@@ -43,47 +47,48 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class RangingSessionTest {
-    private static final IUwbAdapter ADAPTER = mock(IUwbAdapter.class);
     private static final Executor EXECUTOR = UwbTestUtils.getExecutor();
     private static final PersistableBundle PARAMS = new PersistableBundle();
-    private static final @RangingSession.Callback.CloseReason int CLOSE_REASON =
-            RangingSession.Callback.CLOSE_REASON_LOCAL_GENERIC_ERROR;
+    private static final @RangingSession.Callback.Reason int REASON =
+            RangingSession.Callback.REASON_GENERIC_ERROR;
 
     @Test
-    public void testOnRangingStarted_OnOpenSuccessCalled() {
+    public void testOnRangingOpened_OnOpenSuccessCalled() {
         SessionHandle handle = new SessionHandle(123);
         RangingSession.Callback callback = mock(RangingSession.Callback.class);
-        RangingSession session = new RangingSession(EXECUTOR, callback, ADAPTER, handle);
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
         verifyOpenState(session, false);
 
-        session.onRangingStarted(PARAMS);
+        session.onRangingOpened();
         verifyOpenState(session, true);
 
         // Verify that the onOpenSuccess callback was invoked
-        verify(callback, times(1)).onOpenSuccess(eq(session), any());
+        verify(callback, times(1)).onOpened(eq(session));
         verify(callback, times(0)).onClosed(anyInt(), any());
     }
 
     @Test
-    public void testOnRangingStarted_CannotOpenClosedSession() {
+    public void testOnRangingOpened_CannotOpenClosedSession() {
         SessionHandle handle = new SessionHandle(123);
         RangingSession.Callback callback = mock(RangingSession.Callback.class);
-        RangingSession session = new RangingSession(EXECUTOR, callback, ADAPTER, handle);
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
 
-        session.onRangingStarted(PARAMS);
+        session.onRangingOpened();
         verifyOpenState(session, true);
-        verify(callback, times(1)).onOpenSuccess(eq(session), any());
+        verify(callback, times(1)).onOpened(eq(session));
         verify(callback, times(0)).onClosed(anyInt(), any());
 
-        session.onRangingClosed(CLOSE_REASON, PARAMS);
+        session.onRangingClosed(REASON, PARAMS);
         verifyOpenState(session, false);
-        verify(callback, times(1)).onOpenSuccess(eq(session), any());
+        verify(callback, times(1)).onOpened(eq(session));
         verify(callback, times(1)).onClosed(anyInt(), any());
 
         // Now invoke the ranging started callback and ensure the session remains closed
-        session.onRangingStarted(PARAMS);
+        session.onRangingOpened();
         verifyOpenState(session, false);
-        verify(callback, times(1)).onOpenSuccess(eq(session), any());
+        verify(callback, times(1)).onOpened(eq(session));
         verify(callback, times(1)).onClosed(anyInt(), any());
     }
 
@@ -91,27 +96,30 @@
     public void testOnRangingClosed_OnClosedCalledWhenSessionNotOpen() {
         SessionHandle handle = new SessionHandle(123);
         RangingSession.Callback callback = mock(RangingSession.Callback.class);
-        RangingSession session = new RangingSession(EXECUTOR, callback, ADAPTER, handle);
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
         verifyOpenState(session, false);
 
-        session.onRangingClosed(CLOSE_REASON, PARAMS);
+        session.onRangingClosed(REASON, PARAMS);
         verifyOpenState(session, false);
 
         // Verify that the onOpenSuccess callback was invoked
-        verify(callback, times(0)).onOpenSuccess(eq(session), any());
+        verify(callback, times(0)).onOpened(eq(session));
         verify(callback, times(1)).onClosed(anyInt(), any());
     }
 
-    @Test public void testOnRangingClosed_OnClosedCalled() {
+    @Test
+    public void testOnRangingClosed_OnClosedCalled() {
         SessionHandle handle = new SessionHandle(123);
         RangingSession.Callback callback = mock(RangingSession.Callback.class);
-        RangingSession session = new RangingSession(EXECUTOR, callback, ADAPTER, handle);
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
         session.onRangingStarted(PARAMS);
-        session.onRangingClosed(CLOSE_REASON, PARAMS);
+        session.onRangingClosed(REASON, PARAMS);
         verify(callback, times(1)).onClosed(anyInt(), any());
 
         verifyOpenState(session, false);
-        session.onRangingClosed(CLOSE_REASON, PARAMS);
+        session.onRangingClosed(REASON, PARAMS);
         verify(callback, times(2)).onClosed(anyInt(), any());
     }
 
@@ -119,7 +127,8 @@
     public void testOnRangingResult_OnReportReceivedCalled() {
         SessionHandle handle = new SessionHandle(123);
         RangingSession.Callback callback = mock(RangingSession.Callback.class);
-        RangingSession session = new RangingSession(EXECUTOR, callback, ADAPTER, handle);
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
         verifyOpenState(session, false);
 
         session.onRangingStarted(PARAMS);
@@ -131,11 +140,83 @@
     }
 
     @Test
-    public void testClose() throws RemoteException {
+    public void testStart_CannotStartIfAlreadyStarted() throws RemoteException {
         SessionHandle handle = new SessionHandle(123);
         RangingSession.Callback callback = mock(RangingSession.Callback.class);
-        RangingSession session = new RangingSession(EXECUTOR, callback, ADAPTER, handle);
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
+        doAnswer(new StartAnswer(session)).when(adapter).startRanging(any(), any());
+        session.onRangingOpened();
+
+        session.start(PARAMS);
+        verify(callback, times(1)).onStarted(any());
+
+        // Calling start again should throw an illegal state
+        verifyThrowIllegalState(() -> session.start(PARAMS));
+        verify(callback, times(1)).onStarted(any());
+    }
+
+    @Test
+    public void testStop_CannotStopIfAlreadyStopped() throws RemoteException {
+        SessionHandle handle = new SessionHandle(123);
+        RangingSession.Callback callback = mock(RangingSession.Callback.class);
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
+        doAnswer(new StartAnswer(session)).when(adapter).startRanging(any(), any());
+        doAnswer(new StopAnswer(session)).when(adapter).stopRanging(any());
+        session.onRangingOpened();
+        session.start(PARAMS);
+
+        verifyNoThrowIllegalState(session::stop);
+        verify(callback, times(1)).onStopped();
+
+        // Calling stop again should throw an illegal state
+        verifyThrowIllegalState(session::stop);
+        verify(callback, times(1)).onStopped();
+    }
+
+    @Test
+    public void testReconfigure_OnlyWhenOpened() throws RemoteException {
+        SessionHandle handle = new SessionHandle(123);
+        RangingSession.Callback callback = mock(RangingSession.Callback.class);
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
+        doAnswer(new StartAnswer(session)).when(adapter).startRanging(any(), any());
+        doAnswer(new ReconfigureAnswer(session)).when(adapter).reconfigureRanging(any(), any());
+
+        verifyThrowIllegalState(() -> session.reconfigure(PARAMS));
+        verify(callback, times(0)).onReconfigured(any());
+        verifyOpenState(session, false);
+
+        session.onRangingOpened();
+        verifyNoThrowIllegalState(() -> session.reconfigure(PARAMS));
+        verify(callback, times(1)).onReconfigured(any());
+        verifyOpenState(session, true);
+
         session.onRangingStarted(PARAMS);
+        verifyNoThrowIllegalState(() -> session.reconfigure(PARAMS));
+        verify(callback, times(2)).onReconfigured(any());
+        verifyOpenState(session, true);
+
+        session.onRangingStopped();
+        verifyNoThrowIllegalState(() -> session.reconfigure(PARAMS));
+        verify(callback, times(3)).onReconfigured(any());
+        verifyOpenState(session, true);
+
+
+        session.onRangingClosed(REASON, PARAMS);
+        verifyThrowIllegalState(() -> session.reconfigure(PARAMS));
+        verify(callback, times(3)).onReconfigured(any());
+        verifyOpenState(session, false);
+    }
+
+    @Test
+    public void testClose_NoCallbackUntilInvoked() throws RemoteException {
+        SessionHandle handle = new SessionHandle(123);
+        RangingSession.Callback callback = mock(RangingSession.Callback.class);
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
+        session.onRangingOpened();
 
         // Calling close multiple times should invoke closeRanging until the session receives
         // the onClosed callback.
@@ -143,7 +224,7 @@
         for (int i = 1; i <= totalCallsBeforeOnRangingClosed; i++) {
             session.close();
             verifyOpenState(session, true);
-            verify(ADAPTER, times(i)).closeRanging(handle);
+            verify(adapter, times(i)).closeRanging(handle);
             verify(callback, times(0)).onClosed(anyInt(), any());
         }
 
@@ -151,18 +232,47 @@
         // the session's close.
         final int totalCallsAfterOnRangingClosed = 2;
         for (int i = 1; i <= totalCallsAfterOnRangingClosed; i++) {
-            session.onRangingClosed(CLOSE_REASON, PARAMS);
+            session.onRangingClosed(REASON, PARAMS);
             verifyOpenState(session, false);
-            verify(ADAPTER, times(totalCallsBeforeOnRangingClosed)).closeRanging(handle);
+            verify(adapter, times(totalCallsBeforeOnRangingClosed)).closeRanging(handle);
             verify(callback, times(i)).onClosed(anyInt(), any());
         }
     }
 
     @Test
+    public void testClose_OnClosedCalled() throws RemoteException {
+        SessionHandle handle = new SessionHandle(123);
+        RangingSession.Callback callback = mock(RangingSession.Callback.class);
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
+        doAnswer(new CloseAnswer(session)).when(adapter).closeRanging(any());
+        session.onRangingOpened();
+
+        session.close();
+        verify(callback, times(1)).onClosed(anyInt(), any());
+    }
+
+    @Test
+    public void testClose_CannotInteractFurther() throws RemoteException {
+        SessionHandle handle = new SessionHandle(123);
+        RangingSession.Callback callback = mock(RangingSession.Callback.class);
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
+        doAnswer(new CloseAnswer(session)).when(adapter).closeRanging(any());
+        session.close();
+
+        verifyThrowIllegalState(() -> session.start(PARAMS));
+        verifyThrowIllegalState(() -> session.reconfigure(PARAMS));
+        verifyThrowIllegalState(() -> session.stop());
+        verifyNoThrowIllegalState(() -> session.close());
+    }
+
+    @Test
     public void testOnRangingResult_OnReportReceivedCalledWhenOpen() {
         SessionHandle handle = new SessionHandle(123);
         RangingSession.Callback callback = mock(RangingSession.Callback.class);
-        RangingSession session = new RangingSession(EXECUTOR, callback, ADAPTER, handle);
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
 
         assertFalse(session.isOpen());
         session.onRangingStarted(PARAMS);
@@ -178,7 +288,8 @@
     public void testOnRangingResult_OnReportReceivedNotCalledWhenNotOpen() {
         SessionHandle handle = new SessionHandle(123);
         RangingSession.Callback callback = mock(RangingSession.Callback.class);
-        RangingSession session = new RangingSession(EXECUTOR, callback, ADAPTER, handle);
+        IUwbAdapter adapter = mock(IUwbAdapter.class);
+        RangingSession session = new RangingSession(EXECUTOR, callback, adapter, handle);
 
         assertFalse(session.isOpen());
 
@@ -191,4 +302,77 @@
     private void verifyOpenState(RangingSession session, boolean expected) {
         assertEquals(expected, session.isOpen());
     }
+
+    private void verifyThrowIllegalState(Runnable runnable) {
+        try {
+            runnable.run();
+            fail();
+        } catch (IllegalStateException e) {
+            // Pass
+        }
+    }
+
+    private void verifyNoThrowIllegalState(Runnable runnable) {
+        try {
+            runnable.run();
+        } catch (IllegalStateException e) {
+            fail();
+        }
+    }
+
+    abstract class AdapterAnswer implements Answer {
+        protected RangingSession mSession;
+
+        protected AdapterAnswer(RangingSession session) {
+            mSession = session;
+        }
+    }
+
+    class StartAnswer extends AdapterAnswer {
+        StartAnswer(RangingSession session) {
+            super(session);
+        }
+
+        @Override
+        public Object answer(InvocationOnMock invocation) {
+            mSession.onRangingStarted(PARAMS);
+            return null;
+        }
+    }
+
+    class ReconfigureAnswer extends AdapterAnswer {
+        ReconfigureAnswer(RangingSession session) {
+            super(session);
+        }
+
+        @Override
+        public Object answer(InvocationOnMock invocation) {
+            mSession.onRangingReconfigured(PARAMS);
+            return null;
+        }
+    }
+
+    class StopAnswer extends AdapterAnswer {
+        StopAnswer(RangingSession session) {
+            super(session);
+        }
+
+        @Override
+        public Object answer(InvocationOnMock invocation) {
+            mSession.onRangingStopped();
+            return null;
+        }
+    }
+
+    class CloseAnswer extends AdapterAnswer {
+        CloseAnswer(RangingSession session) {
+            super(session);
+        }
+
+        @Override
+        public Object answer(InvocationOnMock invocation) {
+            mSession.onRangingClosed(REASON, PARAMS);
+            return null;
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index 74e3851..c191a78 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -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 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.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 @@
     @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 @@
     @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 @@
         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 @@
                             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 @@
             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 @@
                 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 @@
                 "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 @@
     @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 @@
         }
     }
 
-    /**
-     * 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);
         }
     }
 }
diff --git a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
index c060807..d8a145d9 100644
--- a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
+++ b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java
@@ -36,6 +36,8 @@
 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 @@
     @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 @@
         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 @@
      * 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 @@
             // 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 @@
     /** 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 @@
 
         @Override
         public int hashCode() {
-            return Objects.hash(mSubIdToGroupMap, mActiveGroups);
+            return Objects.hash(mSubIdToGroupMap, mPrivilegedPackages);
         }
 
         @Override
@@ -285,7 +317,15 @@
             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
+                    + " }";
         }
     }
 
diff --git a/services/core/java/com/android/server/vcn/Vcn.java b/services/core/java/com/android/server/vcn/Vcn.java
index d51d16b..9d21b92 100644
--- a/services/core/java/com/android/server/vcn/Vcn.java
+++ b/services/core/java/com/android/server/vcn/Vcn.java
@@ -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 @@
         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 */
diff --git a/services/core/java/com/android/server/vcn/VcnContext.java b/services/core/java/com/android/server/vcn/VcnContext.java
index 8ab52931..dba59bd 100644
--- a/services/core/java/com/android/server/vcn/VcnContext.java
+++ b/services/core/java/com/android/server/vcn/VcnContext.java
@@ -20,8 +20,6 @@
 import android.content.Context;
 import android.os.Looper;
 
-import com.android.server.VcnManagementService.VcnNetworkProvider;
-
 import java.util.Objects;
 
 /**
diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
index 49c9b32..7ea8e04 100644
--- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
+++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java
@@ -65,8 +65,8 @@
                 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();
     }
 
diff --git a/services/core/java/com/android/server/vcn/VcnNetworkProvider.java b/services/core/java/com/android/server/vcn/VcnNetworkProvider.java
new file mode 100644
index 0000000..7f5b23c
--- /dev/null
+++ b/services/core/java/com/android/server/vcn/VcnNetworkProvider.java
@@ -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);
+    }
+}
diff --git a/tests/vcn/java/android/net/vcn/VcnConfigTest.java b/tests/vcn/java/android/net/vcn/VcnConfigTest.java
index 77944de..c1ef350 100644
--- a/tests/vcn/java/android/net/vcn/VcnConfigTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnConfigTest.java
@@ -18,12 +18,17 @@
 
 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 @@
 @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 @@
         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 @@
 
     @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);
diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
index 1cc9532..696110f 100644
--- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
+++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
@@ -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 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 @@
     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 @@
 
         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 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 @@
         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 @@
 
     @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 @@
         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 @@
 
     @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 @@
         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();
+    }
 }
diff --git a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
index 17b8f64..528f240 100644
--- a/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
+++ b/tests/vcn/java/com/android/server/vcn/TelephonySubscriptionTrackerTest.java
@@ -30,6 +30,7 @@
 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.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.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.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 @@
 @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 @@
     @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 @@
         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 @@
         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 @@
         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 @@
                 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 @@
     }
 
     @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 @@
         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 @@
         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 @@
 
     @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 @@
     @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)),
diff --git a/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java b/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java
new file mode 100644
index 0000000..c2c6200
--- /dev/null
+++ b/tests/vcn/java/com/android/server/vcn/VcnNetworkProviderTest.java
@@ -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);
+    }
+}