Snap for 11273583 from 32c7e888ba8ff0dcf3dcb5ca421c7eece08ff569 to mainline-ipsec-release

Change-Id: I0d01f139a11673e7d1dd4698a303eb23f80f91e9
diff --git a/OWNERS b/OWNERS
index 01955db..2cbf527 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,3 +1,4 @@
+# Bug component: 1084908
 sethmo@google.com
 vikramgaur@google.com
 
diff --git a/apex/Android.bp b/apex/Android.bp
index aaa8cd8..f465cca 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -37,7 +37,7 @@
         "com.android.rkpd-systemserverclasspath-fragment",
     ],
     updatable: true,
-    min_sdk_version: "UpsideDownCake",
+    min_sdk_version: "33",
     apps: ["rkpdapp"],
     compile_multilib: "both",
 }
diff --git a/apex/manifest.json b/apex/manifest.json
index f8decae..54e9e34 100644
--- a/apex/manifest.json
+++ b/apex/manifest.json
@@ -1,4 +1,4 @@
 {
   "name": "com.android.rkpd",
-  "version": 1
+  "version": 0
 }
diff --git a/app/Android.bp b/app/Android.bp
index 77b978e..2f7aef9 100644
--- a/app/Android.bp
+++ b/app/Android.bp
@@ -59,7 +59,8 @@
 android_app {
     name: "rkpdapp",
     sdk_version: "module_current",
-    min_sdk_version: "UpsideDownCake",
+    target_sdk_version: "34",
+    min_sdk_version: "33",
     updatable: false,
     privileged: true,
     libs: [
diff --git a/app/TEST_MAPPING b/app/TEST_MAPPING
index cb4d40b..824fc9e 100644
--- a/app/TEST_MAPPING
+++ b/app/TEST_MAPPING
@@ -4,13 +4,15 @@
       "name": "RkpdAppUnitTests"
     },
     {
-      "name": "RkpdAppGoogleUnitTests"
+      "name": "RkpdAppGoogleUnitTests",
+      "keywords": ["internal"]
     },
     {
       "name": "RkpdAppIntegrationTests"
     },
     {
-      "name": "RkpdAppGoogleIntegrationTests"
+      "name": "RkpdAppGoogleIntegrationTests",
+      "keywords": ["internal"]
     }
   ],
   "mainline-presubmit": [
diff --git a/app/proguard.flags b/app/proguard.flags
index 6339e99..2e7d09b 100644
--- a/app/proguard.flags
+++ b/app/proguard.flags
@@ -2,3 +2,14 @@
 -keep class com.android.rkpdapp.interfaces.ServiceManagerInterface { *; }
 -keep class com.android.rkpdapp.service.** { *; }
 -keep class com.android.rkpdapp.utils.Settings { *; }
+
+# Required for tests that use Mockito's thenThrow with checked exceptions.
+-keepattributes Exceptions
+
+# Minimal set of keep rules for mocked methods with checked exceptions.
+# This can be relaxed to specific packages if that simplifies testing.
+# See also https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md#r8-full-mode.
+-keepclassmembers,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class android.hardware.security.keymint.IRemotelyProvisionedComponent { public <methods>; }
+-keepclassmembers,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.android.rkpdapp.IGetRegistrationCallback { public <methods>; }
+-keepclassmembers,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.android.rkpdapp.interfaces.SystemInterface { public <methods>; }
+-keepclassmembers,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.android.rkpdapp.provisioner.Provisioner { public <methods>; }
\ No newline at end of file
diff --git a/app/src/com/android/rkpdapp/ThreadPool.java b/app/src/com/android/rkpdapp/ThreadPool.java
index 6d4b9a1..3ed7755 100644
--- a/app/src/com/android/rkpdapp/ThreadPool.java
+++ b/app/src/com/android/rkpdapp/ThreadPool.java
@@ -17,21 +17,29 @@
 package com.android.rkpdapp;
 
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 
 /**
  * This class provides a global thread pool to RKPD app.
  */
 public class ThreadPool {
-    public static final int NUMBER_OF_THREADS = Runtime.getRuntime().availableProcessors();
+    public static final int NUMBER_OF_THREADS = 4;
     /*
-     * This thread pool has a minimum of 0 threads and a maximum of up to the
-     * number of processors. If a thread is idle for more than 30 seconds, it is
-     * terminated. RKPD is idle most of the time. So, this way we can don't keep
-     * unused threads around.
-     *
-     * Each thread has an unbounded queue. This allows RKPD to serve requests
-     * asynchronously.
+     * This thread pool has a minimum of 0 threads and a maximum of up to 4. If
+     * a thread is idle for more than 60 seconds, it is terminated. RKPD is idle
+     * most of the time. So, this way we can don't keep unused threads around.
      */
-    public static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
+    public static final ExecutorService EXECUTOR;
+
+    static {
+        ThreadPoolExecutor executor =
+                new ThreadPoolExecutor(/*corePoolSize=*/ NUMBER_OF_THREADS,
+                    /*maximumPoolSize=*/ NUMBER_OF_THREADS,
+                    /*keepAliveTime=*/ 60L, /*unit=*/ TimeUnit.SECONDS,
+                    /*workQueue=*/ new LinkedBlockingQueue<Runnable>());
+        executor.allowCoreThreadTimeOut(true);
+        EXECUTOR = executor;
+    }
 }
diff --git a/app/src/com/android/rkpdapp/interfaces/ServerInterface.java b/app/src/com/android/rkpdapp/interfaces/ServerInterface.java
index a989cf6..8ce4917 100644
--- a/app/src/com/android/rkpdapp/interfaces/ServerInterface.java
+++ b/app/src/com/android/rkpdapp/interfaces/ServerInterface.java
@@ -18,8 +18,9 @@
 
 import android.content.Context;
 import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
+import android.net.NetworkCapabilities;
 import android.net.TrafficStats;
+import android.net.Uri;
 import android.util.Base64;
 import android.util.Log;
 
@@ -37,6 +38,7 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
 import java.net.SocketTimeoutException;
 import java.net.URL;
 import java.nio.charset.Charset;
@@ -53,19 +55,31 @@
  */
 public class ServerInterface {
 
+    private static final int SYNC_CONNECT_TIMEOUT_MS = 1000;
     private static final int TIMEOUT_MS = 20000;
     private static final int BACKOFF_TIME_MS = 100;
 
     private static final String TAG = "RkpdServerInterface";
     private static final String GEEK_URL = ":fetchEekChain";
-    private static final String CERTIFICATE_SIGNING_URL = ":signCertificates?";
-    private static final String CHALLENGE_PARAMETER = "challenge=";
-    private static final String REQUEST_ID_PARAMETER = "request_id=";
+    private static final String CERTIFICATE_SIGNING_URL = ":signCertificates";
+    private static final String CHALLENGE_PARAMETER = "challenge";
+    private static final String REQUEST_ID_PARAMETER = "request_id";
     private final Context mContext;
+    private final boolean mIsAsync;
 
     private enum Operation {
-        FETCH_GEEK,
-        SIGN_CERTS;
+        FETCH_GEEK(1),
+        SIGN_CERTS(2);
+
+        private final int mTrafficTag;
+
+        Operation(int trafficTag) {
+            mTrafficTag = trafficTag;
+        }
+
+        public int getTrafficTag() {
+            return mTrafficTag;
+        }
 
         public ProvisioningAttempt.Status getHttpErrorStatus() {
             if (Objects.equals(name(), FETCH_GEEK.name())) {
@@ -95,8 +109,13 @@
         }
     }
 
-    public ServerInterface(Context context) {
+    public ServerInterface(Context context, boolean isAsync) {
         this.mContext = context;
+        this.mIsAsync = isAsync;
+    }
+
+    private int getConnectTimeoutMs() {
+        return mIsAsync ? TIMEOUT_MS : SYNC_CONNECT_TIMEOUT_MS;
     }
 
     /**
@@ -114,11 +133,9 @@
      */
     public List<byte[]> requestSignedCertificates(byte[] csr, byte[] challenge,
             ProvisioningAttempt metrics) throws RkpdException, InterruptedException {
-        final String challengeParam = CHALLENGE_PARAMETER + Base64.encodeToString(challenge,
-                Base64.URL_SAFE | Base64.NO_WRAP);
-        final String fullUrl = CERTIFICATE_SIGNING_URL + String.join("&", challengeParam,
-                REQUEST_ID_PARAMETER + generateAndLogRequestId());
-        final byte[] cborBytes = connectAndGetData(metrics, fullUrl, csr, Operation.SIGN_CERTS);
+        final byte[] cborBytes =
+                connectAndGetData(metrics, generateSignCertsUrl(challenge),
+                                  csr, Operation.SIGN_CERTS);
         List<byte[]> certChains = CborUtils.parseSignedCertificates(cborBytes);
         if (certChains == null) {
             metrics.setStatus(ProvisioningAttempt.Status.INTERNAL_ERROR);
@@ -143,6 +160,22 @@
         return certChains;
     }
 
+    private URL generateSignCertsUrl(byte[] challenge) throws RkpdException {
+        try {
+            return new URL(Uri.parse(Settings.getUrl(mContext)).buildUpon()
+                    .appendEncodedPath(CERTIFICATE_SIGNING_URL)
+                    .appendQueryParameter(CHALLENGE_PARAMETER,
+                            Base64.encodeToString(challenge, Base64.URL_SAFE | Base64.NO_WRAP))
+                    .appendQueryParameter(REQUEST_ID_PARAMETER, generateAndLogRequestId())
+                    .build()
+                    .toString()
+                    // Needed due to the `:` in the URL endpoint.
+                    .replaceFirst("%3A", ":"));
+        } catch (MalformedURLException e) {
+            throw new RkpdException(RkpdException.ErrorCode.HTTP_CLIENT_ERROR, "Bad URL", e);
+        }
+    }
+
     private String generateAndLogRequestId() {
         String reqId = UUID.randomUUID().toString();
         Log.i(TAG, "request_id: " + reqId);
@@ -152,7 +185,7 @@
     /**
      * Calls out to the specified backend servers to retrieve an Endpoint Encryption Key and
      * corresponding certificate chain to provide to KeyMint. This public key will be used to
-     * perform an ECDH computation, using the shared secret to encrypt privacy sensitive components
+     * perform an ECDH computation, using the shared secret to encrypt privacy-sensitive components
      * in the bundle that the server needs from the device in order to provision certificates.
      *
      * A challenge is also returned from the server so that it can check freshness of the follow-up
@@ -163,7 +196,8 @@
     public GeekResponse fetchGeek(ProvisioningAttempt metrics)
             throws RkpdException, InterruptedException {
         byte[] input = CborUtils.buildProvisioningInfo(mContext);
-        byte[] cborBytes = connectAndGetData(metrics, GEEK_URL, input, Operation.FETCH_GEEK);
+        byte[] cborBytes =
+                connectAndGetData(metrics, generateFetchGeekUrl(), input, Operation.FETCH_GEEK);
         GeekResponse resp = CborUtils.parseGeekResponse(cborBytes);
         if (resp == null) {
             metrics.setStatus(ProvisioningAttempt.Status.FETCH_GEEK_HTTP_ERROR);
@@ -174,6 +208,19 @@
         return resp;
     }
 
+    private URL generateFetchGeekUrl() throws RkpdException {
+        try {
+            return new URL(Uri.parse(Settings.getUrl(mContext)).buildUpon()
+                            .appendPath(GEEK_URL)
+                            .build()
+                            .toString()
+                            // Needed due to the `:` in the URL endpoint.
+                            .replaceFirst("%3A", ":"));
+        } catch (MalformedURLException e) {
+            throw new RkpdException(RkpdException.ErrorCode.INTERNAL_ERROR, "Bad URL", e);
+        }
+    }
+
     private void checkDataBudget(ProvisioningAttempt metrics)
             throws RkpdException {
         if (!Settings.hasErrDataBudget(mContext, null /* curTime */)) {
@@ -186,9 +233,7 @@
 
     private RkpdException makeNetworkError(String message,
             ProvisioningAttempt metrics) {
-        ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
-        NetworkInfo networkInfo = cm.getActiveNetworkInfo();
-        if (networkInfo != null && networkInfo.isConnected()) {
+        if (isNetworkConnected(mContext)) {
             return new RkpdException(
                     RkpdException.ErrorCode.NETWORK_COMMUNICATION_ERROR, message);
         }
@@ -198,6 +243,18 @@
     }
 
     /**
+     * Checks whether network is connected.
+     * @return true if connected else false.
+     */
+    public static boolean isNetworkConnected(Context context) {
+        ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
+        NetworkCapabilities capabilities = cm.getNetworkCapabilities(cm.getActiveNetwork());
+        return capabilities != null
+                && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
+    }
+
+    /**
      * Fetch a GEEK from the server and update SettingsManager appropriately with the return
      * values. This will also delete all keys in the attestation key pool if the server has
      * indicated that RKP should be turned off.
@@ -273,45 +330,52 @@
         }
     }
 
-    private byte[] connectAndGetData(ProvisioningAttempt metrics, String endpoint, byte[] input,
+    private byte[] connectAndGetData(ProvisioningAttempt metrics, URL url, byte[] input,
             Operation operation) throws RkpdException, InterruptedException {
-        TrafficStats.setThreadStatsTag(0);
+        final int oldTrafficTag = TrafficStats.getAndSetThreadStatsTag(operation.getTrafficTag());
         int backoff_time = BACKOFF_TIME_MS;
         int attempt = 1;
+        RkpdException lastSeenRkpdException = null;
         try (StopWatch retryTimer = new StopWatch(TAG)) {
             retryTimer.start();
+            // Retry logic.
+            // Provide longer retries (up to 10s) for RkpdExceptions
+            // Provide shorter retries (once) for everything else.
             while (true) {
                 checkDataBudget(metrics);
                 try {
                     Log.v(TAG, "Requesting data from server. Attempt " + attempt);
-                    return requestData(metrics, new URL(Settings.getUrl(mContext) + endpoint),
-                            input);
+                    return requestData(metrics, url, input);
                 } catch (SocketTimeoutException e) {
                     metrics.setStatus(operation.getTimedOutStatus());
                     Log.e(TAG, "Server timed out. " + e.getMessage());
                 } catch (IOException e) {
                     metrics.setStatus(operation.getIoExceptionStatus());
-                    Log.e(TAG, "Failed to complete request from server." + e.getMessage());
+                    Log.e(TAG, "Failed to complete request from server. " + e.getMessage());
                 } catch (RkpdException e) {
+                    lastSeenRkpdException = e;
                     if (e.getErrorCode() == RkpdException.ErrorCode.DEVICE_NOT_REGISTERED) {
                         metrics.setStatus(
                                 ProvisioningAttempt.Status.SIGN_CERTS_DEVICE_NOT_REGISTERED);
                         throw e;
                     } else {
                         metrics.setStatus(operation.getHttpErrorStatus());
-                        if (e.getErrorCode() == RkpdException.ErrorCode.HTTP_CLIENT_ERROR) {
-                            throw e;
-                        }
                     }
                 }
-                if (retryTimer.getElapsedMillis() > Settings.getMaxRequestTime(mContext)) {
+                // Only RkpdExceptions should get longer retries.
+                if (retryTimer.getElapsedMillis() > Settings.getMaxRequestTime(mContext)
+                        || lastSeenRkpdException == null) {
                     break;
-                } else {
-                    Thread.sleep(backoff_time);
-                    backoff_time *= 2;
-                    attempt += 1;
                 }
+                Thread.sleep(backoff_time);
+                backoff_time *= 2;
+                attempt += 1;
             }
+        } finally {
+            TrafficStats.setThreadStatsTag(oldTrafficTag);
+        }
+        if (lastSeenRkpdException != null) {
+            throw lastSeenRkpdException;
         }
         Settings.incrementFailureCounter(mContext);
         throw makeNetworkError("Error getting data from server.", metrics);
@@ -323,7 +387,7 @@
         try (StopWatch serverWaitTimer = metrics.startServerWait()) {
             HttpURLConnection con = (HttpURLConnection) url.openConnection();
             con.setRequestMethod("POST");
-            con.setConnectTimeout(TIMEOUT_MS);
+            con.setConnectTimeout(getConnectTimeoutMs());
             con.setReadTimeout(TIMEOUT_MS);
             con.setDoOutput(true);
 
diff --git a/app/src/com/android/rkpdapp/interfaces/ServiceManagerInterface.java b/app/src/com/android/rkpdapp/interfaces/ServiceManagerInterface.java
index 38766b1..3ba716c 100644
--- a/app/src/com/android/rkpdapp/interfaces/ServiceManagerInterface.java
+++ b/app/src/com/android/rkpdapp/interfaces/ServiceManagerInterface.java
@@ -19,8 +19,11 @@
 import android.annotation.TestApi;
 import android.hardware.security.keymint.IRemotelyProvisionedComponent;
 import android.os.ServiceManager;
+import android.util.Log;
 
 import java.util.Arrays;
+import java.util.Map;
+import java.util.Objects;
 
 /**
  * Provides convenience methods for interfacing with ServiceManager class and its static functions.
@@ -28,33 +31,54 @@
 public class ServiceManagerInterface {
     private static final String TAG = "RkpdSvcManagerInterface";
     private static SystemInterface[] sInstances;
+    private static Map<String, IRemotelyProvisionedComponent> sBinders;
 
     private ServiceManagerInterface() {
     }
 
-    private static SystemInterface createSystemInterface(String serviceName) {
+    private static SystemInterface tryCreateSystemInterface(IRemotelyProvisionedComponent binder,
+            String serviceName) {
+        try {
+            return new SystemInterface(binder, serviceName);
+        } catch (UnsupportedOperationException e) {
+            Log.i(TAG, serviceName + " is unsupported.");
+            return null;
+        }
+    }
+
+    private static IRemotelyProvisionedComponent getBinder(String serviceName) {
         IRemotelyProvisionedComponent binder = IRemotelyProvisionedComponent.Stub.asInterface(
                 ServiceManager.waitForDeclaredService(serviceName));
         if (binder == null) {
             throw new IllegalArgumentException("Cannot find any implementation for " + serviceName);
         }
-        return new SystemInterface(binder, serviceName);
+        return binder;
     }
 
     /**
      * Gets all the instances on this device for IRemotelyProvisionedComponent as an array. The
      * returned values each contain a binder for interacting with the instance.
      *
-     * For testing purposes, the instances may be overridden by setInstances
+     * For testing purposes, the instances may be overridden by either setInstances or setBinders.
      */
     public static SystemInterface[] getAllInstances() {
+        if (sBinders != null) {
+            return sBinders.entrySet().stream()
+                    .map(x -> tryCreateSystemInterface(x.getValue(), x.getKey()))
+                    .filter(Objects::nonNull)
+                    .toArray(SystemInterface[]::new);
+        }
         if (sInstances != null) {
             return sInstances;
         }
 
         String irpcInterface = IRemotelyProvisionedComponent.DESCRIPTOR;
         return Arrays.stream(ServiceManager.getDeclaredInstances(irpcInterface))
-                .map(x -> createSystemInterface(irpcInterface + "/" + x))
+                .map(x -> {
+                    String serviceName = irpcInterface + "/" + x;
+                    return tryCreateSystemInterface(getBinder(serviceName), serviceName);
+                })
+                .filter(Objects::nonNull)
                 .toArray(SystemInterface[]::new);
     }
 
@@ -62,10 +86,17 @@
      * Get a specific system interface instance for a given IRemotelyProvisionedComponent.
      * If the given serviceName does not map to a known IRemotelyProvisionedComponent, this
      * method throws IllegalArgumentException.
+     * If the given serviceName is not supported, this method throws UnsupportedOperationException.
      *
-     * For testing purposes, the instances may be overridden by setInstances.
+     * For testing purposes, the instances may be overridden by either setInstances or setBinders.
      */
     public static SystemInterface getInstance(String serviceName) {
+        if (sBinders != null) {
+            if (sBinders.containsKey(serviceName)) {
+                return new SystemInterface(sBinders.get(serviceName), serviceName);
+            }
+            throw new IllegalArgumentException("Cannot find any binder for " + serviceName);
+        }
         if (sInstances != null) {
             for (SystemInterface i : sInstances) {
                 if (i.getServiceName().equals(serviceName)) {
@@ -75,11 +106,16 @@
             throw new IllegalArgumentException("Cannot find any implementation for " + serviceName);
         }
 
-        return createSystemInterface(serviceName);
+        return new SystemInterface(getBinder(serviceName), serviceName);
     }
 
     @TestApi
     public static void setInstances(SystemInterface[] instances) {
         sInstances = instances;
     }
+
+    @TestApi
+    public static void setBinders(Map<String, IRemotelyProvisionedComponent> binders) {
+        sBinders = binders;
+    }
 }
diff --git a/app/src/com/android/rkpdapp/interfaces/SystemInterface.java b/app/src/com/android/rkpdapp/interfaces/SystemInterface.java
index 87cba0a..9bca6e3 100644
--- a/app/src/com/android/rkpdapp/interfaces/SystemInterface.java
+++ b/app/src/com/android/rkpdapp/interfaces/SystemInterface.java
@@ -182,13 +182,13 @@
 
         int batchSize = mBinder.getHardwareInfo().supportedNumKeysInCsr;
 
-        if (batchSize <= RpcHardwareInfo.MIN_SUPPORTED_NUM_KEYS_IN_CSR) {
+        if (batchSize < RpcHardwareInfo.MIN_SUPPORTED_NUM_KEYS_IN_CSR) {
             Log.w(TAG, "HAL returned a batch size that's too small (" + batchSize
                     + "), defaulting to " + RpcHardwareInfo.MIN_SUPPORTED_NUM_KEYS_IN_CSR);
             return RpcHardwareInfo.MIN_SUPPORTED_NUM_KEYS_IN_CSR;
         }
 
-        if (batchSize >= maxBatchSize) {
+        if (batchSize > maxBatchSize) {
             Log.w(TAG, "HAL returned a batch size that's too large (" + batchSize
                     + "), defaulting to " + maxBatchSize);
             return maxBatchSize;
diff --git a/app/src/com/android/rkpdapp/metrics/ProvisioningAttempt.java b/app/src/com/android/rkpdapp/metrics/ProvisioningAttempt.java
index 4d24219..6613f30 100644
--- a/app/src/com/android/rkpdapp/metrics/ProvisioningAttempt.java
+++ b/app/src/com/android/rkpdapp/metrics/ProvisioningAttempt.java
@@ -192,8 +192,8 @@
                 transportType, getIntStatus(), mHttpStatusError);
         RkpdStatsLog.write(RkpdStatsLog.REMOTE_KEY_PROVISIONING_TIMING,
                 mServerWaitTimer.getElapsedMillis(), mBinderWaitTimer.getElapsedMillis(),
-                mLockWaitTimer.getElapsedMillis(),
-                mTotalTimer.getElapsedMillis(), transportType, mRemotelyProvisionedComponent);
+                mLockWaitTimer.getElapsedMillis(), mTotalTimer.getElapsedMillis(), transportType,
+                mRemotelyProvisionedComponent, mCause, getIntStatus());
     }
 
     private static Enablement getEnablementForComponent(String serviceName) {
diff --git a/app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java b/app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java
index 04ca1be..f3da031 100644
--- a/app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java
+++ b/app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java
@@ -36,6 +36,8 @@
 import com.android.rkpdapp.utils.Settings;
 
 import java.time.Instant;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import co.nstant.in.cbor.CborException;
 
@@ -47,6 +49,7 @@
 public class PeriodicProvisioner extends Worker {
     public static final String UNIQUE_WORK_NAME = "ProvisioningJob";
     private static final String TAG = "RkpdPeriodicProvisioner";
+    private static final boolean IS_ASYNC = true;
 
     private final Context mContext;
     private final ProvisionedKeyDao mKeyDao;
@@ -71,7 +74,7 @@
             return Result.success();
         }
 
-        if (Settings.getDefaultUrl().isEmpty()) {
+        if (Settings.getUrl(mContext).isEmpty()) {
             Log.i(TAG, "Stopping periodic provisioner: system has no configured server endpoint");
             WorkManager.getInstance(mContext).cancelWorkById(getId());
             return Result.success();
@@ -85,7 +88,7 @@
             // Fetch geek from the server and figure out whether provisioning needs to be stopped.
             GeekResponse response;
             try {
-                response = new ServerInterface(mContext).fetchGeekAndUpdate(metrics);
+                response = new ServerInterface(mContext, IS_ASYNC).fetchGeekAndUpdate(metrics);
             } catch (InterruptedException | RkpdException e) {
                 Log.e(TAG, "Error fetching configuration from the RKP server", e);
                 return Result.failure();
@@ -102,9 +105,9 @@
             }
 
             Log.i(TAG, "Total services found implementing IRPC: " + irpcs.length);
-            Provisioner provisioner = new Provisioner(mContext, mKeyDao);
-            Result result = Result.success();
-            for (SystemInterface irpc : irpcs) {
+            Provisioner provisioner = new Provisioner(mContext, mKeyDao, IS_ASYNC);
+            final AtomicBoolean result = new AtomicBoolean(true);
+            Arrays.stream(irpcs).parallel().forEach(irpc -> {
                 Log.i(TAG, "Starting provisioning for " + irpc);
                 try {
                     provisioner.provisionKeys(metrics, irpc, response);
@@ -112,13 +115,13 @@
                     Log.i(TAG, "Successfully provisioned " + irpc);
                 } catch (CborException e) {
                     Log.e(TAG, "Error parsing CBOR for " + irpc, e);
-                    result = Result.failure();
+                    result.set(false);
                 } catch (InterruptedException | RkpdException e) {
                     Log.e(TAG, "Error provisioning keys for " + irpc, e);
-                    result = Result.failure();
+                    result.set(false);
                 }
-            }
-            return result;
+            });
+            return result.get() ? Result.success() : Result.failure();
         }
     }
 
diff --git a/app/src/com/android/rkpdapp/provisioner/Provisioner.java b/app/src/com/android/rkpdapp/provisioner/Provisioner.java
index eb63b50..19db3a7 100644
--- a/app/src/com/android/rkpdapp/provisioner/Provisioner.java
+++ b/app/src/com/android/rkpdapp/provisioner/Provisioner.java
@@ -53,10 +53,13 @@
 
     private final Context mContext;
     private final ProvisionedKeyDao mKeyDao;
+    private final boolean mIsAsync;
 
-    public Provisioner(final Context applicationContext, ProvisionedKeyDao keyDao) {
+    public Provisioner(final Context applicationContext, ProvisionedKeyDao keyDao,
+            boolean isAsync) {
         mContext = applicationContext;
         mKeyDao = keyDao;
+        mIsAsync = isAsync;
     }
 
     /**
@@ -124,7 +127,7 @@
             throws RkpdException, CborException, InterruptedException {
         int provisionedSoFar = 0;
         List<byte[]> certChains = new ArrayList<>(keysGenerated.size());
-        int maxBatchSize = 0;
+        int maxBatchSize;
         try {
             maxBatchSize = systemInterface.getBatchSize();
         } catch (RemoteException e) {
@@ -154,7 +157,7 @@
             throw new RkpdException(RkpdException.ErrorCode.INTERNAL_ERROR,
                     "Failed to serialize payload");
         }
-        return new ServerInterface(mContext).requestSignedCertificates(certRequest,
+        return new ServerInterface(mContext, mIsAsync).requestSignedCertificates(certRequest,
                 response.getChallenge(), metrics);
     }
 
@@ -162,9 +165,11 @@
             List<RkpKey> keysGenerated) throws RkpdException {
         List<ProvisionedKey> provisionedKeys = new ArrayList<>();
         for (byte[] chain : certChains) {
-            X509Certificate cert = X509Utils.formatX509Certs(chain)[0];
-            long expirationDate = cert.getNotAfter().getTime();
-            byte[] rawPublicKey = X509Utils.getAndFormatRawPublicKey(cert);
+            X509Certificate[] certChain = X509Utils.formatX509Certs(chain);
+            X509Certificate leafCertificate = certChain[0];
+            long expirationDate = X509Utils.getExpirationTimeForCertificateChain(certChain)
+                    .getTime();
+            byte[] rawPublicKey = X509Utils.getAndFormatRawPublicKey(leafCertificate);
             if (rawPublicKey == null) {
                 Log.e(TAG, "Skipping malformed public key.");
                 continue;
diff --git a/app/src/com/android/rkpdapp/service/RemoteProvisioningService.java b/app/src/com/android/rkpdapp/service/RemoteProvisioningService.java
index 544d59d..c92cbd7 100644
--- a/app/src/com/android/rkpdapp/service/RemoteProvisioningService.java
+++ b/app/src/com/android/rkpdapp/service/RemoteProvisioningService.java
@@ -39,6 +39,7 @@
 /** Provides the implementation for IRemoteProvisioning.aidl */
 public class RemoteProvisioningService extends Service {
     public static final String TAG = "com.android.rkpdapp";
+    private static final boolean IS_ASYNC = false;
     private final IRemoteProvisioning.Stub mBinder = new RemoteProvisioningBinder();
 
     @Override
@@ -58,7 +59,7 @@
             final Context context = getApplicationContext();
             RkpdClientOperation metric = RkpdClientOperation.getRegistration(callerUid, irpcName);
             try (metric) {
-                if (Settings.getDefaultUrl().isEmpty()) {
+                if (Settings.getUrl(context).isEmpty()) {
                     callback.onError("RKP is disabled. System configured with no default URL.");
                     metric.setResult(RkpdClientOperation.Result.RKP_UNSUPPORTED);
                     return;
@@ -75,9 +76,9 @@
                 }
 
                 ProvisionedKeyDao dao = RkpdDatabase.getDatabase(context).provisionedKeyDao();
-                Provisioner provisioner = new Provisioner(context, dao);
+                Provisioner provisioner = new Provisioner(context, dao, IS_ASYNC);
                 IRegistration.Stub registration = new RegistrationBinder(context, callerUid,
-                        systemInterface, dao, new ServerInterface(context), provisioner,
+                        systemInterface, dao, new ServerInterface(context, IS_ASYNC), provisioner,
                         ThreadPool.EXECUTOR);
                 metric.setResult(RkpdClientOperation.Result.SUCCESS);
                 callback.onSuccess(registration);
diff --git a/app/src/com/android/rkpdapp/utils/X509Utils.java b/app/src/com/android/rkpdapp/utils/X509Utils.java
index 4e12872..61b55c2 100644
--- a/app/src/com/android/rkpdapp/utils/X509Utils.java
+++ b/app/src/com/android/rkpdapp/utils/X509Utils.java
@@ -40,6 +40,7 @@
 import java.security.interfaces.ECPublicKey;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.Set;
 
 /**
@@ -158,4 +159,16 @@
             return false;
         }
     }
+
+    /**
+     * Gets the date when the entire cert chain expires. This can be calculated by
+     * checking the individual certificate times and returning the minimum of all.
+     * @return Expiration time for the certificate chain
+     */
+    public static Date getExpirationTimeForCertificateChain(X509Certificate[] certChain) {
+        return Arrays.stream(certChain)
+                .map(X509Certificate::getNotAfter)
+                .min(Date::compareTo)
+                .orElse(null);
+    }
 }
diff --git a/app/tests/e2e/Android.bp b/app/tests/e2e/Android.bp
index 1bba10c..baa003d 100644
--- a/app/tests/e2e/Android.bp
+++ b/app/tests/e2e/Android.bp
@@ -26,7 +26,7 @@
         "androidx.test.rules",
         "androidx.work_work-testing",
         "platform-test-annotations",
-        "truth-prebuilt",
+        "truth",
     ],
     platform_apis: true,
     test_suites: [
@@ -34,6 +34,6 @@
         "device-tests",
         "mts-rkpd",
     ],
-    min_sdk_version: "UpsideDownCake",
+    min_sdk_version: "33",
     instrumentation_for: "rkpdapp",
 }
diff --git a/app/tests/e2e/src/com/android/rkpdapp/e2etest/KeystoreIntegrationTest.java b/app/tests/e2e/src/com/android/rkpdapp/e2etest/KeystoreIntegrationTest.java
index 5a8e728..6f46896 100644
--- a/app/tests/e2e/src/com/android/rkpdapp/e2etest/KeystoreIntegrationTest.java
+++ b/app/tests/e2e/src/com/android/rkpdapp/e2etest/KeystoreIntegrationTest.java
@@ -41,11 +41,12 @@
 import com.android.rkpdapp.database.ProvisionedKey;
 import com.android.rkpdapp.database.ProvisionedKeyDao;
 import com.android.rkpdapp.database.RkpdDatabase;
+import com.android.rkpdapp.interfaces.ServerInterface;
 import com.android.rkpdapp.interfaces.ServiceManagerInterface;
 import com.android.rkpdapp.interfaces.SystemInterface;
 import com.android.rkpdapp.provisioner.PeriodicProvisioner;
 import com.android.rkpdapp.testutil.FakeRkpServer;
-import com.android.rkpdapp.testutil.NetworkUtils;
+import com.android.rkpdapp.testutil.SystemInterfaceSelector;
 import com.android.rkpdapp.testutil.SystemPropertySetter;
 import com.android.rkpdapp.utils.Settings;
 import com.android.rkpdapp.utils.X509Utils;
@@ -108,15 +109,20 @@
     @BeforeClass
     public static void init() {
         sContext = ApplicationProvider.getApplicationContext();
-
-        assume()
-                .withMessage("The RKP server hostname is not configured -- assume RKP disabled.")
-                .that(SystemProperties.get("remote_provisioning.hostname"))
-                .isNotEmpty();
     }
 
     @Before
     public void setUp() throws Exception {
+        assume()
+                .withMessage("The RKP server hostname is not configured -- assume RKP disabled.")
+                .that(SystemProperties.get("remote_provisioning.hostname"))
+                .isNotEmpty();
+
+        assume()
+                .withMessage("RKP Integration tests rely on network availability.")
+                .that(ServerInterface.isNetworkConnected(sContext))
+                .isTrue();
+
         Settings.clearPreferences(sContext);
 
         mKeyDao = RkpdDatabase.getDatabase(sContext).provisionedKeyDao();
@@ -124,7 +130,8 @@
         mKeyStore.load(null);
         mKeyDao.deleteAllKeys();
 
-        SystemInterface systemInterface = ServiceManagerInterface.getInstance(mServiceName);
+        SystemInterface systemInterface =
+                SystemInterfaceSelector.getSystemInterfaceForServiceName(mServiceName);
         ServiceManagerInterface.setInstances(new SystemInterface[] {systemInterface});
     }
 
@@ -132,8 +139,10 @@
     public void tearDown() throws Exception {
         Settings.clearPreferences(sContext);
 
-        mKeyStore.deleteEntry(getTestKeyAlias());
-        mKeyDao.deleteAllKeys();
+        if (mKeyDao != null) {
+            mKeyStore.deleteEntry(getTestKeyAlias());
+            mKeyDao.deleteAllKeys();
+        }
 
         ServiceManagerInterface.setInstances(null);
     }
@@ -221,21 +230,17 @@
         // Verify that if the system is set to rkp only, key creation fails when RKP is unable
         // to get keys.
 
-        try {
+        try (FakeRkpServer server = new FakeRkpServer(FakeRkpServer.Response.INTERNAL_ERROR,
+                FakeRkpServer.Response.INTERNAL_ERROR)) {
             Settings.setDeviceConfig(sContext, Settings.EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT,
-                    Duration.ofDays(1), "bad url");
+                    Duration.ofDays(1), server.getUrl());
             Settings.setMaxRequestTime(sContext, 100);
             createKeystoreKeyBackedByRkp();
             assertWithMessage("Should have gotten a KeyStoreException").fail();
         } catch (ProviderException e) {
             assertThat(e.getCause()).isInstanceOf(KeyStoreException.class);
-            if (NetworkUtils.isNetworkConnected(sContext)) {
-                assertThat(((KeyStoreException) e.getCause()).getErrorCode())
-                        .isEqualTo(ResponseCode.OUT_OF_KEYS_TRANSIENT_ERROR);
-            } else {
-                assertThat(((KeyStoreException) e.getCause()).getErrorCode())
-                        .isEqualTo(ResponseCode.OUT_OF_KEYS_PENDING_INTERNET_CONNECTIVITY);
-            }
+            assertThat(((KeyStoreException) e.getCause()).getErrorCode())
+                    .isEqualTo(ResponseCode.OUT_OF_KEYS_TRANSIENT_ERROR);
         }
     }
 
@@ -247,14 +252,17 @@
                 .that(SystemProperties.getBoolean(getRkpOnlyProp(), false))
                 .isFalse();
 
-        Settings.setDeviceConfig(sContext, Settings.EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT,
-                Duration.ofDays(1), "bad url");
+        try (FakeRkpServer server = new FakeRkpServer(FakeRkpServer.Response.INTERNAL_ERROR,
+                FakeRkpServer.Response.INTERNAL_ERROR)) {
+            Settings.setDeviceConfig(sContext, Settings.EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT,
+                    Duration.ofDays(1), server.getUrl());
 
-        createKeystoreKey();
+            createKeystoreKey();
 
-        // Ensure the key has a cert, but it didn't come from rkpd.
-        assertThat(mKeyStore.getCertificateChain(getTestKeyAlias())).isNotEmpty();
-        assertThat(mKeyDao.getTotalKeysForIrpc(mServiceName)).isEqualTo(0);
+            // Ensure the key has a cert, but it didn't come from rkpd.
+            assertThat(mKeyStore.getCertificateChain(getTestKeyAlias())).isNotEmpty();
+            assertThat(mKeyDao.getTotalKeysForIrpc(mServiceName)).isEqualTo(0);
+        }
     }
 
     @Test
@@ -275,8 +283,9 @@
 
     @Test
     public void testRetryableRkpError() throws Exception {
-        try {
-            Settings.setDeviceConfig(sContext, 1, Duration.ofDays(1), "bad url");
+        try (FakeRkpServer server = new FakeRkpServer(FakeRkpServer.Response.INTERNAL_ERROR,
+                FakeRkpServer.Response.INTERNAL_ERROR)) {
+            Settings.setDeviceConfig(sContext, 1, Duration.ofDays(1), server.getUrl());
             Settings.setMaxRequestTime(sContext, 100);
             createKeystoreKeyBackedByRkp();
             Assert.fail("Expected a keystore exception");
diff --git a/app/tests/e2e/src/com/android/rkpdapp/e2etest/RkpdHostTestHelperTests.java b/app/tests/e2e/src/com/android/rkpdapp/e2etest/RkpdHostTestHelperTests.java
index 51b5e5b..c1762d5 100644
--- a/app/tests/e2e/src/com/android/rkpdapp/e2etest/RkpdHostTestHelperTests.java
+++ b/app/tests/e2e/src/com/android/rkpdapp/e2etest/RkpdHostTestHelperTests.java
@@ -38,9 +38,11 @@
 import com.android.rkpdapp.database.ProvisionedKey;
 import com.android.rkpdapp.database.ProvisionedKeyDao;
 import com.android.rkpdapp.database.RkpdDatabase;
+import com.android.rkpdapp.interfaces.ServerInterface;
 import com.android.rkpdapp.interfaces.ServiceManagerInterface;
 import com.android.rkpdapp.interfaces.SystemInterface;
 import com.android.rkpdapp.provisioner.PeriodicProvisioner;
+import com.android.rkpdapp.testutil.SystemInterfaceSelector;
 import com.android.rkpdapp.testutil.TestDatabase;
 import com.android.rkpdapp.testutil.TestProvisionedKeyDao;
 import com.android.rkpdapp.utils.Settings;
@@ -58,7 +60,6 @@
 import java.security.KeyPairGenerator;
 import java.security.KeyStore;
 import java.security.spec.ECGenParameterSpec;
-import java.time.Duration;
 import java.time.Instant;
 import java.util.List;
 import java.util.concurrent.Executors;
@@ -89,32 +90,42 @@
     @BeforeClass
     public static void init() {
         sContext = ApplicationProvider.getApplicationContext();
-
-        assume()
-                .withMessage("The RKP server hostname is not configured -- assume RKP disabled.")
-                .that(SystemProperties.get("remote_provisioning.hostname"))
-                .isNotEmpty();
     }
 
     @Before
     public void setUp() throws Exception {
+        assume()
+                .withMessage("The RKP server hostname is not configured -- assume RKP disabled.")
+                .that(SystemProperties.get("remote_provisioning.hostname"))
+                .isNotEmpty();
+
+        assume()
+                .withMessage("RKP Integration tests rely on network availability.")
+                .that(ServerInterface.isNetworkConnected(sContext))
+                .isTrue();
+
         Settings.clearPreferences(sContext);
         mRealDao = RkpdDatabase.getDatabase(sContext).provisionedKeyDao();
         mRealDao.deleteAllKeys();
         mTestDao = Room.databaseBuilder(sContext, TestDatabase.class, DB_NAME).build().dao();
-        SystemInterface systemInterface = ServiceManagerInterface.getInstance(mServiceName);
-        ServiceManagerInterface.setInstances(new SystemInterface[] {systemInterface});
 
         mProvisioner = TestWorkerBuilder.from(
                 sContext,
                 PeriodicProvisioner.class,
                 Executors.newSingleThreadExecutor()).build();
+
+        SystemInterface systemInterface =
+                SystemInterfaceSelector.getSystemInterfaceForServiceName(mServiceName);
+        ServiceManagerInterface.setInstances(new SystemInterface[] {systemInterface});
     }
 
     @After
     public void tearDown() throws Exception {
         Settings.clearPreferences(sContext);
-        mRealDao.deleteAllKeys();
+
+        if (mRealDao != null) {
+            mRealDao.deleteAllKeys();
+        }
 
         KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
         keyStore.load(null);
@@ -144,8 +155,6 @@
     public void provisionThenExpireThenProvisionAgain() throws Exception {
         assertThat(mProvisioner.doWork()).isEqualTo(ListenableWorker.Result.success());
 
-        final Instant expiry = Instant.now().plus(Duration.ofHours(1));
-
         List<ProvisionedKey> keys = mTestDao.getAllKeys();
 
         // Expire a key
@@ -177,6 +186,8 @@
         StatsProcessor.PoolStats updatedPool = StatsProcessor.processPool(mRealDao, mServiceName,
                 Settings.getExtraSignedKeysAvailable(sContext),
                 Settings.getExpirationTime(sContext));
-        assertThat(updatedPool.toString()).isEqualTo(pool.toString());
+
+        assertThat(updatedPool.keysInUse + updatedPool.keysUnassigned)
+                .isEqualTo(pool.keysInUse + pool.keysUnassigned);
     }
 }
diff --git a/app/tests/hosttest/Android.bp b/app/tests/hosttest/Android.bp
index 282951c..c0bfd87 100644
--- a/app/tests/hosttest/Android.bp
+++ b/app/tests/hosttest/Android.bp
@@ -29,7 +29,7 @@
         "host-libprotobuf-java-full",
         "platformprotos",
         "tradefed",
-        "truth-prebuilt",
+        "truth",
     ],
     static_libs: [
         "cts-statsd-atom-host-test-utils",
diff --git a/app/tests/hosttest/AndroidTest.xml b/app/tests/hosttest/AndroidTest.xml
index 5e79b2e..10c71c2 100644
--- a/app/tests/hosttest/AndroidTest.xml
+++ b/app/tests/hosttest/AndroidTest.xml
@@ -26,4 +26,11 @@
     <test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" >
         <option name="jar" value="RkpdAppHostTests.jar" />
     </test>
+
+    <!-- Only run if RKPD mainline module is installed -->
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="enable" value="true" />
+        <option name="mainline-module-package-name" value="com.android.rkpd" />
+    </object>
 </configuration>
diff --git a/app/tests/hosttest/src/com/android/rkpdapp/hosttest/RkpdStatsTests.java b/app/tests/hosttest/src/com/android/rkpdapp/hosttest/RkpdStatsTests.java
index 823179d..e7bf7cc 100644
--- a/app/tests/hosttest/src/com/android/rkpdapp/hosttest/RkpdStatsTests.java
+++ b/app/tests/hosttest/src/com/android/rkpdapp/hosttest/RkpdStatsTests.java
@@ -40,6 +40,7 @@
 @RunWith(DeviceJUnit4ClassRunner.class)
 public final class RkpdStatsTests extends AtomsHostTest {
     private static final int NO_HTTP_STATUS_ERROR = 0;
+    private static final int HTTP_STATUS_SERVER_ERROR = 500;
     private static final int HTTPS_OK = 200;
     private static final String RPC_DEFAULT =
             "android.hardware.security.keymint.IRemotelyProvisionedComponent/default";
@@ -107,7 +108,7 @@
         assertThat(attempt.getUptime()).isNotEqualTo(UpTime.UPTIME_UNKNOWN);
         assertThat(attempt.getEnablement()).isEqualTo(Enablement.ENABLED_RKP_ONLY);
         assertThat(attempt.getStatus()).isEqualTo(
-                RemoteKeyProvisioningStatus.FETCH_GEEK_IO_EXCEPTION);
+                RemoteKeyProvisioningStatus.FETCH_GEEK_HTTP_ERROR);
 
         final RemoteKeyProvisioningTiming timing = getTimingMetric(data);
         assertThat(timing).isNotNull();
@@ -125,7 +126,7 @@
         assertThat(network).isNotNull();
         assertThat(network.getTransportType()).isEqualTo(timing.getTransportType());
         assertThat(network.getStatus()).isEqualTo(attempt.getStatus());
-        assertThat(network.getHttpStatusError()).isEqualTo(NO_HTTP_STATUS_ERROR);
+        assertThat(network.getHttpStatusError()).isEqualTo(HTTP_STATUS_SERVER_ERROR);
     }
 
     @Test
diff --git a/app/tests/stress/Android.bp b/app/tests/stress/Android.bp
index 01267df..4320f2f 100644
--- a/app/tests/stress/Android.bp
+++ b/app/tests/stress/Android.bp
@@ -24,7 +24,7 @@
         "androidx.test.core",
         "androidx.test.rules",
         "platform-test-annotations",
-        "truth-prebuilt",
+        "truth",
     ],
     platform_apis: true,
     test_suites: [
@@ -32,6 +32,6 @@
         "device-tests",
         "mts-rkpd",
     ],
-    min_sdk_version: "UpsideDownCake",
+    min_sdk_version: "33",
     instrumentation_for: "rkpdapp",
 }
diff --git a/app/tests/stress/src/com/android/rkpdapp/stress/RegistrationBinderStressTest.java b/app/tests/stress/src/com/android/rkpdapp/stress/RegistrationBinderStressTest.java
index ce64d91..4ab5438 100644
--- a/app/tests/stress/src/com/android/rkpdapp/stress/RegistrationBinderStressTest.java
+++ b/app/tests/stress/src/com/android/rkpdapp/stress/RegistrationBinderStressTest.java
@@ -82,8 +82,10 @@
     }
 
     private RegistrationBinder createRegistrationBinder() {
+        boolean isAsync = false;
         return new RegistrationBinder(mContext, Process.myUid(), mIrpcHal, mKeyDao,
-                new ServerInterface(mContext), new Provisioner(mContext, mKeyDao), mExecutor);
+                new ServerInterface(mContext, isAsync), new Provisioner(mContext, mKeyDao, isAsync),
+                mExecutor);
     }
 
     private void getKeyHelper(int keyId) {
diff --git a/app/tests/unit/Android.bp b/app/tests/unit/Android.bp
index 19eea6b..2a9ae30 100644
--- a/app/tests/unit/Android.bp
+++ b/app/tests/unit/Android.bp
@@ -30,7 +30,7 @@
         "libnanohttpd",
         "mockito-target-extended-minus-junit4",
         "platform-test-annotations",
-        "truth-prebuilt",
+        "truth",
         "rkpdapp-tink-prebuilt-test-only",
         "bouncycastle-unbundled",
     ],
@@ -40,7 +40,7 @@
         "device-tests",
         "mts-rkpd",
     ],
-    min_sdk_version: "UpsideDownCake",
+    min_sdk_version: "33",
     instrumentation_for: "rkpdapp",
 }
 
diff --git a/app/tests/unit/src/com/android/rkpdapp/unittest/PeriodicProvisionerTests.java b/app/tests/unit/src/com/android/rkpdapp/unittest/PeriodicProvisionerTests.java
index 005c4f4..a278f50 100644
--- a/app/tests/unit/src/com/android/rkpdapp/unittest/PeriodicProvisionerTests.java
+++ b/app/tests/unit/src/com/android/rkpdapp/unittest/PeriodicProvisionerTests.java
@@ -24,7 +24,6 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
 
 import android.content.Context;
 
@@ -46,11 +45,13 @@
 import com.android.rkpdapp.interfaces.ServiceManagerInterface;
 import com.android.rkpdapp.interfaces.SystemInterface;
 import com.android.rkpdapp.provisioner.PeriodicProvisioner;
+import com.android.rkpdapp.service.RegistrationBinder;
 import com.android.rkpdapp.testutil.FakeRkpServer;
 import com.android.rkpdapp.testutil.SystemPropertySetter;
 import com.android.rkpdapp.utils.Settings;
 
 import org.junit.After;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -75,6 +76,9 @@
     @Before
     public void setUp() {
         mContext = ApplicationProvider.getApplicationContext();
+
+        Assume.assumeFalse(Settings.getDefaultUrl().isEmpty());
+
         RkpdDatabase.getDatabase(mContext).provisionedKeyDao().deleteAllKeys();
         mProvisioner = TestWorkerBuilder.from(
                 mContext,
@@ -85,7 +89,7 @@
                 .setExecutor(new SynchronousExecutor())
                 .build();
         WorkManagerTestInitHelper.initializeTestWorkManager(mContext, config);
-
+        Settings.clearPreferences(mContext);
     }
 
     @After
@@ -172,8 +176,8 @@
             ServiceManagerInterface.setInstances(new SystemInterface[]{mockHal});
             assertThat(mProvisioner.doWork()).isEqualTo(ListenableWorker.Result.failure());
 
-            // we should have failed before making any local HAl calls
-            verifyNoMoreInteractions(mockHal);
+            // we should have failed before trying to generate any keys
+            verify(mockHal, never()).generateKey(any());
         }
     }
 
@@ -193,8 +197,8 @@
             ServiceManagerInterface.setInstances(new SystemInterface[]{mockHal});
             assertThat(mProvisioner.doWork()).isEqualTo(ListenableWorker.Result.success());
 
-            // since RKP is disabled, there should be no interactions with the HAL
-            verifyNoMoreInteractions(mockHal);
+            // since RKP is disabled, there should be no keys generated
+            verify(mockHal, never()).generateKey(any());
         }
 
         // when RKP is detected as disabled, the provisioner is supposed to delete all keys
@@ -205,9 +209,12 @@
     public void provisioningExpiresOldKeys() throws Exception {
         ProvisionedKeyDao dao = RkpdDatabase.getDatabase(mContext).provisionedKeyDao();
         ProvisionedKey oldKey = new ProvisionedKey(new byte[1], "fake-irpc", new byte[2],
-                new byte[3], Instant.now().minusSeconds(120));
+                new byte[3],
+                Instant.now().minus(RegistrationBinder.MIN_KEY_LIFETIME.multipliedBy(2)));
+        // Add 2 hours so that this key does not get deleted in case getKeyWorker comes alive.
         ProvisionedKey freshKey = new ProvisionedKey(new byte[11], "fake-irpc", new byte[12],
-                new byte[13], Instant.now().plusSeconds(120));
+                new byte[13],
+                Instant.now().plus(RegistrationBinder.MIN_KEY_LIFETIME.multipliedBy(2)));
         dao.insertKeys(List.of(oldKey, freshKey));
         assertThat(dao.getTotalKeysForIrpc("fake-irpc")).isEqualTo(2);
 
diff --git a/app/tests/unit/src/com/android/rkpdapp/unittest/ProvisionerTest.java b/app/tests/unit/src/com/android/rkpdapp/unittest/ProvisionerTest.java
index 24a48f1..384b172 100644
--- a/app/tests/unit/src/com/android/rkpdapp/unittest/ProvisionerTest.java
+++ b/app/tests/unit/src/com/android/rkpdapp/unittest/ProvisionerTest.java
@@ -73,7 +73,7 @@
         ProvisionedKeyDao keyDao = RkpdDatabase.getDatabase(sContext).provisionedKeyDao();
         keyDao.deleteAllKeys();
 
-        mProvisioner = new Provisioner(sContext, keyDao);
+        mProvisioner = new Provisioner(sContext, keyDao, false);
     }
 
     @After
diff --git a/app/tests/unit/src/com/android/rkpdapp/unittest/RemoteProvisioningServiceTest.java b/app/tests/unit/src/com/android/rkpdapp/unittest/RemoteProvisioningServiceTest.java
index 15a6204..81465f1 100644
--- a/app/tests/unit/src/com/android/rkpdapp/unittest/RemoteProvisioningServiceTest.java
+++ b/app/tests/unit/src/com/android/rkpdapp/unittest/RemoteProvisioningServiceTest.java
@@ -58,6 +58,7 @@
         service.attach(mContext, ActivityThread.currentActivityThread(),
                 "RemoteProvisioningService", null, mock(Application.class), null);
         mBinder = IRemoteProvisioning.Stub.asInterface(service.onBind(null));
+        Settings.clearPreferences(mContext);
     }
 
     @After
diff --git a/app/tests/unit/src/com/android/rkpdapp/unittest/ServerInterfaceTest.java b/app/tests/unit/src/com/android/rkpdapp/unittest/ServerInterfaceTest.java
index d7971df..896a4b3 100644
--- a/app/tests/unit/src/com/android/rkpdapp/unittest/ServerInterfaceTest.java
+++ b/app/tests/unit/src/com/android/rkpdapp/unittest/ServerInterfaceTest.java
@@ -21,7 +21,7 @@
 
 import android.content.Context;
 import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
+import android.net.NetworkCapabilities;
 import android.util.Base64;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -61,7 +61,7 @@
     @Before
     public void setUp() {
         Settings.clearPreferences(sContext);
-        mServerInterface = new ServerInterface(sContext);
+        mServerInterface = new ServerInterface(sContext, false);
     }
 
     @After
@@ -81,7 +81,8 @@
                     ProvisioningAttempt.createScheduledAttemptMetrics(sContext));
             assertWithMessage("Expected RkpdException.").fail();
         } catch (RkpdException e) {
-            // should throw this
+            assertThat(e.getErrorCode()).isEqualTo(RkpdException.ErrorCode.HTTP_SERVER_ERROR);
+            assertThat(e).hasMessageThat().contains("HTTP error status encountered");
         }
     }
 
@@ -183,6 +184,7 @@
                 FakeRkpServer.Response.SIGN_CERTS_USER_UNAUTHORIZED)) {
             Settings.setDeviceConfig(sContext, 2 /* extraKeys */,
                     TIME_TO_REFRESH_HOURS /* expiringBy */, server.getUrl());
+            Settings.setMaxRequestTime(sContext, 100);
             ProvisioningAttempt metrics = ProvisioningAttempt.createScheduledAttemptMetrics(
                     sContext);
             mServerInterface.requestSignedCertificates(new byte[0], new byte[0], metrics);
@@ -227,11 +229,18 @@
 
     @Test
     public void testDataBudgetEmptyFetchGeekNetworkConnected() throws Exception {
-        // Check the data budget in order to initialize a rolling window.
-        assertThat(Settings.hasErrDataBudget(sContext, null /* curTime */)).isTrue();
-        Settings.consumeErrDataBudget(sContext, Settings.FAILURE_DATA_USAGE_MAX);
-        ProvisioningAttempt metrics = ProvisioningAttempt.createScheduledAttemptMetrics(sContext);
-        try {
+        try (FakeRkpServer server = new FakeRkpServer(
+                FakeRkpServer.Response.FETCH_EEK_OK,
+                FakeRkpServer.Response.SIGN_CERTS_OK_VALID_CBOR)) {
+            Settings.setDeviceConfig(sContext, 2 /* extraKeys */,
+                    TIME_TO_REFRESH_HOURS /* expiringBy */, server.getUrl());
+
+            // Check the data budget in order to initialize a rolling window.
+            assertThat(Settings.hasErrDataBudget(sContext, null /* curTime */)).isTrue();
+            Settings.consumeErrDataBudget(sContext, Settings.FAILURE_DATA_USAGE_MAX);
+            ProvisioningAttempt metrics = ProvisioningAttempt.createScheduledAttemptMetrics(
+                    sContext);
+
             // We are okay in mocking connectivity failure since err data budget is the first thing
             // to be checked.
             mockConnectivityFailure(ConnectivityState.CONNECTED);
@@ -246,15 +255,21 @@
 
     @Test
     public void testDataBudgetEmptyFetchGeekNetworkDisconnected() throws Exception {
-        // Check the data budget in order to initialize a rolling window.
-        try {
-            // We are okay in mocking connectivity failure since err data budget is the first thing
-            // to be checked.
-            mockConnectivityFailure(ConnectivityState.DISCONNECTED);
+        try (FakeRkpServer server = new FakeRkpServer(
+                FakeRkpServer.Response.FETCH_EEK_OK,
+                FakeRkpServer.Response.SIGN_CERTS_OK_VALID_CBOR)) {
+            Settings.setDeviceConfig(sContext, 2 /* extraKeys */,
+                    TIME_TO_REFRESH_HOURS /* expiringBy */, server.getUrl());
+
+            // Check the data budget in order to initialize a rolling window.
             assertThat(Settings.hasErrDataBudget(sContext, null /* curTime */)).isTrue();
             Settings.consumeErrDataBudget(sContext, Settings.FAILURE_DATA_USAGE_MAX);
             ProvisioningAttempt metrics = ProvisioningAttempt.createScheduledAttemptMetrics(
                     sContext);
+
+            // We are okay in mocking connectivity failure since err data budget is the first thing
+            // to be checked.
+            mockConnectivityFailure(ConnectivityState.DISCONNECTED);
             mServerInterface.fetchGeek(metrics);
             assertWithMessage("Network transaction should not have proceeded.").fail();
         } catch (RkpdException e) {
@@ -376,12 +391,16 @@
 
     private void mockConnectivityFailure(ConnectivityState state) {
         ConnectivityManager mockedConnectivityManager = Mockito.mock(ConnectivityManager.class);
-        NetworkInfo mockedNetwork = Mockito.mock(NetworkInfo.class);
 
         Mockito.when(sContext.getSystemService(ConnectivityManager.class))
                 .thenReturn(mockedConnectivityManager);
-        Mockito.when(mockedConnectivityManager.getActiveNetworkInfo()).thenReturn(mockedNetwork);
-        Mockito.when(mockedNetwork.isConnected()).thenReturn(state == ConnectivityState.CONNECTED);
+        NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder();
+        builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        if (state == ConnectivityState.CONNECTED) {
+            builder.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
+        }
+        Mockito.when(mockedConnectivityManager.getNetworkCapabilities(Mockito.any()))
+                .thenReturn(builder.build());
     }
 
     private enum ConnectivityState {
diff --git a/app/tests/unit/src/com/android/rkpdapp/unittest/SystemInterfaceTest.java b/app/tests/unit/src/com/android/rkpdapp/unittest/SystemInterfaceTest.java
index 92dc16f..c201e11 100644
--- a/app/tests/unit/src/com/android/rkpdapp/unittest/SystemInterfaceTest.java
+++ b/app/tests/unit/src/com/android/rkpdapp/unittest/SystemInterfaceTest.java
@@ -25,6 +25,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
@@ -53,8 +54,10 @@
 import com.android.rkpdapp.metrics.ProvisioningAttempt;
 import com.android.rkpdapp.utils.CborUtils;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.crypto.tink.subtle.Ed25519Sign;
 
+import org.junit.After;
 import org.junit.Assert;
 import org.junit.Assume;
 import org.junit.Before;
@@ -93,6 +96,12 @@
         Assume.assumeTrue(ServiceManager.isDeclared(SERVICE));
     }
 
+    @After
+    public void cleanUp() {
+        ServiceManagerInterface.setBinders(null);
+        ServiceManagerInterface.setInstances(null);
+    }
+
     @Test
     public void testGetDeclaredInstances() {
         SystemInterface[] instances = ServiceManagerInterface.getAllInstances();
@@ -109,8 +118,32 @@
         try {
             ServiceManagerInterface.getInstance("non-existent");
             fail("Getting the declared service 'non-existent' should fail due to SEPolicy.");
-        } catch (RuntimeException e) {
-            assertThat(e).isInstanceOf(SecurityException.class);
+        } catch (IllegalArgumentException | SecurityException e) {
+            // Pre-android V, we'd get SecurityException. Post-V we get IllegalArgumentException
+        }
+    }
+
+    @Test
+    public void testGetAllInstancesWithUnsupportedService() throws RemoteException {
+        IRemotelyProvisionedComponent mockedBinder = mock(IRemotelyProvisionedComponent.class);
+        doThrow(new UnsupportedOperationException()).when(mockedBinder).getHardwareInfo();
+        ServiceManagerInterface.setBinders(ImmutableMap.of(SERVICE, mockedBinder));
+
+        SystemInterface[] instances = ServiceManagerInterface.getAllInstances();
+        assertThat(instances).isEmpty();
+    }
+
+    @Test
+    public void testGetInstanceWithUnsupportedService() throws RemoteException {
+        IRemotelyProvisionedComponent mockedBinder = mock(IRemotelyProvisionedComponent.class);
+        doThrow(new UnsupportedOperationException()).when(mockedBinder).getHardwareInfo();
+        ServiceManagerInterface.setBinders(ImmutableMap.of(SERVICE, mockedBinder));
+
+        try {
+            SystemInterface ignored = ServiceManagerInterface.getInstance(SERVICE);
+            fail("Expected UnsupportedOperationException");
+        } catch (UnsupportedOperationException e) {
+            // pass
         }
     }
 
diff --git a/app/tests/unit/src/com/android/rkpdapp/unittest/Utils.java b/app/tests/unit/src/com/android/rkpdapp/unittest/Utils.java
index d478db7..bbce2d9 100644
--- a/app/tests/unit/src/com/android/rkpdapp/unittest/Utils.java
+++ b/app/tests/unit/src/com/android/rkpdapp/unittest/Utils.java
@@ -144,6 +144,12 @@
 
     public static X509Certificate signPublicKey(KeyPair issuerKeyPair, PublicKey publicKeyToSign)
             throws Exception {
+        return signPublicKey(issuerKeyPair, publicKeyToSign,
+                Instant.now().plus(Duration.ofDays(1)));
+    }
+
+    public static X509Certificate signPublicKey(KeyPair issuerKeyPair, PublicKey publicKeyToSign,
+            Instant expirationInstant) throws Exception {
         X500Principal issuer = new X500Principal("CN=TEE");
         BigInteger serial = BigInteger.ONE;
         X500Principal subject = new X500Principal("CN=TEE");
@@ -153,7 +159,7 @@
         certificateBuilder.setIssuerDN(issuer);
         certificateBuilder.setSerialNumber(serial);
         certificateBuilder.setNotBefore(Date.from(now));
-        certificateBuilder.setNotAfter(Date.from(now.plus(Duration.ofDays(1))));
+        certificateBuilder.setNotAfter(Date.from(expirationInstant));
         certificateBuilder.setSignatureAlgorithm("SHA256WITHECDSA");
         certificateBuilder.setSubjectDN(subject);
         certificateBuilder.setPublicKey(publicKeyToSign);
diff --git a/app/tests/unit/src/com/android/rkpdapp/unittest/X509UtilsTest.java b/app/tests/unit/src/com/android/rkpdapp/unittest/X509UtilsTest.java
index d4f9d0a..83378e4 100644
--- a/app/tests/unit/src/com/android/rkpdapp/unittest/X509UtilsTest.java
+++ b/app/tests/unit/src/com/android/rkpdapp/unittest/X509UtilsTest.java
@@ -39,6 +39,9 @@
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Date;
 
 @RunWith(AndroidJUnit4.class)
 public class X509UtilsTest {
@@ -133,6 +136,36 @@
         assertThat(X509Utils.isCertChainValid(certChain)).isFalse();
     }
 
+    @Test
+    public void testCertChainExpirationTimeWhenRootExpiresLater() throws Exception {
+        KeyPair root = generateEcdsaKeyPair();
+        KeyPair leaf = generateEcdsaKeyPair();
+        X509Certificate[] certs = new X509Certificate[2];
+
+        // root cert expires later.
+        certs[0] = signPublicKey(root, leaf.getPublic(), Instant.now().plus(Duration.ofDays(2)));
+        certs[1] = signPublicKey(root, root.getPublic(), Instant.now().plus(Duration.ofDays(1)));
+
+        Date expirationTime = X509Utils.getExpirationTimeForCertificateChain(certs);
+        assertThat(expirationTime).isEqualTo(certs[1].getNotAfter());
+        assertThat(expirationTime).isNotEqualTo(certs[0].getNotAfter());
+    }
+
+    @Test
+    public void testCertChainExpirationTimeWhenLeafExpiresLater() throws Exception {
+        KeyPair root = generateEcdsaKeyPair();
+        KeyPair leaf = generateEcdsaKeyPair();
+        X509Certificate[] certs = new X509Certificate[2];
+
+        // leaf cert expires later.
+        certs[0] = signPublicKey(root, leaf.getPublic(), Instant.now().plus(Duration.ofDays(1)));
+        certs[1] = signPublicKey(root, root.getPublic(), Instant.now().plus(Duration.ofDays(2)));
+
+        Date expirationTime = X509Utils.getExpirationTimeForCertificateChain(certs);
+        assertThat(expirationTime).isEqualTo(certs[0].getNotAfter());
+        assertThat(expirationTime).isNotEqualTo(certs[1].getNotAfter());
+    }
+
     private X509Certificate generateCertificateFromEncodedBytes(String encodedCert)
             throws CertificateException {
         CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
diff --git a/app/tests/util/Android.bp b/app/tests/util/Android.bp
index 6b8daa8..c12f372 100644
--- a/app/tests/util/Android.bp
+++ b/app/tests/util/Android.bp
@@ -25,7 +25,7 @@
         "androidx.room_room-runtime",
         "libnanohttpd",
         "libprotobuf-java-lite",
-        "truth-prebuilt",
+        "truth",
     ],
     plugins: [
         "androidx.room_room-compiler-plugin",
diff --git a/app/tests/util/src/com/android/rkpdapp/testutil/NetworkUtils.java b/app/tests/util/src/com/android/rkpdapp/testutil/NetworkUtils.java
deleted file mode 100644
index 77a5d96..0000000
--- a/app/tests/util/src/com/android/rkpdapp/testutil/NetworkUtils.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2023 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.rkpdapp.testutil;
-
-import android.content.Context;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-
-public class NetworkUtils {
-    public static boolean isNetworkConnected(Context context) {
-        ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
-        NetworkInfo networkInfo = cm.getActiveNetworkInfo();
-        if (networkInfo != null && networkInfo.isConnected()) {
-            return true;
-        }
-        return false;
-    }
-}
diff --git a/app/tests/util/src/com/android/rkpdapp/testutil/SystemInterfaceSelector.java b/app/tests/util/src/com/android/rkpdapp/testutil/SystemInterfaceSelector.java
new file mode 100644
index 0000000..0f49691
--- /dev/null
+++ b/app/tests/util/src/com/android/rkpdapp/testutil/SystemInterfaceSelector.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 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.rkpdapp.testutil;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+
+import android.hardware.security.keymint.IRemotelyProvisionedComponent;
+
+import com.android.rkpdapp.interfaces.ServiceManagerInterface;
+import com.android.rkpdapp.interfaces.SystemInterface;
+
+public class SystemInterfaceSelector {
+    /**
+     * Gets the system interface object for provided service name.
+     */
+    public static SystemInterface getSystemInterfaceForServiceName(String serviceName) {
+        SystemInterface matchingInterface = null;
+        for (SystemInterface systemInterface: ServiceManagerInterface.getAllInstances()) {
+            if (systemInterface.getServiceName().equals(serviceName)) {
+                matchingInterface = systemInterface;
+            }
+        }
+        if (matchingInterface == null) {
+            assertThat(serviceName).isEqualTo(IRemotelyProvisionedComponent.DESCRIPTOR + "/avf");
+            assume().withMessage("AVF is not supported by this system").fail();
+        }
+        return matchingInterface;
+    }
+}
diff --git a/system-server/tests/unit/Android.bp b/system-server/tests/unit/Android.bp
index 3aac9e7..a44c774 100644
--- a/system-server/tests/unit/Android.bp
+++ b/system-server/tests/unit/Android.bp
@@ -24,7 +24,7 @@
         "androidx.test.runner",
         "mockito-target",
         "service-rkp.impl",
-        "truth-prebuilt",
+        "truth",
     ],
     libs: [
         "android.test.mock",
diff --git a/system-server/tests/unit/AndroidTest.xml b/system-server/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..517d827
--- /dev/null
+++ b/system-server/tests/unit/AndroidTest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2022 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.
+-->
+<configuration description="Unit tests for the rkpd system server.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="service-rkp-unittest.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="android.security.rkp.service.test" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </test>
+
+    <!-- Only run if RKPD mainline module is installed -->
+    <object type="module_controller"
+            class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="enable" value="true" />
+        <option name="mainline-module-package-name" value="com.android.rkpd" />
+        <option name="mainline-module-package-name" value="com.google.android.rkpd" />
+    </object>
+</configuration>