Add missing ABSL includes. am: 0c5056789e

Original change: https://android-review.googlesource.com/c/platform/packages/modules/OnDevicePersonalization/+/3348651

Change-Id: I0c466eaaf6c9ef4f0b2097c9fad7c9b0bf54686a
Signed-off-by: Automerger Merge Worker <[email protected]>
diff --git a/Android.bp b/Android.bp
index 1914698..7e661a5 100644
--- a/Android.bp
+++ b/Android.bp
@@ -110,6 +110,7 @@
     ],
     static_libs: [
         "androidx.concurrent_concurrent-futures",
+        "federated-compute-java-proto-lite",
         "guava",
         "kotlin-stdlib",
         "kotlinx_coroutines",
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 5c57e47..77027b6 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -27,7 +27,7 @@
 
     <uses-permission android:name="android.permission.INTERNET"/>
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
-    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+    <uses-permission android:name="android.permission.READ_BASIC_PHONE_STATE"/>
 
     <!-- Required for the app to find all packages onboarded to ODP -->
     <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
@@ -62,11 +62,6 @@
                 <action android:name="android.OnDevicePersonalizationService" />
             </intent-filter>
         </service>
-        <service android:name=".OnDevicePersonalizationConfigServiceImpl" android:exported="true">
-            <intent-filter>
-                <action android:name="android.OnDevicePersonalizationConfigService"/>
-            </intent-filter>
-        </service>
         <service android:name=".OnDevicePersonalizationDebugServiceImpl" android:exported="true">
             <intent-filter>
                 <action android:name="android.OnDevicePersonalizationDebugService"/>
@@ -105,6 +100,11 @@
             android:exported="false"
             android:permission="android.permission.BIND_JOB_SERVICE">
         </service>
+        <service
+            android:name="com.android.ondevicepersonalization.services.data.errors.AggregateErrorDataReportingService"
+            android:exported="false"
+            android:permission="android.permission.BIND_JOB_SERVICE">
+        </service>
         <service android:name="com.android.ondevicepersonalization.libraries.plugin.internal.PluginExecutorService"
                  android:isolatedProcess="true"
                  android:process=":plugin_disable_art_image_"
diff --git a/OWNERS b/OWNERS
index 58aac2c..1e6d84a 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,15 +1,16 @@
 # Bug component: 1117807
 [email protected]
[email protected]
[email protected]
 [email protected]
[email protected]
 [email protected]
 [email protected]
[email protected]
 [email protected]
[email protected]
 [email protected]
 [email protected]
 [email protected]
 [email protected]
 [email protected]
 [email protected]
[email protected]
[email protected]
diff --git a/TEST_MAPPING b/TEST_MAPPING
index aeab832..d91e2fc 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -80,6 +80,9 @@
     },
     {
       "name": "CtsOnDevicePersonalizationE2ETests"
+    },
+    {
+      "name": "CtsOnDevicePersonalizationConfigTests"
     }
   ]
 }
diff --git a/federatedcompute/src/com/android/federatedcompute/services/common/FileUtils.java b/common/java/com/android/odp/module/common/FileUtils.java
similarity index 87%
rename from federatedcompute/src/com/android/federatedcompute/services/common/FileUtils.java
rename to common/java/com/android/odp/module/common/FileUtils.java
index 7549029..9e9a810 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/common/FileUtils.java
+++ b/common/java/com/android/odp/module/common/FileUtils.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -14,19 +14,21 @@
  * limitations under the License.
  */
 
-package com.android.federatedcompute.services.common;
+package com.android.odp.module.common;
 
 import android.os.ParcelFileDescriptor;
 
 import com.android.federatedcompute.internal.util.LogUtil;
 
 import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 
 /** Utils related to {@link File} and {@link ParcelFileDescriptor}. */
 public class FileUtils {
@@ -60,11 +62,18 @@
 
     /** Write the provided data to the file. */
     public static void writeToFile(String fileName, byte[] data) throws IOException {
-        FileOutputStream out = new FileOutputStream(fileName);
+        OutputStream out = new BufferedOutputStream(new FileOutputStream(fileName));
         out.write(data);
         out.close();
     }
 
+    /** Write the provided data to the file. */
+    public static long writeToFile(String fileName, InputStream inputStream) throws IOException {
+        try (OutputStream out = new BufferedOutputStream(new FileOutputStream(fileName))) {
+            return inputStream.transferTo(out);
+        }
+    }
+
     /** Read the input file content to a byte array. */
     public static byte[] readFileAsByteArray(String filePath) throws IOException {
         File file = new File(filePath);
diff --git a/common/java/com/android/odp/module/common/HttpClient.java b/common/java/com/android/odp/module/common/HttpClient.java
new file mode 100644
index 0000000..aae4f8a
--- /dev/null
+++ b/common/java/com/android/odp/module/common/HttpClient.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 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.odp.module.common;
+
+import static com.android.odp.module.common.HttpClientUtils.HTTP_OK_STATUS;
+
+import android.annotation.NonNull;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
+import java.io.IOException;
+import java.util.concurrent.Callable;
+
+/**
+ * The HTTP client to be used by FederatedCompute and ODP services/jobs to communicate with remote
+ * servers.
+ */
+public class HttpClient {
+
+    interface HttpIOSupplier<T> {
+        T get() throws IOException; // Declared to throw IOException
+    }
+
+    private final int mRetryLimit;
+
+    /** The executor to use for making http requests. */
+    private final ListeningExecutorService mBlockingExecutor;
+
+    public HttpClient(int retryLimit, ListeningExecutorService blockingExecutor) {
+        mRetryLimit = retryLimit;
+        mBlockingExecutor = blockingExecutor;
+    }
+
+    /**
+     * Perform HTTP requests based on given {@link OdpHttpRequest} asynchronously with configured
+     * number of retries.
+     *
+     * <p>Retry limit provided during construction is used in case http does not return {@code OK}
+     * response code.
+     */
+    @NonNull
+    public ListenableFuture<OdpHttpResponse> performRequestAsyncWithRetry(OdpHttpRequest request) {
+        return performCallableAsync(
+                () -> performRequestWithRetry(() -> HttpClientUtils.performRequest(request)));
+    }
+
+    /**
+     * Perform HTTP requests based on given information asynchronously with retries in case http
+     * will return not OK response code. Payload will be saved directly into the file.
+     */
+    @NonNull
+    public ListenableFuture<OdpHttpResponse> performRequestIntoFileAsyncWithRetry(
+            OdpHttpRequest request) {
+        return performCallableAsync(
+                () -> performRequestWithRetry(() -> HttpClientUtils.performRequest(request, true)));
+    }
+
+    /**
+     * Perform HTTP requests based on given information asynchronously with retries in case http
+     * will return not OK response code.
+     */
+    @NonNull
+    private ListenableFuture<OdpHttpResponse> performCallableAsync(
+            Callable<OdpHttpResponse> callable) {
+        try {
+            return mBlockingExecutor.submit(callable);
+        } catch (Exception e) {
+            return Futures.immediateFailedFuture(e);
+        }
+    }
+
+    /** Perform HTTP requests based on given information with retries. */
+    @NonNull
+    @VisibleForTesting
+    OdpHttpResponse performRequestWithRetry(HttpIOSupplier<OdpHttpResponse> supplier)
+            throws IOException {
+        OdpHttpResponse response = null;
+        int retryLimit = mRetryLimit;
+        while (retryLimit > 0) {
+            try {
+                response = supplier.get();
+                if (HTTP_OK_STATUS.contains(response.getStatusCode())) {
+                    return response;
+                }
+                // we want to continue retry in case it is IO exception.
+            } catch (IOException e) {
+                // propagate IO exception after RETRY_LIMIT times attempt.
+                if (retryLimit <= 1) {
+                    throw e;
+                }
+            } finally {
+                retryLimit--;
+            }
+        }
+        return response;
+    }
+}
diff --git a/common/java/com/android/odp/module/common/HttpClientUtils.java b/common/java/com/android/odp/module/common/HttpClientUtils.java
new file mode 100644
index 0000000..f6b5138
--- /dev/null
+++ b/common/java/com/android/odp/module/common/HttpClientUtils.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2024 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.odp.module.common;
+
+import static com.android.odp.module.common.FileUtils.createTempFile;
+import static com.android.odp.module.common.FileUtils.writeToFile;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.federatedcompute.internal.util.LogUtil;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.internal.federatedcompute.v1.ResourceCapabilities;
+import com.google.internal.federatedcompute.v1.ResourceCompressionFormat;
+import com.google.protobuf.ByteString;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+/** Shared utilities for http connections used by fcp and odp server requests. */
+public class HttpClientUtils {
+    private static final String TAG = HttpClientUtils.class.getSimpleName();
+
+    @VisibleForTesting
+    static final int NETWORK_CONNECT_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(5);
+
+    @VisibleForTesting
+    static final int NETWORK_READ_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(30);
+
+    public static final String CONTENT_ENCODING_HDR = "Content-Encoding";
+
+    public static final String ACCEPT_ENCODING_HDR = "Accept-Encoding";
+    public static final String CONTENT_LENGTH_HDR = "Content-Length";
+    public static final String GZIP_ENCODING_HDR = "gzip";
+    public static final String CONTENT_TYPE_HDR = "Content-Type";
+    public static final String PROTOBUF_CONTENT_TYPE = "application/x-protobuf";
+    public static final String OCTET_STREAM = "application/octet-stream";
+    public static final ImmutableSet<Integer> HTTP_OK_STATUS = ImmutableSet.of(200, 201);
+
+
+    public static final int DEFAULT_BUFFER_SIZE = 1024;
+    public static final byte[] EMPTY_BODY = new byte[0];
+
+    /** Returns the full URI based on the provided base URL and suffix. */
+    public static String joinBaseUriWithSuffix(String baseUri, String suffix) {
+        if (suffix.isEmpty() || !suffix.startsWith("/")) {
+            throw new IllegalArgumentException("uri_suffix be empty or must have a leading '/'");
+        }
+
+        if (baseUri.endsWith("/")) {
+            baseUri = baseUri.substring(0, baseUri.length() - 1);
+        }
+        suffix = suffix.substring(1);
+        return String.join("/", baseUri, suffix);
+    }
+
+    interface HttpURLConnectionSupplier {
+        HttpURLConnection get() throws IOException; // Declared to throw IOException
+    }
+
+    /** Get the current client capabilities. */
+    public static ResourceCapabilities getResourceCapabilities() {
+        // Compression formats supported for resources downloaded via `Resource.uri`.
+        // All clients are assumed to support uncompressed payloads.
+        return ResourceCapabilities.newBuilder()
+                .addSupportedCompressionFormats(
+                        ResourceCompressionFormat.RESOURCE_COMPRESSION_FORMAT_GZIP)
+                .build();
+    }
+
+    /** Compresses the input data using Gzip. */
+    public static byte[] compressWithGzip(byte[] uncompressedData) {
+        try (ByteString.Output outputStream = ByteString.newOutput(uncompressedData.length);
+                GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream)) {
+            gzipOutputStream.write(uncompressedData);
+            gzipOutputStream.finish();
+            return outputStream.toByteString().toByteArray();
+        } catch (IOException e) {
+            LogUtil.e(TAG, "Failed to compress using Gzip");
+            throw new IllegalStateException("Failed to compress using Gzip", e);
+        }
+    }
+
+    /** Un-compresses the input data using Gzip. */
+    public static byte[] uncompressWithGzip(byte[] data) {
+        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
+                GZIPInputStream gzip = new GZIPInputStream(inputStream);
+                ByteArrayOutputStream result = new ByteArrayOutputStream()) {
+            int length;
+            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+            while ((length = gzip.read(buffer, 0, DEFAULT_BUFFER_SIZE)) > 0) {
+                result.write(buffer, 0, length);
+            }
+            return result.toByteArray();
+        } catch (Exception e) {
+            LogUtil.e(TAG, e, "Failed to decompress the data.");
+            throw new IllegalStateException("Failed to un-compress using Gzip", e);
+        }
+    }
+
+    /** Calculates total bytes are sent via network based on provided http request. */
+    public static long getTotalSentBytes(OdpHttpRequest request) {
+        long totalBytes = 0;
+        totalBytes +=
+                request.getHttpMethod().name().length()
+                        + " ".length()
+                        + request.getUri().length()
+                        + " HTTP/1.1\r\n".length();
+        for (String key : request.getExtraHeaders().keySet()) {
+            totalBytes +=
+                    key.length()
+                            + ": ".length()
+                            + request.getExtraHeaders().get(key).length()
+                            + "\r\n".length();
+        }
+        if (request.getExtraHeaders().containsKey(CONTENT_LENGTH_HDR)) {
+            totalBytes += Long.parseLong(request.getExtraHeaders().get(CONTENT_LENGTH_HDR));
+        }
+        return totalBytes;
+    }
+
+    /** Calculates total bytes are received via network based on provided http response. */
+    public static long getTotalReceivedBytes(OdpHttpResponse response) {
+        long totalBytes = 0;
+        boolean foundContentLengthHdr = false;
+        for (Map.Entry<String, List<String>> header : response.getHeaders().entrySet()) {
+            if (header.getKey() == null) {
+                continue;
+            }
+            for (String headerValue : header.getValue()) {
+                totalBytes += header.getKey().length() + ": ".length();
+                totalBytes += headerValue == null ? 0 : headerValue.length();
+            }
+            // Uses Content-Length header to estimate total received bytes which is the most
+            // accurate.
+            if (header.getKey().equals(CONTENT_LENGTH_HDR)) {
+                totalBytes += Long.parseLong(header.getValue().get(0));
+                foundContentLengthHdr = true;
+            }
+        }
+        if (!foundContentLengthHdr) {
+            if (response.getPayload() != null) {
+                totalBytes += response.getPayload().length;
+            } else if (response.getPayloadFileName() != null) {
+                totalBytes += response.getDownloadedPayloadSize();
+            }
+        }
+        return totalBytes;
+    }
+
+    /** Opens a {@link URLConnection} to the specified URL with default timeouts. */
+    @VisibleForTesting
+    @NonNull
+    static URLConnection setup(@NonNull URL url) throws IOException {
+        Objects.requireNonNull(url);
+        URLConnection urlConnection = url.openConnection();
+        urlConnection.setConnectTimeout(NETWORK_CONNECT_TIMEOUT_MS);
+        urlConnection.setReadTimeout(NETWORK_READ_TIMEOUT_MS);
+        return urlConnection;
+    }
+
+    /** Perform HTTP requests based on given information and returns the {@link OdpHttpResponse}. */
+    @NonNull
+    public static OdpHttpResponse performRequest(OdpHttpRequest request) throws IOException {
+        return performRequest(request, /* savePayloadIntoFile= */ false);
+    }
+
+    /** Perform HTTP requests based on given information and returns the {@link OdpHttpResponse}. */
+    @NonNull
+    public static OdpHttpResponse performRequest(
+            OdpHttpRequest request, boolean savePayloadIntoFile) throws IOException {
+        if (request.getUri() == null || request.getHttpMethod() == null) {
+            LogUtil.e(TAG, "Endpoint or http method is empty");
+            throw new IllegalArgumentException("Endpoint or http method is empty");
+        }
+
+        URL url;
+        try {
+            url = new URL(request.getUri());
+        } catch (MalformedURLException e) {
+            LogUtil.e(TAG, e, "Malformed registration target URL");
+            throw new IllegalArgumentException("Malformed registration target URL", e);
+        }
+
+        return performRequest(request, () -> (HttpURLConnection) setup(url), savePayloadIntoFile);
+    }
+
+    @NonNull
+    @VisibleForTesting
+    static OdpHttpResponse performRequest(
+            OdpHttpRequest request,
+            HttpURLConnectionSupplier urlConnectionProvider,
+            boolean savePayloadIntoFile)
+            throws IOException {
+        HttpURLConnection urlConnection;
+        try {
+            urlConnection = urlConnectionProvider.get();
+        } catch (Exception e) {
+            LogUtil.e(TAG, e, "Failed to open target URL");
+            throw new IOException("Failed to open target URL", e);
+        }
+
+        try {
+            urlConnection.setRequestMethod(request.getHttpMethod().name());
+            urlConnection.setInstanceFollowRedirects(true);
+
+            if (request.getExtraHeaders() != null && !request.getExtraHeaders().isEmpty()) {
+                for (Map.Entry<String, String> entry : request.getExtraHeaders().entrySet()) {
+                    urlConnection.setRequestProperty(entry.getKey(), entry.getValue());
+                }
+            }
+
+            if (request.getBody() != null && request.getBody().length > 0) {
+                urlConnection.setDoOutput(true);
+                try (BufferedOutputStream out =
+                        new BufferedOutputStream(urlConnection.getOutputStream())) {
+                    out.write(request.getBody());
+                }
+            }
+
+            int responseCode = urlConnection.getResponseCode();
+            if (HTTP_OK_STATUS.contains(responseCode)) {
+                OdpHttpResponse.Builder builder =
+                        new OdpHttpResponse.Builder()
+                                .setHeaders(urlConnection.getHeaderFields())
+                                .setStatusCode(responseCode);
+                if (savePayloadIntoFile) {
+                    String inputFile = createTempFile("input", ".tmp");
+                    long downloadedSize =
+                            saveIntoFile(
+                                    inputFile,
+                                    urlConnection.getInputStream(),
+                                    urlConnection.getContentLengthLong());
+                    if (downloadedSize != 0) {
+                        builder.setPayloadFileName(inputFile);
+                        builder.setDownloadedPayloadSize(downloadedSize);
+                    }
+                } else {
+                    builder.setPayload(
+                            getByteArray(
+                                    urlConnection.getInputStream(),
+                                    urlConnection.getContentLengthLong()));
+                }
+                return builder.build();
+            } else {
+                return new OdpHttpResponse.Builder()
+                        .setPayload(
+                                getByteArray(
+                                        urlConnection.getErrorStream(),
+                                        urlConnection.getContentLengthLong()))
+                        .setHeaders(urlConnection.getHeaderFields())
+                        .setStatusCode(responseCode)
+                        .build();
+            }
+        } catch (IOException e) {
+            LogUtil.e(TAG, e, "Failed to get registration response");
+            throw new IOException("Failed to get registration response", e);
+        } finally {
+            if (urlConnection != null) {
+                urlConnection.disconnect();
+            }
+        }
+    }
+
+    private static long saveIntoFile(String fileName, @Nullable InputStream in, long contentLength)
+            throws IOException {
+        if (contentLength == 0) {
+            return 0;
+        }
+        try (InputStream bufIn = new BufferedInputStream(in)) {
+            // Process download resource.
+            long downloadedSize = writeToFile(fileName, bufIn);
+            return downloadedSize;
+        }
+    }
+
+    private static byte[] getByteArray(@Nullable InputStream in, long contentLength)
+            throws IOException {
+        if (contentLength == 0) {
+            return EMPTY_BODY;
+        }
+        try {
+            // TODO(b/297952090): evaluate the large file download.
+            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            int bytesRead;
+            while ((bytesRead = in.read(buffer)) != -1) {
+                out.write(buffer, 0, bytesRead);
+            }
+            return out.toByteArray();
+        } finally {
+            in.close();
+        }
+    }
+
+    private HttpClientUtils() {}
+
+    /** The supported http methods. */
+    public enum HttpMethod {
+        GET,
+        POST,
+        PUT,
+    }
+}
diff --git a/federatedcompute/src/com/android/federatedcompute/services/http/FederatedComputeHttpRequest.java b/common/java/com/android/odp/module/common/OdpHttpRequest.java
similarity index 72%
rename from federatedcompute/src/com/android/federatedcompute/services/http/FederatedComputeHttpRequest.java
rename to common/java/com/android/odp/module/common/OdpHttpRequest.java
index d83cc69..894ae8c 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/http/FederatedComputeHttpRequest.java
+++ b/common/java/com/android/odp/module/common/OdpHttpRequest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -14,26 +14,26 @@
  * limitations under the License.
  */
 
-package com.android.federatedcompute.services.http;
+package com.android.odp.module.common;
 
-import static com.android.federatedcompute.services.http.HttpClientUtil.CONTENT_LENGTH_HDR;
+import static com.android.odp.module.common.HttpClientUtils.CONTENT_LENGTH_HDR;
 
-import com.android.federatedcompute.services.http.HttpClientUtil.HttpMethod;
+import com.android.odp.module.common.HttpClientUtils.HttpMethod;
 
 import java.util.Map;
 
-/** Class to hold FederatedCompute http request. */
-public final class FederatedComputeHttpRequest {
+/** Class to hold http requests for federated compute and other odp use-cases. */
+public final class OdpHttpRequest {
     private static final String TAG = "FCPHttpRequest";
     private static final String HTTPS_SCHEMA = "https://";
     private static final String LOCAL_HOST_URI = "http://localhost:";
 
-    private String mUri;
-    private HttpMethod mHttpMethod;
-    private Map<String, String> mExtraHeaders;
-    private byte[] mBody;
+    private final String mUri;
+    private final HttpMethod mHttpMethod;
+    private final Map<String, String> mExtraHeaders;
+    private final byte[] mBody;
 
-    private FederatedComputeHttpRequest(
+    private OdpHttpRequest(
             String uri, HttpMethod httpMethod, Map<String, String> extraHeaders, byte[] body) {
         this.mUri = uri;
         this.mHttpMethod = httpMethod;
@@ -41,8 +41,8 @@
         this.mBody = body;
     }
 
-    /** Creates a {@link FederatedComputeHttpRequest} based on given inputs. */
-    public static FederatedComputeHttpRequest create(
+    /** Creates a {@link OdpHttpRequest} based on given inputs. */
+    public static OdpHttpRequest create(
             String uri, HttpMethod httpMethod, Map<String, String> extraHeaders, byte[] body) {
         if (!uri.startsWith(HTTPS_SCHEMA) && !uri.startsWith(LOCAL_HOST_URI)) {
             throw new IllegalArgumentException("Non-HTTPS URIs are not supported: " + uri);
@@ -57,7 +57,7 @@
             }
             extraHeaders.put(CONTENT_LENGTH_HDR, String.valueOf(body.length));
         }
-        return new FederatedComputeHttpRequest(uri, httpMethod, extraHeaders, body);
+        return new OdpHttpRequest(uri, httpMethod, extraHeaders, body);
     }
 
     public String getUri() {
diff --git a/federatedcompute/src/com/android/federatedcompute/services/http/FederatedComputeHttpResponse.java b/common/java/com/android/odp/module/common/OdpHttpResponse.java
similarity index 63%
rename from federatedcompute/src/com/android/federatedcompute/services/http/FederatedComputeHttpResponse.java
rename to common/java/com/android/odp/module/common/OdpHttpResponse.java
index 48338f2..494e33c 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/http/FederatedComputeHttpResponse.java
+++ b/common/java/com/android/odp/module/common/OdpHttpResponse.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.federatedcompute.services.http;
+package com.android.odp.module.common;
 
-import static com.android.federatedcompute.services.http.HttpClientUtil.CONTENT_ENCODING_HDR;
-import static com.android.federatedcompute.services.http.HttpClientUtil.GZIP_ENCODING_HDR;
+import static com.android.odp.module.common.HttpClientUtils.CONTENT_ENCODING_HDR;
+import static com.android.odp.module.common.HttpClientUtils.GZIP_ENCODING_HDR;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -26,15 +26,15 @@
 import java.util.List;
 import java.util.Map;
 
-/** Class to hold FederatedCompute http response. */
-public class FederatedComputeHttpResponse {
+public class OdpHttpResponse {
     private Integer mStatusCode;
     private Map<String, List<String>> mHeaders = new HashMap<>();
     private byte[] mPayload;
+    private String mPayloadFileName;
+    private long mDownloadedPayloadSize;
 
-    private FederatedComputeHttpResponse() {}
+    private OdpHttpResponse() {}
 
-    @NonNull
     public int getStatusCode() {
         return mStatusCode;
     }
@@ -49,6 +49,15 @@
         return mPayload;
     }
 
+    @Nullable
+    public String getPayloadFileName() {
+        return mPayloadFileName;
+    }
+
+    public long getDownloadedPayloadSize() {
+        return mDownloadedPayloadSize;
+    }
+
     /** Returns whether http response body is compressed with gzip. */
     public boolean isResponseCompressed() {
         if (mHeaders.containsKey(CONTENT_ENCODING_HDR)) {
@@ -61,13 +70,13 @@
         return false;
     }
 
-    /** Builder for FederatedComputeHttpResponse. */
+    /** Builder for {@link OdpHttpResponse}. */
     public static final class Builder {
-        private final FederatedComputeHttpResponse mHttpResponse;
+        private final OdpHttpResponse mHttpResponse;
 
-        /** Default constructor of {@link FederatedComputeHttpResponse}. */
+        /** Default constructor of {@link OdpHttpResponse}. */
         public Builder() {
-            mHttpResponse = new FederatedComputeHttpResponse();
+            mHttpResponse = new OdpHttpResponse();
         }
 
         /** Set the status code of http response. */
@@ -88,8 +97,20 @@
             return this;
         }
 
-        /** Build {@link FederatedComputeHttpResponse}. */
-        public FederatedComputeHttpResponse build() {
+        /** Set payload file name where payload is saved. */
+        public Builder setPayloadFileName(String fileName) {
+            mHttpResponse.mPayloadFileName = fileName;
+            return this;
+        }
+
+        /** Set payload file name where payload is saved. */
+        public Builder setDownloadedPayloadSize(long downloadedSize) {
+            mHttpResponse.mDownloadedPayloadSize = downloadedSize;
+            return this;
+        }
+
+        /** Build {@link OdpHttpResponse}. */
+        public OdpHttpResponse build() {
             if (mHttpResponse.mStatusCode == null) {
                 throw new IllegalArgumentException("Empty status code.");
             }
diff --git a/federatedcompute/apk/AndroidManifest.xml b/federatedcompute/apk/AndroidManifest.xml
index c317e09..595bb58 100644
--- a/federatedcompute/apk/AndroidManifest.xml
+++ b/federatedcompute/apk/AndroidManifest.xml
@@ -75,7 +75,7 @@
         </service>
         <!-- On BOOT_COMPLETED receiver for registering jobs -->
         <receiver android:name=".FederatedComputeBroadcastReceiver"
-            android:enabled="true"
+            android:enabled="@bool/config_enableBootReceiver"
             android:exported="false">
             <intent-filter>
                 <action android:name="android.intent.action.BOOT_COMPLETED" />
diff --git a/federatedcompute/apk/res/values/config.xml b/federatedcompute/apk/res/values/config.xml
new file mode 100644
index 0000000..03d218e
--- /dev/null
+++ b/federatedcompute/apk/res/values/config.xml
@@ -0,0 +1,4 @@
+<resources>
+    <!-- Enable or disable boot receiver of federatedcompute -->
+    <bool name="config_enableBootReceiver">true</bool>
+</resources>
\ No newline at end of file
diff --git a/federatedcompute/src/com/android/federatedcompute/services/common/Flags.java b/federatedcompute/src/com/android/federatedcompute/services/common/Flags.java
index d3800be..a12659d 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/common/Flags.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/common/Flags.java
@@ -243,7 +243,7 @@
 
     long FCP_DEFAULT_MEMORY_SIZE_LIMIT = 50000000L; // 50 MBs in bytes
 
-    /** Provides upper limit for FCP temp files. */
+    /** Provides lower limit for FCP temp files. */
     default long getFcpMemorySizeLimit() {
         return FCP_DEFAULT_MEMORY_SIZE_LIMIT;
     }
@@ -267,4 +267,11 @@
     default int getFcpTaskLimitPerPackage() {
         return DEFAULT_FCP_TASK_LIMIT_PER_PACKAGE;
     }
+
+    int FCP_DEFAULT_CHECKPOINT_FILE_SIZE_LIMIT = 50000000; // 50 MBs in bytes
+
+    /** Provides upper limit for FCP temp files. */
+    default int getFcpCheckpointFileSizeLimit() {
+        return FCP_DEFAULT_CHECKPOINT_FILE_SIZE_LIMIT;
+    }
 }
diff --git a/federatedcompute/src/com/android/federatedcompute/services/common/PhFlags.java b/federatedcompute/src/com/android/federatedcompute/services/common/PhFlags.java
index ea2e88a..82df441 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/common/PhFlags.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/common/PhFlags.java
@@ -77,6 +77,7 @@
 
     static final String FCP_MEMORY_SIZE_LIMIT_CONFIG_NAME = "memory_size_limit";
     static final String FCP_TASK_LIMIT_PER_PACKAGE_CONFIG_NAME = "task_limit_per_package";
+    static final String FCP_CHECKPOINT_FILE_SIZE_LIMIT_CONFIG_NAME = "checkpoint_file_size_limit";
     static final String FCP_ENABLE_CLIENT_ERROR_LOGGING = "fcp_enable_client_error_logging";
     static final String FCP_ENABLE_BACKGROUND_JOBS_LOGGING = "fcp_enable_background_jobs_logging";
     static final String FCP_BACKGROUND_JOB_LOGGING_SAMPLING_RATE =
@@ -273,18 +274,16 @@
                 /* defaultValue= */ ENABLE_CLIENT_ERROR_LOGGING);
     }
 
+    /**
+     * {@inheritDoc}
+     *
+     * <p>This method always return {@code true} because the underlying flag is fully launched on
+     * {@code FederatedCompute} but the method cannot be removed (as it's defined on {@code
+     * ModuleSharedFlags}).
+     */
     @Override
     public boolean getBackgroundJobsLoggingEnabled() {
-        // needs stable: execution stats may be less accurate if value changed during job execution
-        return (boolean)
-                sStableFlags.computeIfAbsent(
-                        FCP_ENABLE_BACKGROUND_JOBS_LOGGING,
-                        key -> {
-                            return DeviceConfig.getBoolean(
-                                    /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
-                                    /* name= */ FCP_ENABLE_BACKGROUND_JOBS_LOGGING,
-                                    /* defaultValue= */ BACKGROUND_JOB_LOGGING_ENABLED);
-                        });
+        return true;
     }
 
     @Override
@@ -366,4 +365,12 @@
                 /* name= */ FCP_TASK_LIMIT_PER_PACKAGE_CONFIG_NAME,
                 /* defaultValue= */ DEFAULT_FCP_TASK_LIMIT_PER_PACKAGE);
     }
+
+    @Override
+    public int getFcpCheckpointFileSizeLimit() {
+        return DeviceConfig.getInt(
+                /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                /* name= */ FCP_CHECKPOINT_FILE_SIZE_LIMIT_CONFIG_NAME,
+                /* defaultValue= */ FCP_DEFAULT_CHECKPOINT_FILE_SIZE_LIMIT);
+    }
 }
diff --git a/federatedcompute/src/com/android/federatedcompute/services/common/TrainingEventLogger.java b/federatedcompute/src/com/android/federatedcompute/services/common/TrainingEventLogger.java
index 6fb6be0..f9541a2 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/common/TrainingEventLogger.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/common/TrainingEventLogger.java
@@ -25,15 +25,18 @@
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_DOWNLOAD_PLAN_URI_RECEIVED;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_DOWNLOAD_STARTED;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_DOWNLOAD_TURNED_AWAY;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_DOWNLOAD_TURNED_AWAY_NO_TASK_AVAILABLE;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_DOWNLOAD_TURNED_AWAY_UNAUTHENTICATED;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_DOWNLOAD_TURNED_AWAY_UNAUTHORIZED;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_FAILURE_UPLOADED;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_FAILURE_UPLOAD_STARTED;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_INITIATE_REPORT_RESULT_AUTH_SUCCEEDED;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_KEY_ATTESTATION_SUCCEEDED;
-import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_NOT_STARTED;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_REPORT_RESULT_UNAUTHORIZED;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RESULT_UPLOADED;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RESULT_UPLOAD_SERVER_ABORTED;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RESULT_UPLOAD_STARTED;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_CONDITIONS_FAILED;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_TASK_ASSIGNMENT_AUTH_SUCCEEDED;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_TASK_ASSIGNMENT_UNAUTHORIZED;
 
@@ -41,6 +44,8 @@
 import com.android.federatedcompute.services.statsd.FederatedComputeStatsdLogger;
 import com.android.federatedcompute.services.statsd.TrainingEventReported;
 
+import com.google.internal.federatedcompute.v1.RejectionInfo;
+
 /** The helper function to log {@link TrainingEventReported} in statsd. */
 public class TrainingEventLogger {
     private static final String TAG = TrainingEventLogger.class.getSimpleName();
@@ -70,7 +75,7 @@
         TrainingEventReported.Builder event =
                 new TrainingEventReported.Builder()
                         .setEventKind(
-                                FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_NOT_STARTED);
+                                FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_CONDITIONS_FAILED);
         logEvent(event);
     }
 
@@ -84,10 +89,28 @@
     }
 
     /** Logs when device is turned away from federated training. */
-    public void logCheckinRejected(NetworkStats networkStats) {
-        logNetworkEvent(
-                FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_DOWNLOAD_TURNED_AWAY,
-                networkStats);
+    public void logCheckinRejected(RejectionInfo rejectionInfo, NetworkStats networkStats) {
+        switch (rejectionInfo.getReason()) {
+            case UNAUTHORIZED:
+                logNetworkEvent(
+                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_DOWNLOAD_TURNED_AWAY_UNAUTHORIZED,
+                        networkStats);
+                break;
+            case UNAUTHENTICATED:
+                logNetworkEvent(
+                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_DOWNLOAD_TURNED_AWAY_UNAUTHENTICATED,
+                        networkStats);
+                break;
+            case NO_TASK_AVAILABLE:
+                logNetworkEvent(
+                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_DOWNLOAD_TURNED_AWAY_NO_TASK_AVAILABLE,
+                        networkStats);
+                break;
+            default:
+                logNetworkEvent(
+                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_DOWNLOAD_TURNED_AWAY,
+                        networkStats);
+        }
     }
 
     /**
diff --git a/federatedcompute/src/com/android/federatedcompute/services/data/FederatedComputeDbHelper.java b/federatedcompute/src/com/android/federatedcompute/services/data/FederatedComputeDbHelper.java
index a7be7b6..f82a9f1 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/data/FederatedComputeDbHelper.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/data/FederatedComputeDbHelper.java
@@ -148,14 +148,12 @@
      * only.
      */
     @VisibleForTesting
-    public static FederatedComputeDbHelper getInstanceForTest(Context context) {
-        synchronized (FederatedComputeDbHelper.class) {
-            if (sInstance == null) {
-                // Use null database name to make it in-memory
-                sInstance = new FederatedComputeDbHelper(context, null);
-            }
-            return sInstance;
+    public static synchronized FederatedComputeDbHelper getInstanceForTest(Context context) {
+        if (sInstance == null) {
+            // Use null database name to make it in-memory
+            sInstance = new FederatedComputeDbHelper(context, null);
         }
+        return sInstance;
     }
 
     /**
@@ -263,12 +261,10 @@
 
     /** It's only public to testing. */
     @VisibleForTesting
-    public static void resetInstance() {
-        synchronized (FederatedComputeDbHelper.class) {
-            if (sInstance != null) {
-                sInstance.close();
-                sInstance = null;
-            }
+    public static synchronized void resetInstance() {
+        if (sInstance != null) {
+            sInstance.close();
+            sInstance = null;
         }
     }
 
diff --git a/federatedcompute/src/com/android/federatedcompute/services/data/FederatedComputeEncryptionKeyContract.java b/federatedcompute/src/com/android/federatedcompute/services/data/FederatedComputeEncryptionKeyContract.java
index f5c6d07..81148ec 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/data/FederatedComputeEncryptionKeyContract.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/data/FederatedComputeEncryptionKeyContract.java
@@ -21,7 +21,7 @@
 
     private FederatedComputeEncryptionKeyContract() {}
 
-    public static final class FederatedComputeEncryptionColumns {
+    static final class FederatedComputeEncryptionColumns {
         private FederatedComputeEncryptionColumns() {}
 
         /**
diff --git a/federatedcompute/src/com/android/federatedcompute/services/data/FederatedComputeEncryptionKeyDao.java b/federatedcompute/src/com/android/federatedcompute/services/data/FederatedComputeEncryptionKeyDao.java
index 33282de..82cf969 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/data/FederatedComputeEncryptionKeyDao.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/data/FederatedComputeEncryptionKeyDao.java
@@ -35,7 +35,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
-/** DAO for accessing encryption key table */
+/** DAO for accessing encryption key table that stores {@link FederatedComputeEncryptionKey}s. */
 public class FederatedComputeEncryptionKeyDao {
     private static final String TAG = FederatedComputeEncryptionKeyDao.class.getSimpleName();
 
@@ -50,9 +50,7 @@
         mClock = clock;
     }
 
-    /**
-     * @return an instance of FederatedComputeEncryptionKeyDao given a context
-     */
+    /** Returns an instance of {@link FederatedComputeEncryptionKeyDao} given a context. */
     @NonNull
     public static FederatedComputeEncryptionKeyDao getInstance(Context context) {
         if (sSingletonInstance == null) {
@@ -68,7 +66,11 @@
         return sSingletonInstance;
     }
 
-    /** It is only public to unit test. */
+    /**
+     * Helper method to get instance of {@link FederatedComputeEncryptionKeyDao} for use in tests.
+     *
+     * <p>Public for use in unit tests.
+     */
     @VisibleForTesting
     public static FederatedComputeEncryptionKeyDao getInstanceForTest(Context context) {
         if (sSingletonInstance == null) {
@@ -84,7 +86,12 @@
         return sSingletonInstance;
     }
 
-    /** Insert a key to the encryption_key table. */
+    /**
+     * Insert a key to the encryption_key table.
+     *
+     * @param key the {@link FederatedComputeEncryptionKey} to insert into DB.
+     * @return Whether the key was inserted successfully.
+     */
     public boolean insertEncryptionKey(FederatedComputeEncryptionKey key) {
         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
         if (db == null) {
@@ -98,16 +105,16 @@
         values.put(FederatedComputeEncryptionColumns.CREATION_TIME, key.getCreationTime());
         values.put(FederatedComputeEncryptionColumns.EXPIRY_TIME, key.getExpiryTime());
 
-        long jobId =
+        long insertedRowId =
                 db.insertWithOnConflict(
                         ENCRYPTION_KEY_TABLE, "", values, SQLiteDatabase.CONFLICT_REPLACE);
-        return jobId != -1;
+        return insertedRowId != -1;
     }
 
     /**
-     * Read from encryption key table given selection, order and limit conidtions.
+     * Read from encryption key table given selection, order and limit conditions.
      *
-     * @return a list of {@link FederatedComputeEncryptionKey}.
+     * @return a list of matching {@link FederatedComputeEncryptionKey}s.
      */
     @VisibleForTesting
     public List<FederatedComputeEncryptionKey> readFederatedComputeEncryptionKeysFromDatabase(
diff --git a/federatedcompute/src/com/android/federatedcompute/services/encryption/BackgroundKeyFetchJobService.java b/federatedcompute/src/com/android/federatedcompute/services/encryption/BackgroundKeyFetchJobService.java
index d79743a..91ad4ef 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/encryption/BackgroundKeyFetchJobService.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/encryption/BackgroundKeyFetchJobService.java
@@ -49,6 +49,7 @@
     private static final int ENCRYPTION_KEY_FETCH_JOB_ID =
             FederatedComputeJobInfo.ENCRYPTION_KEY_FETCH_JOB_ID;
 
+    @VisibleForTesting
     static class Injector {
         ListeningExecutorService getExecutor() {
             return FederatedComputeExecutors.getBackgroundExecutor();
@@ -66,7 +67,7 @@
     private final Injector mInjector;
 
     public BackgroundKeyFetchJobService() {
-        mInjector = new Injector();
+        this(new Injector());
     }
 
     @VisibleForTesting
diff --git a/federatedcompute/src/com/android/federatedcompute/services/encryption/Encrypter.java b/federatedcompute/src/com/android/federatedcompute/services/encryption/Encrypter.java
index 7398e1f..3c4adaa 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/encryption/Encrypter.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/encryption/Encrypter.java
@@ -20,7 +20,7 @@
 public interface Encrypter {
 
     /**
-     * encrypt {@code plainText} to cipher text {@code byte[]}.
+     * Encrypt the {@code plainText} to cipher text {@code byte[]}.
      *
      * @param publicKey the public key used for encryption
      * @param plainText the plain text string to encrypt
@@ -28,5 +28,4 @@
      * @return the encrypted ciphertext
      */
     byte[] encrypt(byte[] publicKey, byte[] plainText, byte[] associatedData);
-
 }
diff --git a/federatedcompute/src/com/android/federatedcompute/services/encryption/FederatedComputeEncryptionKeyManager.java b/federatedcompute/src/com/android/federatedcompute/services/encryption/FederatedComputeEncryptionKeyManager.java
index eef3371..af728d9 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/encryption/FederatedComputeEncryptionKeyManager.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/encryption/FederatedComputeEncryptionKeyManager.java
@@ -24,12 +24,13 @@
 import com.android.federatedcompute.services.common.FlagsFactory;
 import com.android.federatedcompute.services.data.FederatedComputeEncryptionKey;
 import com.android.federatedcompute.services.data.FederatedComputeEncryptionKeyDao;
-import com.android.federatedcompute.services.http.FederatedComputeHttpRequest;
-import com.android.federatedcompute.services.http.FederatedComputeHttpResponse;
-import com.android.federatedcompute.services.http.HttpClient;
 import com.android.federatedcompute.services.http.HttpClientUtil;
 import com.android.odp.module.common.Clock;
+import com.android.odp.module.common.HttpClient;
+import com.android.odp.module.common.HttpClientUtils;
 import com.android.odp.module.common.MonotonicClock;
+import com.android.odp.module.common.OdpHttpRequest;
+import com.android.odp.module.common.OdpHttpResponse;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
@@ -91,24 +92,21 @@
         mBackgroundExecutor = backgroundExecutor;
     }
 
-    /**
-     * @return a singleton instance for key manager
-     */
+    /** Returns a singleton instance for the {@link FederatedComputeEncryptionKeyManager}. */
     public static FederatedComputeEncryptionKeyManager getInstance(Context context) {
         if (sBackgroundKeyManager == null) {
             synchronized (FederatedComputeEncryptionKeyManager.class) {
                 if (sBackgroundKeyManager == null) {
                     FederatedComputeEncryptionKeyDao encryptionKeyDao =
                             FederatedComputeEncryptionKeyDao.getInstance(context);
-                    HttpClient client = new HttpClient();
-                    Clock clock = MonotonicClock.getInstance();
-                    Flags flags = FlagsFactory.getFlags();
                     sBackgroundKeyManager =
                             new FederatedComputeEncryptionKeyManager(
-                                    clock,
+                                    MonotonicClock.getInstance(),
                                     encryptionKeyDao,
-                                    flags,
-                                    client,
+                                    FlagsFactory.getFlags(),
+                                    new HttpClient(
+                                            FlagsFactory.getFlags().getHttpRequestRetryLimit(),
+                                            FederatedComputeExecutors.getBlockingExecutor()),
                                     FederatedComputeExecutors.getBackgroundExecutor());
                 }
             }
@@ -118,7 +116,7 @@
 
     /** For testing only, returns an instance of key manager for test. */
     @VisibleForTesting
-    public static FederatedComputeEncryptionKeyManager getInstanceForTest(
+    static FederatedComputeEncryptionKeyManager getInstanceForTest(
             Clock clock,
             FederatedComputeEncryptionKeyDao encryptionKeyDao,
             Flags flags,
@@ -140,7 +138,7 @@
      * Fetch the active key from the server, persists the fetched key to encryption_key table, and
      * deletes expired keys
      */
-    public FluentFuture<List<FederatedComputeEncryptionKey>> fetchAndPersistActiveKeys(
+    FluentFuture<List<FederatedComputeEncryptionKey>> fetchAndPersistActiveKeys(
             @FederatedComputeEncryptionKey.KeyType int keyType, boolean isScheduledJob) {
         String fetchUri = mFlags.getEncryptionKeyFetchUrl();
         if (fetchUri == null) {
@@ -148,13 +146,13 @@
                     new IllegalArgumentException("Url to fetch active encryption keys is null")));
         }
 
-        FederatedComputeHttpRequest request;
+        OdpHttpRequest request;
         try {
             request =
-                    FederatedComputeHttpRequest.create(
+                    OdpHttpRequest.create(
                             fetchUri,
-                            HttpClientUtil.HttpMethod.GET,
-                            new HashMap<String, String>(),
+                            HttpClientUtils.HttpMethod.GET,
+                            new HashMap<>(),
                             HttpClientUtil.EMPTY_BODY);
         } catch (Exception e) {
             return FluentFuture.from(Futures.immediateFailedFuture(e));
@@ -180,7 +178,7 @@
     }
 
     private ImmutableList<FederatedComputeEncryptionKey> parseFetchEncryptionKeyPayload(
-            FederatedComputeHttpResponse keyFetchResponse,
+            OdpHttpResponse keyFetchResponse,
             @FederatedComputeEncryptionKey.KeyType int keyType,
             Long fetchTime) {
         String payload = new String(Objects.requireNonNull(keyFetchResponse.getPayload()));
@@ -224,7 +222,7 @@
 
     /**
      * Parse the "age" and "cache-control" of response headers. Calculate the ttl of the current key
-     * maxage (in cache-control) - age.
+     * max-age (in cache-control) - age.
      *
      * @return the ttl in seconds of the keys.
      */
@@ -242,7 +240,6 @@
                         cacheControl = field.get(0).toLowerCase(Locale.ENGLISH);
                         remainingHeaders -= 1;
                     }
-
                 } else if (key.equalsIgnoreCase(
                         EncryptionKeyResponseContract.RESPONSE_HEADER_AGE_LABEL)) {
                     List<String> field = headers.get(key);
@@ -292,9 +289,11 @@
         return maxAge - cachedAge;
     }
 
-    /** Get active keys, if there is no active key, then force a fetch from the key service.
-     * In the case of key fetching from the key service, the http call
-     * is executed on a BlockingExecutor.
+    /**
+     * Get active keys, if there is no active key, then force a fetch from the key service. In the
+     * case of key fetching from the key service, the http call is executed on a {@code
+     * BlockingExecutor}.
+     *
      * @return The list of active keys.
      */
     public List<FederatedComputeEncryptionKey> getOrFetchActiveKeys(int keyType, int keyCount) {
diff --git a/federatedcompute/src/com/android/federatedcompute/services/examplestore/ExampleStoreServiceProvider.java b/federatedcompute/src/com/android/federatedcompute/services/examplestore/ExampleStoreServiceProvider.java
index 849d91e..f8f1d55 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/examplestore/ExampleStoreServiceProvider.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/examplestore/ExampleStoreServiceProvider.java
@@ -17,6 +17,10 @@
 package com.android.federatedcompute.services.examplestore;
 
 import static com.android.federatedcompute.services.common.Constants.TRACE_GET_EXAMPLE_STORE_ITERATOR;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_ERROR;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_START;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_SUCCESS;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_TIMEOUT;
 
 import android.content.Context;
 import android.federatedcompute.aidl.IExampleStoreCallback;
@@ -24,18 +28,14 @@
 import android.federatedcompute.aidl.IExampleStoreService;
 import android.federatedcompute.common.ClientConstants;
 import android.os.Bundle;
-import android.os.SystemClock;
 import android.os.Trace;
 
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-
 import com.android.federatedcompute.internal.util.AbstractServiceBinder;
 import com.android.federatedcompute.internal.util.LogUtil;
-import com.android.federatedcompute.services.common.ExampleStats;
 import com.android.federatedcompute.services.common.FlagsFactory;
+import com.android.federatedcompute.services.common.TrainingEventLogger;
 import com.android.federatedcompute.services.data.FederatedTrainingTask;
 
-import com.google.common.util.concurrent.ListenableFuture;
 import com.google.internal.federated.plan.ExampleSelector;
 
 import java.util.concurrent.ArrayBlockingQueue;
@@ -69,7 +69,8 @@
             FederatedTrainingTask task,
             String taskName,
             int minExample,
-            ExampleSelector exampleSelector) {
+            ExampleSelector exampleSelector,
+            TrainingEventLogger logger) {
         try {
             Trace.beginAsyncSection(TRACE_GET_EXAMPLE_STORE_ITERATOR, 1);
             Bundle bundle = new Bundle();
@@ -87,6 +88,8 @@
                         ClientConstants.EXTRA_COLLECTION_URI, exampleSelector.getCollectionUri());
             }
             BlockingQueue<CallbackResult> asyncResult = new ArrayBlockingQueue<>(1);
+            logger.logEventKind(
+                    FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_START);
             exampleStoreService.startQuery(
                     bundle,
                     new IExampleStoreCallback.Stub() {
@@ -109,12 +112,23 @@
                             FlagsFactory.getFlags().getExampleStoreServiceCallbackTimeoutSec(),
                             TimeUnit.SECONDS);
             // Callback result is null if timeout.
-            if (callbackResult == null || callbackResult.mErrorCode != 0) {
+            if (callbackResult == null) {
+                logger.logEventKind(
+                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_TIMEOUT);
                 return null;
             }
+            if (callbackResult.mErrorCode != 0 || callbackResult.mIterator == null) {
+                logger.logEventKind(
+                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_ERROR);
+                return null;
+            }
+            logger.logEventKind(
+                    FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_SUCCESS);
             return callbackResult.mIterator;
         } catch (Exception e) {
             LogUtil.e(TAG, e, "Got exception when StartQuery");
+            logger.logEventKind(
+                    FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_ERROR);
             return null;
         }
     }
@@ -128,45 +142,4 @@
             mErrorCode = errorCode;
         }
     }
-
-    private ListenableFuture<IExampleStoreIterator> runExampleStoreStartQuery(
-            IExampleStoreService exampleStoreService,
-            Bundle input,
-            ExampleStats exampleStats,
-            long startCallTimeNanos) {
-        return CallbackToFutureAdapter.getFuture(
-                completer -> {
-                    try {
-                        exampleStoreService.startQuery(
-                                input,
-                                new IExampleStoreCallback.Stub() {
-                                    @Override
-                                    public void onStartQuerySuccess(
-                                            IExampleStoreIterator iterator) {
-                                        LogUtil.d(TAG, "Acquired iterator");
-                                        exampleStats.mStartQueryLatencyNanos.addAndGet(
-                                                SystemClock.elapsedRealtimeNanos()
-                                                        - startCallTimeNanos);
-                                        completer.set(iterator);
-                                        Trace.endAsyncSection(TRACE_GET_EXAMPLE_STORE_ITERATOR, 0);
-                                    }
-
-                                    @Override
-                                    public void onStartQueryFailure(int errorCode) {
-                                        LogUtil.e(TAG, "Could not acquire iterator: " + errorCode);
-                                        exampleStats.mStartQueryLatencyNanos.addAndGet(
-                                                SystemClock.elapsedRealtimeNanos()
-                                                        - startCallTimeNanos);
-                                        completer.setException(
-                                                new IllegalStateException(
-                                                        "StartQuery failed: " + errorCode));
-                                        Trace.endAsyncSection(TRACE_GET_EXAMPLE_STORE_ITERATOR, 0);
-                                    }
-                                });
-                    } catch (Exception e) {
-                        completer.setException(e);
-                    }
-                    return "runExampleStoreStartQuery";
-                });
-    }
 }
diff --git a/federatedcompute/src/com/android/federatedcompute/services/http/CheckinResult.java b/federatedcompute/src/com/android/federatedcompute/services/http/CheckinResult.java
index 00542f4..d940935 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/http/CheckinResult.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/http/CheckinResult.java
@@ -25,22 +25,27 @@
 import com.google.ondevicepersonalization.federatedcompute.proto.TaskAssignment;
 
 /**
- * The result after client calls TaskAssignemnt API. It includes init checkpoint data and plan data.
+ * The result after client calls TaskAssignment API. It includes init checkpoint data and plan data.
  */
 public class CheckinResult {
-    private String mInputCheckpoint = null;
-    private ClientOnlyPlan mPlanData = null;
-    private TaskAssignment mTaskAssignment = null;
-    private RejectionInfo mRejectionInfo = null;
+    private final String mInputCheckpoint;
+    private final ClientOnlyPlan mPlanData;
+    private final TaskAssignment mTaskAssignment;
+    private final RejectionInfo mRejectionInfo;
+
     public CheckinResult(
             String inputCheckpoint, ClientOnlyPlan planData, TaskAssignment taskAssignment) {
         this.mInputCheckpoint = inputCheckpoint;
         this.mPlanData = planData;
         this.mTaskAssignment = taskAssignment;
+        this.mRejectionInfo = null;
     }
 
     public CheckinResult(RejectionInfo mRejectionInfo) {
         this.mRejectionInfo = mRejectionInfo;
+        this.mInputCheckpoint = null;
+        this.mPlanData = null;
+        this.mTaskAssignment = null;
     }
 
     @Nullable
diff --git a/federatedcompute/src/com/android/federatedcompute/services/http/HttpClient.java b/federatedcompute/src/com/android/federatedcompute/services/http/HttpClient.java
deleted file mode 100644
index 5bfd162..0000000
--- a/federatedcompute/src/com/android/federatedcompute/services/http/HttpClient.java
+++ /dev/null
@@ -1,198 +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.federatedcompute.services.http;
-
-import static com.android.federatedcompute.services.common.FederatedComputeExecutors.getBlockingExecutor;
-import static com.android.federatedcompute.services.http.HttpClientUtil.HTTP_OK_STATUS;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-
-import com.android.federatedcompute.internal.util.LogUtil;
-import com.android.federatedcompute.services.common.Flags;
-import com.android.federatedcompute.services.common.PhFlags;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLConnection;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.TimeUnit;
-
-/**
- * The HTTP client to be used by the FederatedCompute to communicate with remote federated servers.
- */
-public class HttpClient {
-    private static final String TAG = HttpClient.class.getSimpleName();
-    private static final int NETWORK_CONNECT_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(5);
-    private static final int NETWORK_READ_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(30);
-    private final Flags mFlags;
-
-    public HttpClient() {
-        mFlags = PhFlags.getInstance();
-    }
-
-    @NonNull
-    @VisibleForTesting
-    URLConnection setup(@NonNull URL url) throws IOException {
-        Objects.requireNonNull(url);
-        URLConnection urlConnection = url.openConnection();
-        urlConnection.setConnectTimeout(NETWORK_CONNECT_TIMEOUT_MS);
-        urlConnection.setReadTimeout(NETWORK_READ_TIMEOUT_MS);
-        return urlConnection;
-    }
-
-    /**
-     * Perform HTTP requests based on given information asynchronously with retries in case http
-     * will return not OK response code.
-     */
-    @NonNull
-    public ListenableFuture<FederatedComputeHttpResponse> performRequestAsyncWithRetry(
-            FederatedComputeHttpRequest request) {
-        try {
-            return getBlockingExecutor().submit(() -> performRequestWithRetry(request));
-        } catch (Exception e) {
-            return Futures.immediateFailedFuture(e);
-        }
-    }
-
-    /** Perform HTTP requests based on given information with retries. */
-    @NonNull
-    public FederatedComputeHttpResponse performRequestWithRetry(FederatedComputeHttpRequest request)
-            throws IOException {
-        int count = 0;
-        FederatedComputeHttpResponse response = null;
-        int retryLimit = mFlags.getHttpRequestRetryLimit();
-        while (count < retryLimit) {
-            try {
-                response = performRequest(request);
-                if (HTTP_OK_STATUS.contains(response.getStatusCode())) {
-                    return response;
-                }
-                // we want to continue retry in case it is IO exception.
-            } catch (IOException e) {
-                // propagate IO exception after RETRY_LIMIT times attempt.
-                if (count >= retryLimit - 1) {
-                    throw e;
-                }
-            } finally {
-                count++;
-            }
-        }
-        return response;
-    }
-
-    /** Perform HTTP requests based on given information. */
-    @NonNull
-    public FederatedComputeHttpResponse performRequest(FederatedComputeHttpRequest request)
-            throws IOException {
-        if (request.getUri() == null || request.getHttpMethod() == null) {
-            LogUtil.e(TAG, "Endpoint or http method is empty");
-            throw new IllegalArgumentException("Endpoint or http method is empty");
-        }
-
-        URL url;
-        try {
-            url = new URL(request.getUri());
-        } catch (MalformedURLException e) {
-            LogUtil.e(TAG, e, "Malformed registration target URL");
-            throw new IllegalArgumentException("Malformed registration target URL", e);
-        }
-
-        HttpURLConnection urlConnection;
-        try {
-            urlConnection = (HttpURLConnection) setup(url);
-        } catch (IOException e) {
-            LogUtil.e(TAG, e, "Failed to open target URL");
-            throw new IOException("Failed to open target URL", e);
-        }
-
-        try {
-            urlConnection.setRequestMethod(request.getHttpMethod().name());
-            urlConnection.setInstanceFollowRedirects(true);
-
-            if (request.getExtraHeaders() != null && !request.getExtraHeaders().isEmpty()) {
-                for (Map.Entry<String, String> entry : request.getExtraHeaders().entrySet()) {
-                    urlConnection.setRequestProperty(entry.getKey(), entry.getValue());
-                }
-            }
-
-            if (request.getBody() != null && request.getBody().length > 0) {
-                urlConnection.setDoOutput(true);
-                try (BufferedOutputStream out =
-                        new BufferedOutputStream(urlConnection.getOutputStream())) {
-                    out.write(request.getBody());
-                }
-            }
-
-            int responseCode = urlConnection.getResponseCode();
-            if (HTTP_OK_STATUS.contains(responseCode)) {
-                return new FederatedComputeHttpResponse.Builder()
-                        .setPayload(
-                                getByteArray(
-                                        urlConnection.getInputStream(),
-                                        urlConnection.getContentLengthLong()))
-                        .setHeaders(urlConnection.getHeaderFields())
-                        .setStatusCode(responseCode)
-                        .build();
-            } else {
-                return new FederatedComputeHttpResponse.Builder()
-                        .setPayload(
-                                getByteArray(
-                                        urlConnection.getErrorStream(),
-                                        urlConnection.getContentLengthLong()))
-                        .setHeaders(urlConnection.getHeaderFields())
-                        .setStatusCode(responseCode)
-                        .build();
-            }
-        } catch (IOException e) {
-            LogUtil.e(TAG, e, "Failed to get registration response");
-            throw new IOException("Failed to get registration response", e);
-        } finally {
-            if (urlConnection != null) {
-                urlConnection.disconnect();
-            }
-        }
-    }
-
-    private byte[] getByteArray(@Nullable InputStream in, long contentLength) throws IOException {
-        if (contentLength == 0) {
-            return HttpClientUtil.EMPTY_BODY;
-        }
-        try {
-            // TODO(b/297952090): evaluate the large file download.
-            byte[] buffer = new byte[HttpClientUtil.DEFAULT_BUFFER_SIZE];
-            ByteArrayOutputStream out = new ByteArrayOutputStream();
-            int bytesRead;
-            while ((bytesRead = in.read(buffer)) != -1) {
-                out.write(buffer, 0, bytesRead);
-            }
-            return out.toByteArray();
-        } finally {
-            in.close();
-        }
-    }
-}
diff --git a/federatedcompute/src/com/android/federatedcompute/services/http/HttpClientUtil.java b/federatedcompute/src/com/android/federatedcompute/services/http/HttpClientUtil.java
index 7847118..b1925f1 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/http/HttpClientUtil.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/http/HttpClientUtil.java
@@ -16,21 +16,10 @@
 
 package com.android.federatedcompute.services.http;
 
-import com.android.federatedcompute.internal.util.LogUtil;
-
 import com.google.common.collect.ImmutableSet;
-import com.google.protobuf.ByteString;
 
 import org.json.JSONObject;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.List;
-import java.util.Map;
-import java.util.zip.GZIPInputStream;
-import java.util.zip.GZIPOutputStream;
-
 /** Utility class containing http related variable e.g. headers, method. */
 public final class HttpClientUtil {
     private static final String TAG = HttpClientUtil.class.getSimpleName();
@@ -64,13 +53,6 @@
     public static final int DEFAULT_BUFFER_SIZE = 1024;
     public static final byte[] EMPTY_BODY = new byte[0];
 
-    /** The supported http methods. */
-    public enum HttpMethod {
-        GET,
-        POST,
-        PUT,
-    }
-
     public static final class FederatedComputePayloadDataContract {
         public static final String KEY_ID = "keyId";
 
@@ -81,81 +63,5 @@
         public static final byte[] ASSOCIATED_DATA = new JSONObject().toString().getBytes();
     }
 
-    /** Compresses the input data using Gzip. */
-    public static byte[] compressWithGzip(byte[] uncompressedData) {
-        try (ByteString.Output outputStream = ByteString.newOutput(uncompressedData.length);
-                GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream)) {
-            gzipOutputStream.write(uncompressedData);
-            gzipOutputStream.finish();
-            return outputStream.toByteString().toByteArray();
-        } catch (IOException e) {
-            LogUtil.e(TAG, "Failed to compress using Gzip");
-            throw new IllegalStateException("Failed to compress using Gzip", e);
-        }
-    }
-
-    /** Uncompresses the input data using Gzip. */
-    public static byte[] uncompressWithGzip(byte[] data) {
-        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
-                GZIPInputStream gzip = new GZIPInputStream(inputStream);
-                ByteArrayOutputStream result = new ByteArrayOutputStream()) {
-            int length;
-            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
-            while ((length = gzip.read(buffer, 0, DEFAULT_BUFFER_SIZE)) > 0) {
-                result.write(buffer, 0, length);
-            }
-            return result.toByteArray();
-        } catch (Exception e) {
-            LogUtil.e(TAG, e, "Failed to decompress the data.");
-            throw new IllegalStateException("Failed to unscompress using Gzip", e);
-        }
-    }
-
-    /** Calculates total bytes are sent via network based on provided http request. */
-    public static long getTotalSentBytes(FederatedComputeHttpRequest request) {
-        long totalBytes = 0;
-        totalBytes +=
-                request.getHttpMethod().name().length()
-                        + " ".length()
-                        + request.getUri().length()
-                        + " HTTP/1.1\r\n".length();
-        for (String key : request.getExtraHeaders().keySet()) {
-            totalBytes +=
-                    key.length()
-                            + ": ".length()
-                            + request.getExtraHeaders().get(key).length()
-                            + "\r\n".length();
-        }
-        if (request.getExtraHeaders().containsKey(CONTENT_LENGTH_HDR)) {
-            totalBytes += Long.parseLong(request.getExtraHeaders().get(CONTENT_LENGTH_HDR));
-        }
-        return totalBytes;
-    }
-
-    /** Calculates total bytes are received via network based on provided http response. */
-    public static long getTotalReceivedBytes(FederatedComputeHttpResponse response) {
-        long totalBytes = 0;
-        boolean foundContentLengthHdr = false;
-        for (Map.Entry<String, List<String>> header : response.getHeaders().entrySet()) {
-            if (header.getKey() == null) {
-                continue;
-            }
-            for (String headerValue : header.getValue()) {
-                totalBytes += header.getKey().length() + ": ".length();
-                totalBytes += headerValue == null ? 0 : headerValue.length();
-            }
-            // Uses Content-Length header to estimate total received bytes which is the most
-            // accurate.
-            if (header.getKey().equals(CONTENT_LENGTH_HDR)) {
-                totalBytes += Long.parseLong(header.getValue().get(0));
-                foundContentLengthHdr = true;
-            }
-        }
-        if (!foundContentLengthHdr && response.getPayload() != null) {
-            totalBytes += response.getPayload().length;
-        }
-        return totalBytes;
-    }
-
     private HttpClientUtil() {}
 }
diff --git a/federatedcompute/src/com/android/federatedcompute/services/http/HttpFederatedProtocol.java b/federatedcompute/src/com/android/federatedcompute/services/http/HttpFederatedProtocol.java
index 3dd2d25..fc7ce1f 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/http/HttpFederatedProtocol.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/http/HttpFederatedProtocol.java
@@ -19,10 +19,8 @@
 import static com.android.federatedcompute.services.common.Constants.TRACE_HTTP_ISSUE_CHECKIN;
 import static com.android.federatedcompute.services.common.Constants.TRACE_HTTP_REPORT_RESULT;
 import static com.android.federatedcompute.services.common.FederatedComputeExecutors.getBackgroundExecutor;
+import static com.android.federatedcompute.services.common.FederatedComputeExecutors.getBlockingExecutor;
 import static com.android.federatedcompute.services.common.FederatedComputeExecutors.getLightweightExecutor;
-import static com.android.federatedcompute.services.common.FileUtils.createTempFile;
-import static com.android.federatedcompute.services.common.FileUtils.readFileAsByteArray;
-import static com.android.federatedcompute.services.common.FileUtils.writeToFile;
 import static com.android.federatedcompute.services.common.TrainingEventLogger.getTaskIdForLogging;
 import static com.android.federatedcompute.services.http.HttpClientUtil.ACCEPT_ENCODING_HDR;
 import static com.android.federatedcompute.services.http.HttpClientUtil.FCP_OWNER_ID_DIGEST;
@@ -31,10 +29,13 @@
 import static com.android.federatedcompute.services.http.HttpClientUtil.HTTP_OK_STATUS;
 import static com.android.federatedcompute.services.http.HttpClientUtil.HTTP_UNAUTHORIZED_STATUS;
 import static com.android.federatedcompute.services.http.HttpClientUtil.ODP_IDEMPOTENCY_KEY;
-import static com.android.federatedcompute.services.http.HttpClientUtil.compressWithGzip;
-import static com.android.federatedcompute.services.http.HttpClientUtil.getTotalReceivedBytes;
-import static com.android.federatedcompute.services.http.HttpClientUtil.getTotalSentBytes;
-import static com.android.federatedcompute.services.http.HttpClientUtil.uncompressWithGzip;
+import static com.android.odp.module.common.FileUtils.createTempFile;
+import static com.android.odp.module.common.FileUtils.readFileAsByteArray;
+import static com.android.odp.module.common.FileUtils.writeToFile;
+import static com.android.odp.module.common.HttpClientUtils.compressWithGzip;
+import static com.android.odp.module.common.HttpClientUtils.getTotalReceivedBytes;
+import static com.android.odp.module.common.HttpClientUtils.getTotalSentBytes;
+import static com.android.odp.module.common.HttpClientUtils.uncompressWithGzip;
 
 import android.os.Trace;
 import android.util.Base64;
@@ -46,9 +47,12 @@
 import com.android.federatedcompute.services.data.FederatedComputeEncryptionKey;
 import com.android.federatedcompute.services.encryption.Encrypter;
 import com.android.federatedcompute.services.http.HttpClientUtil.FederatedComputePayloadDataContract;
-import com.android.federatedcompute.services.http.HttpClientUtil.HttpMethod;
 import com.android.federatedcompute.services.security.AuthorizationContext;
 import com.android.federatedcompute.services.training.util.ComputationResult;
+import com.android.odp.module.common.HttpClient;
+import com.android.odp.module.common.HttpClientUtils;
+import com.android.odp.module.common.OdpHttpRequest;
+import com.android.odp.module.common.OdpHttpResponse;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Preconditions;
@@ -59,23 +63,25 @@
 import com.google.internal.federatedcompute.v1.ClientVersion;
 import com.google.internal.federatedcompute.v1.RejectionInfo;
 import com.google.internal.federatedcompute.v1.Resource;
-import com.google.internal.federatedcompute.v1.ResourceCapabilities;
 import com.google.internal.federatedcompute.v1.ResourceCompressionFormat;
+import com.google.internal.federatedcompute.v1.UploadInstruction;
 import com.google.ondevicepersonalization.federatedcompute.proto.CreateTaskAssignmentRequest;
 import com.google.ondevicepersonalization.federatedcompute.proto.CreateTaskAssignmentResponse;
 import com.google.ondevicepersonalization.federatedcompute.proto.ReportResultRequest;
 import com.google.ondevicepersonalization.federatedcompute.proto.ReportResultRequest.Result;
 import com.google.ondevicepersonalization.federatedcompute.proto.ReportResultResponse;
 import com.google.ondevicepersonalization.federatedcompute.proto.TaskAssignment;
-import com.google.ondevicepersonalization.federatedcompute.proto.UploadInstruction;
 import com.google.protobuf.InvalidProtocolBufferException;
 
 import org.json.JSONObject;
 
+import java.io.BufferedInputStream;
+import java.io.FileInputStream;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.Callable;
+import java.util.zip.GZIPInputStream;
 
 /** Implements a single session of HTTP-based federated compute protocol. */
 public final class HttpFederatedProtocol {
@@ -117,7 +123,8 @@
                 entryUri,
                 clientVersion,
                 populationName,
-                new HttpClient(),
+                new HttpClient(
+                        FlagsFactory.getFlags().getHttpRequestRetryLimit(), getBlockingExecutor()),
                 encrypter,
                 trainingEventLogger);
     }
@@ -138,14 +145,14 @@
                         getLightweightExecutor());
     }
 
-    /** Donwloads model checkpoint and federated compute plan from remote server. */
+    /** Downloads model checkpoint and federated compute plan from remote server. */
     public ListenableFuture<CheckinResult> downloadTaskAssignment(TaskAssignment taskAssignment) {
         NetworkStats networkStats = new NetworkStats();
         networkStats.recordStartTimeNow();
-        ListenableFuture<FederatedComputeHttpResponse> planDataResponseFuture =
+        ListenableFuture<OdpHttpResponse> planDataResponseFuture =
                 fetchTaskResource(taskAssignment.getPlan(), networkStats);
-        ListenableFuture<FederatedComputeHttpResponse> checkpointDataResponseFuture =
-                fetchTaskResource(taskAssignment.getInitCheckpoint(), networkStats);
+        ListenableFuture<OdpHttpResponse> checkpointDataResponseFuture =
+                fetchTaskResource(taskAssignment.getInitCheckpoint(), networkStats, true);
         return Futures.whenAllSucceed(planDataResponseFuture, checkpointDataResponseFuture)
                 .call(
                         new Callable<CheckinResult>() {
@@ -161,7 +168,7 @@
                         getBackgroundExecutor());
     }
 
-    /** Helper functions to reporting result and upload result. */
+    /** Helper functions to report and upload result. */
     public FluentFuture<RejectionInfo> reportResult(
             ComputationResult computationResult,
             FederatedComputeEncryptionKey encryptionKey,
@@ -194,7 +201,6 @@
                                     return Futures.immediateFuture(
                                             reportResultResponse.getRejectionInfo());
                                 }
-                                // TODO (b/328789639): add a event to track ReportResult success.
                                 NetworkStats uploadStats = new NetworkStats();
                                 return FluentFuture.from(
                                                 processReportResultResponseAndUploadResult(
@@ -234,9 +240,7 @@
     }
 
     private CreateTaskAssignmentResponse processCreateTaskAssignmentResponse(
-            AuthorizationContext authContext,
-            FederatedComputeHttpResponse response,
-            NetworkStats networkStats) {
+            AuthorizationContext authContext, OdpHttpResponse response, NetworkStats networkStats) {
         networkStats.recordEndTimeNow();
         if (authContext.isFirstAuthTry()) {
             validateHttpResponseAllowAuthStatus("Start task assignment", response);
@@ -257,7 +261,8 @@
             throw new IllegalStateException("Could not parse StartTaskAssignmentResponse proto", e);
         }
         if (taskAssignmentResponse.hasRejectionInfo()) {
-            mTrainingEventLogger.logCheckinRejected(networkStats);
+            mTrainingEventLogger.logCheckinRejected(
+                    taskAssignmentResponse.getRejectionInfo(), networkStats);
             return taskAssignmentResponse;
         }
         TaskAssignment taskAssignment = getTaskAssignment(taskAssignmentResponse);
@@ -268,18 +273,14 @@
         return taskAssignmentResponse;
     }
 
-    private ListenableFuture<FederatedComputeHttpResponse> createTaskAssignment(
+    private ListenableFuture<OdpHttpResponse> createTaskAssignment(
             AuthorizationContext authContext, NetworkStats networkStats) {
         CreateTaskAssignmentRequest request =
                 CreateTaskAssignmentRequest.newBuilder()
                         .setClientVersion(
                                 ClientVersion.newBuilder()
                                         .setVersionCode(String.valueOf(mClientVersion)))
-                        .setResourceCapabilities(
-                                ResourceCapabilities.newBuilder()
-                                        .addSupportedCompressionFormats(
-                                                ResourceCompressionFormat
-                                                        .RESOURCE_COMPRESSION_FORMAT_GZIP))
+                        .setResourceCapabilities(HttpClientUtils.getResourceCapabilities())
                         .build();
 
         String taskAssignmentUriSuffix =
@@ -291,10 +292,10 @@
         headers.put(ODP_IDEMPOTENCY_KEY, System.currentTimeMillis() + " - " + UUID.randomUUID());
         headers.put(
                 FCP_OWNER_ID_DIGEST, authContext.getOwnerId() + "-" + authContext.getOwnerCert());
-        FederatedComputeHttpRequest httpRequest =
+        OdpHttpRequest httpRequest =
                 mTaskAssignmentRequestCreator.createProtoRequest(
                         taskAssignmentUriSuffix,
-                        HttpMethod.POST,
+                        HttpClientUtils.HttpMethod.POST,
                         headers,
                         request.toByteArray(),
                         /* isProtobufEncoded= */ true);
@@ -339,15 +340,14 @@
     }
 
     private CheckinResult getCheckinResult(
-            ListenableFuture<FederatedComputeHttpResponse> planDataResponseFuture,
-            ListenableFuture<FederatedComputeHttpResponse> checkpointDataResponseFuture,
+            ListenableFuture<OdpHttpResponse> planDataResponseFuture,
+            ListenableFuture<OdpHttpResponse> checkpointDataResponseFuture,
             TaskAssignment taskAssignment,
             NetworkStats networkStats)
             throws Exception {
         networkStats.recordEndTimeNow();
-        FederatedComputeHttpResponse planDataResponse = Futures.getDone(planDataResponseFuture);
-        FederatedComputeHttpResponse checkpointDataResponse =
-                Futures.getDone(checkpointDataResponseFuture);
+        OdpHttpResponse planDataResponse = Futures.getDone(planDataResponseFuture);
+        OdpHttpResponse checkpointDataResponse = Futures.getDone(checkpointDataResponseFuture);
         validateHttpResponseStatus("Fetch plan", planDataResponse);
         validateHttpResponseStatus("Fetch checkpoint", checkpointDataResponse);
         networkStats.addBytesDownloaded(getTotalReceivedBytes(planDataResponse));
@@ -367,22 +367,42 @@
             mTrainingEventLogger.logCheckinInvalidPayload(networkStats);
             throw new IllegalStateException("Could not parse ClientOnlyPlan proto", e);
         }
-        mTrainingEventLogger.logCheckinFinished(networkStats);
-
-        // Process download checkpoint resource.
-        String inputCheckpointFile = createTempFile("input", ".ckp");
-        byte[] checkpointData = checkpointDataResponse.getPayload();
-        if (taskAssignment.getInitCheckpoint().getCompressionFormat()
-                        == ResourceCompressionFormat.RESOURCE_COMPRESSION_FORMAT_GZIP
-                || checkpointDataResponse.isResponseCompressed()) {
-            checkpointData = uncompressWithGzip(checkpointData);
+        if (checkpointDataResponse.getPayloadFileName() == null) {
+            Trace.endAsyncSection(TRACE_HTTP_ISSUE_CHECKIN, 0);
+            mTrainingEventLogger.logCheckinInvalidPayload(networkStats);
+            return null;
         }
-        writeToFile(inputCheckpointFile, checkpointData);
+
+        // Process downloaded checkpoint resource.
+        String payloadFileName = checkpointDataResponse.getPayloadFileName();
+        long checkpointFileSize = checkpointDataResponse.getDownloadedPayloadSize();
+        if (checkpointDataResponse.isResponseCompressed()) {
+            String checkpointFile = createTempFile("input", ".ckp");
+            checkpointFileSize =
+                    writeToFile(
+                            checkpointFile,
+                            new GZIPInputStream(
+                                    new BufferedInputStream(new FileInputStream(payloadFileName))));
+            LogUtil.d(TAG, "Uncompressed checkpoint data file size: %d", checkpointFileSize);
+            payloadFileName = checkpointFile;
+        }
+        if (checkpointFileSize > FlagsFactory.getFlags().getFcpCheckpointFileSizeLimit()) {
+            LogUtil.e(
+                    TAG,
+                    "CheckPoint data is too large: %d, which more than a limit: %d",
+                    checkpointFileSize,
+                    FlagsFactory.getFlags().getFcpCheckpointFileSizeLimit());
+            Trace.endAsyncSection(TRACE_HTTP_ISSUE_CHECKIN, 0);
+            mTrainingEventLogger.logCheckinInvalidPayload(networkStats);
+            return null;
+        }
+
+        mTrainingEventLogger.logCheckinFinished(networkStats);
         Trace.endAsyncSection(TRACE_HTTP_ISSUE_CHECKIN, 0);
-        return new CheckinResult(inputCheckpointFile, clientOnlyPlan, taskAssignment);
+        return new CheckinResult(payloadFileName, clientOnlyPlan, taskAssignment);
     }
 
-    private ListenableFuture<FederatedComputeHttpResponse> performReportResult(
+    private ListenableFuture<OdpHttpResponse> performReportResult(
             ComputationResult computationResult,
             AuthorizationContext authContext,
             NetworkStats networkStats) {
@@ -396,11 +416,7 @@
         ReportResultRequest startDataUploadRequest =
                 ReportResultRequest.newBuilder()
                         .setResult(result)
-                        .setResourceCapabilities(
-                                ResourceCapabilities.newBuilder()
-                                        .addSupportedCompressionFormats(
-                                                ResourceCompressionFormat
-                                                        .RESOURCE_COMPRESSION_FORMAT_GZIP))
+                        .setResourceCapabilities(HttpClientUtils.getResourceCapabilities())
                         .build();
         String startDataUploadUri =
                 String.format(
@@ -416,10 +432,10 @@
                 mAssignmentId,
                 result.toString());
         Map<String, String> headers = authContext.generateAuthHeaders();
-        FederatedComputeHttpRequest httpRequest =
+        OdpHttpRequest httpRequest =
                 mTaskAssignmentRequestCreator.createProtoRequest(
                         startDataUploadUri,
-                        HttpMethod.PUT,
+                        HttpClientUtils.HttpMethod.PUT,
                         headers,
                         startDataUploadRequest.toByteArray(),
                         /* isProtobufEncoded= */ true);
@@ -427,12 +443,11 @@
         return mHttpClient.performRequestAsyncWithRetry(httpRequest);
     }
 
-    private ListenableFuture<FederatedComputeHttpResponse>
-            processReportResultResponseAndUploadResult(
-                    ReportResultResponse reportResultResponse,
-                    ComputationResult computationResult,
-                    FederatedComputeEncryptionKey encryptionKey,
-                    NetworkStats networkStats) {
+    private ListenableFuture<OdpHttpResponse> processReportResultResponseAndUploadResult(
+            ReportResultResponse reportResultResponse,
+            ComputationResult computationResult,
+            FederatedComputeEncryptionKey encryptionKey,
+            NetworkStats networkStats) {
         try {
             Preconditions.checkArgument(
                     !computationResult.getOutputCheckpointFile().isEmpty(),
@@ -447,15 +462,10 @@
             // Apply a top-level compression to the payload.
             if (uploadInstruction.getCompressionFormat()
                     == ResourceCompressionFormat.RESOURCE_COMPRESSION_FORMAT_GZIP) {
-                outputBytes = compressWithGzip(outputBytes);
+                outputBytes = HttpClientUtils.compressWithGzip(outputBytes);
             }
-            HashMap<String, String> requestHeader = new HashMap<>();
-            uploadInstruction
-                    .getExtraRequestHeadersMap()
-                    .forEach(
-                            (key, value) -> {
-                                requestHeader.put(key, value);
-                            });
+            HashMap<String, String> requestHeader =
+                    new HashMap<>(uploadInstruction.getExtraRequestHeadersMap());
             LogUtil.d(
                     TAG,
                     "Start upload training result: population name %s, task name %s,"
@@ -463,10 +473,10 @@
                     mPopulationName,
                     mTaskId,
                     mAssignmentId);
-            FederatedComputeHttpRequest httpUploadRequest =
-                    FederatedComputeHttpRequest.create(
+            OdpHttpRequest httpUploadRequest =
+                    OdpHttpRequest.create(
                             uploadInstruction.getUploadLocation(),
-                            HttpMethod.PUT,
+                            HttpClientUtils.HttpMethod.PUT,
                             requestHeader,
                             outputBytes);
             networkStats.recordStartTimeNow();
@@ -506,8 +516,7 @@
         return body.toString().getBytes();
     }
 
-    private void validateHttpResponseStatus(
-            String stage, FederatedComputeHttpResponse httpResponse) {
+    private static void validateHttpResponseStatus(String stage, OdpHttpResponse httpResponse) {
         if (!HTTP_OK_STATUS.contains(httpResponse.getStatusCode())) {
             throw new IllegalStateException(stage + " failed: " + httpResponse.getStatusCode());
         }
@@ -515,8 +524,8 @@
         LogUtil.i(TAG, stage + " success.");
     }
 
-    private void validateHttpResponseAllowAuthStatus(
-            String stage, FederatedComputeHttpResponse httpResponse) {
+    private static void validateHttpResponseAllowAuthStatus(
+            String stage, OdpHttpResponse httpResponse) {
         if (!HTTP_OK_OR_UNAUTHENTICATED_STATUS.contains(httpResponse.getStatusCode())) {
             throw new IllegalStateException(stage + " failed: " + httpResponse.getStatusCode());
         }
@@ -524,28 +533,37 @@
         LogUtil.i(TAG, stage + " success.");
     }
 
-    private ListenableFuture<FederatedComputeHttpResponse> fetchTaskResource(
+    private ListenableFuture<OdpHttpResponse> fetchTaskResource(
             Resource resource, NetworkStats networkStats) {
+        return fetchTaskResource(resource, networkStats, false);
+    }
+
+    private ListenableFuture<OdpHttpResponse> fetchTaskResource(
+            Resource resource, NetworkStats networkStats, boolean payloadIntoFileEnabled) {
         switch (resource.getResourceCase()) {
             case URI:
                 Preconditions.checkArgument(
                         !resource.getUri().isEmpty(), "Resource.uri must be non-empty when set");
                 HashMap<String, String> headerList = new HashMap<>();
-                if (resource.getCompressionFormat()
-                        == ResourceCompressionFormat.RESOURCE_COMPRESSION_FORMAT_GZIP) {
+                boolean gZipCompressionEnabled = resource.getCompressionFormat()
+                        == ResourceCompressionFormat.RESOURCE_COMPRESSION_FORMAT_GZIP;
+                if (gZipCompressionEnabled) {
                     // Set this header to disable decompressive transcoding when download from
                     // Google Cloud Storage.
                     // https://cloud.google.com/storage/docs/transcoding#decompressive_transcoding
                     headerList.put(ACCEPT_ENCODING_HDR, GZIP_ENCODING_HDR);
                 }
                 LogUtil.d(TAG, "start fetch task resources");
-                FederatedComputeHttpRequest httpRequest =
-                        FederatedComputeHttpRequest.create(
+                OdpHttpRequest httpRequest =
+                        OdpHttpRequest.create(
                                 resource.getUri(),
-                                HttpMethod.GET,
+                                HttpClientUtils.HttpMethod.GET,
                                 headerList,
                                 HttpClientUtil.EMPTY_BODY);
                 networkStats.addBytesUploaded(getTotalSentBytes(httpRequest));
+                if (payloadIntoFileEnabled) {
+                    return mHttpClient.performRequestIntoFileAsyncWithRetry(httpRequest);
+                }
                 return mHttpClient.performRequestAsyncWithRetry(httpRequest);
             case INLINE_RESOURCE:
                 return Futures.immediateFailedFuture(
diff --git a/federatedcompute/src/com/android/federatedcompute/services/http/ProtocolRequestCreator.java b/federatedcompute/src/com/android/federatedcompute/services/http/ProtocolRequestCreator.java
index 12062c7..a01b71d 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/http/ProtocolRequestCreator.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/http/ProtocolRequestCreator.java
@@ -16,10 +16,9 @@
 
 package com.android.federatedcompute.services.http;
 
-import static com.android.federatedcompute.services.http.HttpClientUtil.CONTENT_TYPE_HDR;
-import static com.android.federatedcompute.services.http.HttpClientUtil.PROTOBUF_CONTENT_TYPE;
-
-import com.android.federatedcompute.services.http.HttpClientUtil.HttpMethod;
+import com.android.odp.module.common.HttpClientUtils;
+import com.android.odp.module.common.HttpClientUtils.HttpMethod;
+import com.android.odp.module.common.OdpHttpRequest;
 
 import com.google.internal.federatedcompute.v1.ForwardingInfo;
 
@@ -27,14 +26,14 @@
 import java.util.Map;
 
 /**
- * A helper class to create FederatedComputeHttpRequest with base uri, request headers and
- * compression setting.
+ * A helper class to create {@link OdpHttpRequest} with base uri, request headers and compression
+ * setting for federated compute.
  */
-public final class ProtocolRequestCreator {
+final class ProtocolRequestCreator {
     private final String mRequestBaseUri;
     private final HashMap<String, String> mHeaderList;
 
-    public ProtocolRequestCreator(String requestBaseUri, HashMap<String, String> headerList) {
+    ProtocolRequestCreator(String requestBaseUri, HashMap<String, String> headerList) {
         this.mRequestBaseUri = requestBaseUri;
         this.mHeaderList = headerList;
     }
@@ -43,7 +42,7 @@
      * Creates a {@link ProtocolRequestCreator} based on forwarding info. Validates and extracts the
      * base URI for the subsequent requests.
      */
-    public static ProtocolRequestCreator create(ForwardingInfo forwardingInfo) {
+    static ProtocolRequestCreator create(ForwardingInfo forwardingInfo) {
         if (forwardingInfo.getTargetUriPrefix().isEmpty()) {
             throw new IllegalArgumentException("Missing `ForwardingInfo.target_uri_prefix`");
         }
@@ -52,18 +51,15 @@
         return new ProtocolRequestCreator(forwardingInfo.getTargetUriPrefix(), extraHeaders);
     }
 
-    /** Creates a {@link FederatedComputeHttpRequest} with base uri and compression setting. */
-    public FederatedComputeHttpRequest createProtoRequest(
+    /** Creates a {@link OdpHttpRequest} with base uri and compression setting. */
+    OdpHttpRequest createProtoRequest(
             String uri, HttpMethod httpMethod, byte[] requestBody, boolean isProtobufEncoded) {
         HashMap<String, String> extraHeaders = new HashMap<>();
         return createProtoRequest(uri, httpMethod, extraHeaders, requestBody, isProtobufEncoded);
     }
 
-    /**
-     * Creates a {@link FederatedComputeHttpRequest} with base uri, request headers and compression
-     * setting.
-     */
-    public FederatedComputeHttpRequest createProtoRequest(
+    /** Creates a {@link OdpHttpRequest} with base uri, request headers and compression setting. */
+    OdpHttpRequest createProtoRequest(
             String uri,
             HttpMethod httpMethod,
             Map<String, String> extraHeaders,
@@ -74,24 +70,14 @@
         requestHeader.putAll(extraHeaders);
 
         if (isProtobufEncoded && requestBody.length > 0) {
-            requestHeader.put(CONTENT_TYPE_HDR, PROTOBUF_CONTENT_TYPE);
+            requestHeader.put(
+                    HttpClientUtils.CONTENT_TYPE_HDR, HttpClientUtils.PROTOBUF_CONTENT_TYPE);
         }
-        return FederatedComputeHttpRequest.create(
-                joinBaseUriWithSuffix(mRequestBaseUri, uri),
+        return OdpHttpRequest.create(
+                HttpClientUtils.joinBaseUriWithSuffix(mRequestBaseUri, uri),
                 httpMethod,
                 requestHeader,
                 requestBody);
     }
 
-    private String joinBaseUriWithSuffix(String baseUri, String suffix) {
-        if (suffix.isEmpty() || !suffix.startsWith("/")) {
-            throw new IllegalArgumentException("uri_suffix be empty or must have a leading '/'");
-        }
-
-        if (baseUri.endsWith("/")) {
-            baseUri = baseUri.substring(0, baseUri.length() - 1);
-        }
-        suffix = suffix.substring(1);
-        return String.join("/", baseUri, suffix);
-    }
 }
diff --git a/federatedcompute/src/com/android/federatedcompute/services/scheduling/JobSchedulerHelper.java b/federatedcompute/src/com/android/federatedcompute/services/scheduling/JobSchedulerHelper.java
index 5c89557..a49966d 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/scheduling/JobSchedulerHelper.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/scheduling/JobSchedulerHelper.java
@@ -93,7 +93,10 @@
         jobInfo.setRequiresDeviceIdle(task.getTrainingConstraints().requiresSchedulerIdle())
                 .setRequiresBatteryNotLow(
                         task.getTrainingConstraints().requiresSchedulerBatteryNotLow())
-                .setMinimumLatency(task.earliestNextRunTime() - nowMillis)
+                .setMinimumLatency(
+                        (task.earliestNextRunTime() - nowMillis) > 0
+                                ? (task.earliestNextRunTime() - nowMillis)
+                                : 0)
                 .setPersisted(true);
 
         jobInfo.setRequiredNetworkType(
diff --git a/federatedcompute/src/com/android/federatedcompute/services/security/AuthorizationContext.java b/federatedcompute/src/com/android/federatedcompute/services/security/AuthorizationContext.java
index 6a346fd..c2e072d 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/security/AuthorizationContext.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/security/AuthorizationContext.java
@@ -172,8 +172,9 @@
                 AuthTokenCallbackResult callbackResult =
                         authTokenBlockingQueue.poll(
                                 BLOCKING_QUEUE_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
-                if (!callbackResult.isEmpty()) {
-                    LogUtil.e(TAG, "checking blocking queue");
+                if (callbackResult.isEmpty()) {
+                    LogUtil.e(TAG, "Timed out waiting for  blocking queue.");
+                } else {
                     headers.put(
                             ODP_AUTHORIZATION_KEY,
                             callbackResult.getAuthToken().getAuthorizationToken());
@@ -188,7 +189,7 @@
         return headers;
     }
 
-    private FutureCallback<AuthTokenCallbackResult> createCallbackForBlockingQueue(
+    private static FutureCallback<AuthTokenCallbackResult> createCallbackForBlockingQueue(
             BlockingQueue<AuthTokenCallbackResult> authorizationTokenBlockingQueue) {
         return new FutureCallback<>() {
             @Override
@@ -203,7 +204,7 @@
         };
     }
 
-    private AuthTokenCallbackResult convertODPAuthToken(ODPAuthorizationToken authToken) {
+    private static AuthTokenCallbackResult convertODPAuthToken(ODPAuthorizationToken authToken) {
         return new AuthTokenCallbackResult(authToken, authToken == null);
     }
 
diff --git a/federatedcompute/src/com/android/federatedcompute/services/sharedlibrary/spe/FederatedComputeJobServiceFactory.java b/federatedcompute/src/com/android/federatedcompute/services/sharedlibrary/spe/FederatedComputeJobServiceFactory.java
index 2e1c5c9..29d0ef1 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/sharedlibrary/spe/FederatedComputeJobServiceFactory.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/sharedlibrary/spe/FederatedComputeJobServiceFactory.java
@@ -22,11 +22,11 @@
 import android.content.Context;
 
 import com.android.adservices.shared.proto.ModuleJobPolicy;
-import com.android.adservices.shared.proto.ProtoParser;
 import com.android.adservices.shared.spe.framework.JobServiceFactory;
 import com.android.adservices.shared.spe.framework.JobWorker;
 import com.android.adservices.shared.spe.logging.JobSchedulingLogger;
 import com.android.adservices.shared.spe.logging.JobServiceLogger;
+import com.android.adservices.shared.util.ProtoParser;
 import com.android.federatedcompute.internal.util.LogUtil;
 import com.android.federatedcompute.services.common.FederatedComputeExecutors;
 import com.android.federatedcompute.services.common.Flags;
@@ -87,6 +87,7 @@
                 ModuleJobPolicy policy =
                         ProtoParser.parseBase64EncodedStringToProto(
                                 ModuleJobPolicy.parser(),
+                                ClientErrorLogger.getInstance(),
                                 PROTO_PROPERTY_FOR_LOGCAT,
                                 flags.getFcpModuleJobPolicy());
                 sSingleton =
diff --git a/federatedcompute/src/com/android/federatedcompute/services/training/EligibilityDecider.java b/federatedcompute/src/com/android/federatedcompute/services/training/EligibilityDecider.java
index 302d47d..8a66c3d 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/training/EligibilityDecider.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/training/EligibilityDecider.java
@@ -20,6 +20,9 @@
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_ELIGIBILITY_EVAL_COMPUTATION_ELIGIBLE;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_ELIGIBILITY_EVAL_COMPUTATION_ERROR_EXAMPLE_ITERATOR;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_ELIGIBILITY_EVAL_COMPUTATION_STARTED;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_ERROR;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_START;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_SUCCESS;
 
 import android.content.Context;
 import android.federatedcompute.aidl.IExampleStoreIterator;
@@ -157,12 +160,16 @@
             ExampleSelector exampleSelector) {
         try {
             long callStartTimeNanos = SystemClock.elapsedRealtimeNanos();
+            logger.logEventKind(
+                    FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_START);
             IExampleStoreService exampleStoreService =
                     mExampleStoreServiceProvider.getExampleStoreService(
                             task.appPackageName(), context);
             if (exampleStoreService == null) {
                 logger.logEventKind(
                         FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_ELIGIBILITY_EVAL_COMPUTATION_ERROR_EXAMPLE_ITERATOR);
+                logger.logEventKind(
+                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_ERROR);
                 LogUtil.e(
                         TAG,
                         "Failed to compute DataAvailabilityPolicy due to bind ExampleStore"
@@ -172,6 +179,8 @@
                         taskId);
                 return false;
             }
+            logger.logEventKind(
+                    FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_SUCCESS);
             exampleStats.mBindToExampleStoreLatencyNanos.addAndGet(
                     SystemClock.elapsedRealtimeNanos() - callStartTimeNanos);
             callStartTimeNanos = SystemClock.elapsedRealtimeNanos();
@@ -181,7 +190,8 @@
                             task,
                             taskId,
                             dataAvailabilityPolicy.getMinExampleCount(),
-                            exampleSelector);
+                            exampleSelector,
+                            logger);
             if (iterator == null) {
                 LogUtil.d(
                         TAG,
diff --git a/federatedcompute/src/com/android/federatedcompute/services/training/FederatedComputeWorker.java b/federatedcompute/src/com/android/federatedcompute/services/training/FederatedComputeWorker.java
index 1b2a6a1..6c5b0ff 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/training/FederatedComputeWorker.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/training/FederatedComputeWorker.java
@@ -26,11 +26,23 @@
 import static com.android.federatedcompute.services.common.Constants.TRACE_WORKER_START_TRAINING_RUN;
 import static com.android.federatedcompute.services.common.FederatedComputeExecutors.getBackgroundExecutor;
 import static com.android.federatedcompute.services.common.FederatedComputeExecutors.getLightweightExecutor;
-import static com.android.federatedcompute.services.common.FileUtils.createTempFile;
-import static com.android.federatedcompute.services.common.FileUtils.createTempFileDescriptor;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_COMPUTATION_STARTED;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_ELIGIBILITY_EVAL_NOT_CONFIGURED;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_ERROR;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_START;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_SUCCESS;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_ERROR;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_COMPLETE;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_COMPUTATION_FAILED;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_DOWNLOAD_FAILED;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_ENCRYPTION_KEY_FETCH_FAILED;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_NOT_ELIGIBLE;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_REPORT_FAILED;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_WITH_EXCEPTION;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_WITH_REJECTION;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_STARTED;
+import static com.android.odp.module.common.FileUtils.createTempFile;
+import static com.android.odp.module.common.FileUtils.createTempFileDescriptor;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -51,7 +63,6 @@
 import com.android.federatedcompute.internal.util.LogUtil;
 import com.android.federatedcompute.services.common.Constants;
 import com.android.federatedcompute.services.common.ExampleStats;
-import com.android.federatedcompute.services.common.FileUtils;
 import com.android.federatedcompute.services.common.Flags;
 import com.android.federatedcompute.services.common.FlagsFactory;
 import com.android.federatedcompute.services.common.TrainingEventLogger;
@@ -79,6 +90,7 @@
 import com.android.federatedcompute.services.training.util.TrainingConditionsChecker.Condition;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
+import com.android.odp.module.common.FileUtils;
 import com.android.odp.module.common.PackageUtils;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -130,12 +142,12 @@
     private TrainingRun mActiveRun = null;
 
     private HttpFederatedProtocol mHttpFederatedProtocol;
-    private ExampleStoreServiceProvider mExampleStoreServiceProvider;
+    private final ExampleStoreServiceProvider mExampleStoreServiceProvider;
     private AbstractServiceBinder<IIsolatedTrainingService> mIsolatedTrainingServiceBinder;
-    private FederatedComputeEncryptionKeyManager mEncryptionKeyManager;
+    private final FederatedComputeEncryptionKeyManager mEncryptionKeyManager;
 
     @VisibleForTesting
-    public FederatedComputeWorker(
+    FederatedComputeWorker(
             Context context,
             FederatedComputeJobManager jobManager,
             TrainingConditionsChecker trainingConditionsChecker,
@@ -182,6 +194,8 @@
         LogUtil.d(TAG, "startTrainingRun() %d", jobId);
         TrainingEventLogger trainingEventLogger = mInjector.getTrainingEventLogger();
         trainingEventLogger.setClientVersion(PackageUtils.getApexVersion(this.mContext));
+        trainingEventLogger.logEventKind(
+                FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_STARTED);
         return FluentFuture.from(
                         mInjector
                                 .getBgExecutor()
@@ -351,6 +365,10 @@
                             if (taskAssignmentOnUnauthenticated.hasRejectionInfo()) {
                                 // This function is called only when the device received
                                 // 401 (unauthenticated). Only retry rejection is allowed.
+                                LogUtil.d(
+                                        TAG, "job %d was rejected during check in, reason %s",
+                                        run.mTask.jobId(), taskAssignmentOnUnauthenticated
+                                            .getRejectionInfo().getReason());
                                 if (taskAssignmentOnUnauthenticated
                                         .getRejectionInfo()
                                         .hasRetryWindow()) {
@@ -376,6 +394,8 @@
             TrainingRun run,
             CreateTaskAssignmentResponse taskAssignmentResponse,
             boolean enableFailuresTracking) {
+        run.mTrainingEventLogger.logEventKind(
+                FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_WITH_REJECTION);
         performFinishRoutines(
                 run.mCallback,
                 ContributionResult.FAIL,
@@ -422,6 +442,8 @@
                                         .flattenToString(),
                                 run.mTask.ownerIdCertDigest()),
                         run.mTrainingEventLogger);
+                run.mTrainingEventLogger.logEventKind(
+                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_NOT_ELIGIBLE);
                 // Reschedule the job.
                 performFinishRoutines(
                         run.mCallback,
@@ -441,7 +463,24 @@
                         mHttpFederatedProtocol.downloadTaskAssignment(
                                 createTaskAssignmentResponse.getTaskAssignment()))
                 .transformAsync(
-                        checkinResult -> doFederatedComputation(run, checkinResult, eligibleResult),
+                        checkinResult -> {
+                            if (checkinResult == null) {
+                                LogUtil.w(TAG, "Failed to acquire checkin result!");
+                                run.mTrainingEventLogger.logEventKind(
+                                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_DOWNLOAD_FAILED);
+                                // Reschedule the job.
+                                performFinishRoutines(
+                                        run.mCallback,
+                                        ContributionResult.FAIL,
+                                        run.mTask.jobId(),
+                                        run.mTask.populationName(),
+                                        run.mTask.getTrainingIntervalOptions(),
+                                        /* taskRetry= */ null,
+                                        /* enableFailuresTracking= */ true);
+                                return Futures.immediateFuture(null);
+                            }
+                            return doFederatedComputation(run, checkinResult, eligibleResult);
+                        },
                         getBackgroundExecutor());
     }
 
@@ -485,7 +524,9 @@
                         : activeKeys.get(new Random().nextInt(activeKeys.size()));
         if (encryptionKey == null) {
             // no active keys to encrypt the FL/FA computation results, stop the computation run.
-            reportFailureResultToServer(run);
+            run.mTrainingEventLogger.logEventKind(
+                    FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_ENCRYPTION_KEY_FETCH_FAILED);
+            reportFailureResultToServer(run, null);
             return Futures.immediateFailedFuture(
                     new IllegalStateException("No active key available on device."));
         }
@@ -502,7 +543,9 @@
         }
 
         if (iterator == null) {
-            reportFailureResultToServer(run);
+            run.mTrainingEventLogger.logEventKind(
+                    FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_COMPUTATION_FAILED);
+            reportFailureResultToServer(run, FLRunnerResult.ErrorStatus.EXAMPLE_ITERATOR_ERROR);
             return Futures.immediateFailedFuture(
                     new IllegalStateException(
                             String.format(
@@ -562,6 +605,11 @@
                             ComputationResult computationResult =
                                     Futures.getDone(computationResultFuture);
                             RejectionInfo reportToServer = Futures.getDone(reportToServerFuture);
+                            if (computationResult.getFlRunnerResult().getContributionResult()
+                                    != ContributionResult.SUCCESS) {
+                                run.mTrainingEventLogger.logEventKind(
+                                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_COMPUTATION_FAILED);
+                            }
                             // report to Server will hold null in case of success, or rejection info
                             // in case server answered with rejection
                             if (reportToServer != null) {
@@ -581,6 +629,13 @@
                                                 run.mTaskId,
                                                 run.mTask,
                                                 failedReportComputationResult);
+                                if (computationResult.getFlRunnerResult().getContributionResult()
+                                        == ContributionResult.SUCCESS) {
+                                    // do not log failed delivery if, failed computation os already
+                                    // logged.
+                                    run.mTrainingEventLogger.logEventKind(
+                                            FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_REPORT_FAILED);
+                                }
                                 performFinishRoutines(
                                         run.mCallback,
                                         ContributionResult.FAIL,
@@ -613,14 +668,15 @@
                         mInjector.getBgExecutor());
     }
 
-    private void reportFailureResultToServer(TrainingRun run) {
+    private void reportFailureResultToServer(
+            TrainingRun run, @Nullable FLRunnerResult.ErrorStatus failureStatus) {
+        FLRunnerResult.Builder runnerResultBuilder =
+                FLRunnerResult.newBuilder().setContributionResult(ContributionResult.FAIL);
+        if (failureStatus != null) {
+            runnerResultBuilder.setErrorStatus(failureStatus);
+        }
         ComputationResult failedComputationResult =
-                new ComputationResult(
-                        null,
-                        FLRunnerResult.newBuilder()
-                                .setContributionResult(ContributionResult.FAIL)
-                                .build(),
-                        null);
+                new ComputationResult(null, runnerResultBuilder.build(), null);
         try {
             reportFailureResultToServer(
                     failedComputationResult,
@@ -657,13 +713,18 @@
             TrainingRun run, ExampleSelector exampleSelector) {
         try {
             long startTimeNanos = SystemClock.elapsedRealtimeNanos();
+            run.mTrainingEventLogger.logEventKind(
+                    FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_START);
             IExampleStoreService exampleStoreService =
                     mExampleStoreServiceProvider.getExampleStoreService(
                             run.mTask.appPackageName(), mContext);
             if (exampleStoreService == null) {
-                run.mTrainingEventLogger.logComputationExampleIteratorError(new ExampleStats());
+                run.mTrainingEventLogger.logEventKind(
+                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_ERROR);
                 return null;
             }
+            run.mTrainingEventLogger.logEventKind(
+                    FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_SUCCESS);
             run.mExampleStats.mBindToExampleStoreLatencyNanos.addAndGet(
                     SystemClock.elapsedRealtimeNanos() - startTimeNanos);
             run.mExampleStoreService = exampleStoreService;
@@ -671,12 +732,18 @@
 
             IExampleStoreIterator iterator =
                     mExampleStoreServiceProvider.getExampleIterator(
-                            run.mExampleStoreService, run.mTask, run.mTaskId, 0, exampleSelector);
+                            run.mExampleStoreService,
+                            run.mTask,
+                            run.mTaskId,
+                            0,
+                            exampleSelector,
+                            run.mTrainingEventLogger);
             run.mExampleStats.mStartQueryLatencyNanos.addAndGet(
                     SystemClock.elapsedRealtimeNanos() - startTimeNanos);
             return iterator;
         } catch (Exception e) {
-            run.mTrainingEventLogger.logComputationExampleIteratorError(new ExampleStats());
+            run.mTrainingEventLogger.logEventKind(
+                    FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_ERROR);
             LogUtil.e(TAG, "StartQuery failure: " + e.getMessage());
             return null;
         }
@@ -706,6 +773,20 @@
         finish(taskRetry, contributionResult, true);
     }
 
+    /** Log that training run failed with exception. */
+    public void logTrainEventFinishedWithException() {
+        synchronized (mLock) {
+            if (mActiveRun == null) {
+                return;
+            }
+            if (mActiveRun.mTrainingEventLogger == null) {
+                return;
+            }
+            mActiveRun.mTrainingEventLogger.logEventKind(
+                    FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_WITH_EXCEPTION);
+        }
+    }
+
     /**
      * Cancel the current running job, schedule recurrent job, unbind from ExampleStoreService and
      * ResultHandlingService etc.
@@ -1237,6 +1318,8 @@
                                                 .setErrorMessage(throwable.getMessage())
                                                 .build(),
                                         null);
+                        mLogger.logEventKind(
+                                FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_COMPUTATION_FAILED);
                         reportFailureResultToServer(
                                 failedReportComputationResult, authContext, mLogger);
                         completer.setException(throwable);
diff --git a/federatedcompute/src/com/android/federatedcompute/services/training/FederatedJobService.java b/federatedcompute/src/com/android/federatedcompute/services/training/FederatedJobService.java
index 76e28f0..dd6c807 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/training/FederatedJobService.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/training/FederatedJobService.java
@@ -69,6 +69,7 @@
                     public void onFailure(Throwable t) {
                         LogUtil.e(
                                 TAG, t, "Failed to handle computation job: %d", params.getJobId());
+                        worker.logTrainEventFinishedWithException();
                         worker.finish(null, ContributionResult.FAIL, false);
                     }
                 },
diff --git a/federatedcompute/src/com/android/federatedcompute/services/training/IsolatedTrainingServiceImpl.java b/federatedcompute/src/com/android/federatedcompute/services/training/IsolatedTrainingServiceImpl.java
index 87ce290..a493275 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/training/IsolatedTrainingServiceImpl.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/training/IsolatedTrainingServiceImpl.java
@@ -31,12 +31,12 @@
 import com.android.federatedcompute.internal.util.LogUtil;
 import com.android.federatedcompute.services.common.Constants;
 import com.android.federatedcompute.services.common.FederatedComputeExecutors;
-import com.android.federatedcompute.services.common.FileUtils;
 import com.android.federatedcompute.services.data.fbs.TrainingFlags;
 import com.android.federatedcompute.services.examplestore.ExampleConsumptionRecorder;
 import com.android.federatedcompute.services.training.aidl.IIsolatedTrainingService;
 import com.android.federatedcompute.services.training.aidl.ITrainingResultCallback;
 import com.android.federatedcompute.services.training.util.ListenableSupplier;
+import com.android.odp.module.common.FileUtils;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.util.concurrent.FutureCallback;
diff --git a/federatedcompute/src/com/android/federatedcompute/services/training/util/ComputationResult.java b/federatedcompute/src/com/android/federatedcompute/services/training/util/ComputationResult.java
index 9e39e99..652c9e9 100644
--- a/federatedcompute/src/com/android/federatedcompute/services/training/util/ComputationResult.java
+++ b/federatedcompute/src/com/android/federatedcompute/services/training/util/ComputationResult.java
@@ -63,6 +63,15 @@
         if (mFlRunnerResult.getErrorStatus() == FLRunnerResult.ErrorStatus.NOT_ELIGIBLE) {
             return Result.NOT_ELIGIBLE;
         }
+        if (mFlRunnerResult.getErrorStatus() == FLRunnerResult.ErrorStatus.TENSORFLOW_ERROR) {
+            return Result.FAILED_MODEL_COMPUTATION;
+        }
+        if (mFlRunnerResult.getErrorStatus() == FLRunnerResult.ErrorStatus.INVALID_ARGUMENT) {
+            return Result.FAILED_MODEL_COMPUTATION;
+        }
+        if (mFlRunnerResult.getErrorStatus() == FLRunnerResult.ErrorStatus.EXAMPLE_ITERATOR_ERROR) {
+            return Result.FAILED_EXAMPLE_GENERATION;
+        }
         return Result.FAILED;
     }
 }
diff --git a/flags/Android.bp b/flags/Android.bp
index 0181d7c..4d4b9fd 100644
--- a/flags/Android.bp
+++ b/flags/Android.bp
@@ -24,7 +24,7 @@
     aconfig_declarations: "ondevicepersonalization_flags",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
     visibility: [
-        "//packages/modules/OnDevicePersonalization/framework",
+        "//packages/modules/OnDevicePersonalization:__subpackages__",
     ],
     min_sdk_version: "33",
     apex_available: [
diff --git a/flags/ondevicepersonalization_flags.aconfig b/flags/ondevicepersonalization_flags.aconfig
index e808ada..9a00688 100644
--- a/flags/ondevicepersonalization_flags.aconfig
+++ b/flags/ondevicepersonalization_flags.aconfig
@@ -4,15 +4,41 @@
 flag {
     name: "on_device_personalization_apis_enabled"
     is_exported: true
-    namespace: "on_device_personalization"
+    namespace: "ondevicepersonalization_aconfig"
     # TODO(b/320156647): Add bug number and description
     bug: "320156647"
     description: "Enter a description per b/320156647"
+    is_fixed_read_only: true
 }
 
 flag {
     name: "fcp_model_version_enabled"
-    namespace: "on_device_personalization"
+    namespace: "ondevicepersonalization_aconfig"
     bug: "335080565"
     description: "Enable model version support for federated compute"
+    is_fixed_read_only: true
+}
+
+flag {
+    name: "data_class_missing_ctors_and_getters_enabled"
+    namespace: "ondevicepersonalization_aconfig"
+    bug: "353356413"
+    description: "Add missing ctors and getters to certain data classes"
+    is_fixed_read_only: true
+}
+
+flag {
+    name: "execute_in_isolated_service_api_enabled"
+    namespace: "ondevicepersonalization_aconfig"
+    bug: "336801193"
+    description: "Enable executeInIsolatedService API"
+    is_fixed_read_only: true
+}
+
+flag {
+    name: "fcp_schedule_with_outcome_receiver_enabled"
+    namespace: "ondevicepersonalization_aconfig"
+    bug: "343848473"
+    description: "Enable the federated compute schedule API that accepts an OutcomeReceiver."
+    is_fixed_read_only: true
 }
diff --git a/framework/Android.bp b/framework/Android.bp
index 3c7db35..c122033 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -50,6 +50,7 @@
         ":framework-ondevicepersonalization-sources",
     ],
     libs: [
+        "app-compat-annotations",
         "modules-utils-preconditions",
         "framework-connectivity.stubs.module_lib",
         "ondevicepersonalization_flags_lib",
diff --git a/framework/api/current.txt b/framework/api/current.txt
index d3ef2f4..8b2ad03 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -2,6 +2,7 @@
 package android.adservices.ondevicepersonalization {
 
   @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.on_device_personalization_apis_enabled") public final class AppInfo implements android.os.Parcelable {
+    ctor @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.data_class_missing_ctors_and_getters_enabled") public AppInfo(boolean);
     method public int describeContents();
     method @NonNull public boolean isInstalled();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -9,6 +10,7 @@
   }
 
   @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.on_device_personalization_apis_enabled") public final class DownloadCompletedInput {
+    ctor @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.data_class_missing_ctors_and_getters_enabled") public DownloadCompletedInput(@NonNull android.adservices.ondevicepersonalization.KeyValueStore);
     method @NonNull public android.adservices.ondevicepersonalization.KeyValueStore getDownloadedContents();
   }
 
@@ -24,6 +26,7 @@
   }
 
   @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.on_device_personalization_apis_enabled") public final class EventInput {
+    ctor @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.data_class_missing_ctors_and_getters_enabled") public EventInput(@Nullable android.adservices.ondevicepersonalization.RequestLogRecord, @NonNull android.os.PersistableBundle);
     method @NonNull public android.os.PersistableBundle getParameters();
     method @Nullable public android.adservices.ondevicepersonalization.RequestLogRecord getRequestLogRecord();
   }
@@ -63,12 +66,43 @@
     method @NonNull @WorkerThread public android.net.Uri createEventTrackingUrlWithResponse(@NonNull android.os.PersistableBundle, @Nullable byte[], @Nullable String);
   }
 
+  @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.execute_in_isolated_service_api_enabled") public class ExecuteInIsolatedServiceRequest {
+    method @NonNull public android.os.PersistableBundle getAppParams();
+    method @NonNull public android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceRequest.OutputSpec getOutputSpec();
+    method @NonNull public android.content.ComponentName getService();
+  }
+
+  public static final class ExecuteInIsolatedServiceRequest.Builder {
+    ctor public ExecuteInIsolatedServiceRequest.Builder(@NonNull android.content.ComponentName);
+    method @NonNull public android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceRequest build();
+    method @NonNull public android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceRequest.Builder setAppParams(@NonNull android.os.PersistableBundle);
+    method @NonNull public android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceRequest.Builder setOutputSpec(@NonNull android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceRequest.OutputSpec);
+  }
+
+  public static class ExecuteInIsolatedServiceRequest.OutputSpec {
+    method @NonNull public static android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceRequest.OutputSpec buildBestValueSpec(@IntRange(from=0) int);
+    method @IntRange(from=android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceResponse.DEFAULT_BEST_VALUE) public int getMaxIntValue();
+    method public int getOutputType();
+    field @NonNull public static final android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceRequest.OutputSpec DEFAULT;
+    field public static final int OUTPUT_TYPE_BEST_VALUE = 1; // 0x1
+    field public static final int OUTPUT_TYPE_NULL = 0; // 0x0
+  }
+
+  @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.execute_in_isolated_service_api_enabled") public class ExecuteInIsolatedServiceResponse {
+    ctor public ExecuteInIsolatedServiceResponse(@Nullable android.adservices.ondevicepersonalization.SurfacePackageToken, @IntRange(from=android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceResponse.DEFAULT_BEST_VALUE) int);
+    method @IntRange(from=android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceResponse.DEFAULT_BEST_VALUE) public int getBestValue();
+    method @Nullable public android.adservices.ondevicepersonalization.SurfacePackageToken getSurfacePackageToken();
+    field public static final int DEFAULT_BEST_VALUE = -1; // 0xffffffff
+  }
+
   @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.on_device_personalization_apis_enabled") public final class ExecuteInput {
+    ctor @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.data_class_missing_ctors_and_getters_enabled") public ExecuteInput(@NonNull String, @NonNull android.os.PersistableBundle);
     method @NonNull public String getAppPackageName();
     method @NonNull public android.os.PersistableBundle getAppParams();
   }
 
   @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.on_device_personalization_apis_enabled") public final class ExecuteOutput {
+    method @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.execute_in_isolated_service_api_enabled") @IntRange(from=android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceResponse.DEFAULT_BEST_VALUE) public int getBestValue();
     method @NonNull public java.util.List<android.adservices.ondevicepersonalization.EventLogRecord> getEventLogRecords();
     method @Nullable public byte[] getOutputData();
     method @Nullable public android.adservices.ondevicepersonalization.RenderingConfig getRenderingConfig();
@@ -79,6 +113,7 @@
     ctor public ExecuteOutput.Builder();
     method @NonNull public android.adservices.ondevicepersonalization.ExecuteOutput.Builder addEventLogRecord(@NonNull android.adservices.ondevicepersonalization.EventLogRecord);
     method @NonNull public android.adservices.ondevicepersonalization.ExecuteOutput build();
+    method @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.execute_in_isolated_service_api_enabled") @NonNull public android.adservices.ondevicepersonalization.ExecuteOutput.Builder setBestValue(@IntRange(from=0) int);
     method @NonNull public android.adservices.ondevicepersonalization.ExecuteOutput.Builder setEventLogRecords(@NonNull java.util.List<android.adservices.ondevicepersonalization.EventLogRecord>);
     method @NonNull public android.adservices.ondevicepersonalization.ExecuteOutput.Builder setOutputData(@Nullable byte...);
     method @NonNull public android.adservices.ondevicepersonalization.ExecuteOutput.Builder setRenderingConfig(@Nullable android.adservices.ondevicepersonalization.RenderingConfig);
@@ -95,9 +130,21 @@
     method @NonNull public android.adservices.ondevicepersonalization.FederatedComputeInput.Builder setPopulationName(@NonNull String);
   }
 
+  @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.fcp_schedule_with_outcome_receiver_enabled") public final class FederatedComputeScheduleRequest {
+    ctor public FederatedComputeScheduleRequest(@NonNull android.adservices.ondevicepersonalization.FederatedComputeScheduler.Params, @NonNull String);
+    method @NonNull public android.adservices.ondevicepersonalization.FederatedComputeScheduler.Params getParams();
+    method @NonNull public String getPopulationName();
+  }
+
+  @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.fcp_schedule_with_outcome_receiver_enabled") public final class FederatedComputeScheduleResponse {
+    ctor public FederatedComputeScheduleResponse(@NonNull android.adservices.ondevicepersonalization.FederatedComputeScheduleRequest);
+    method @NonNull public android.adservices.ondevicepersonalization.FederatedComputeScheduleRequest getFederatedComputeScheduleRequest();
+  }
+
   @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.on_device_personalization_apis_enabled") public class FederatedComputeScheduler {
     method @WorkerThread public void cancel(@NonNull android.adservices.ondevicepersonalization.FederatedComputeInput);
     method @WorkerThread public void schedule(@NonNull android.adservices.ondevicepersonalization.FederatedComputeScheduler.Params, @NonNull android.adservices.ondevicepersonalization.FederatedComputeInput);
+    method @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.fcp_schedule_with_outcome_receiver_enabled") @WorkerThread public void schedule(@NonNull android.adservices.ondevicepersonalization.FederatedComputeScheduleRequest, @NonNull android.os.OutcomeReceiver<android.adservices.ondevicepersonalization.FederatedComputeScheduleResponse,java.lang.Exception>);
   }
 
   public static class FederatedComputeScheduler.Params {
@@ -166,7 +213,10 @@
   }
 
   @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.on_device_personalization_apis_enabled") public final class IsolatedServiceException extends java.lang.Exception {
-    ctor public IsolatedServiceException(@IntRange(from=1, to=127) int);
+    ctor public IsolatedServiceException(int);
+    ctor @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.data_class_missing_ctors_and_getters_enabled") public IsolatedServiceException(int, @Nullable Throwable);
+    ctor @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.data_class_missing_ctors_and_getters_enabled") public IsolatedServiceException(int, @Nullable String, @Nullable Throwable);
+    method @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.data_class_missing_ctors_and_getters_enabled") public int getErrorCode();
   }
 
   @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.on_device_personalization_apis_enabled") public interface IsolatedWorker {
@@ -199,12 +249,20 @@
 
   @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.on_device_personalization_apis_enabled") public class OnDevicePersonalizationException extends java.lang.Exception {
     method public int getErrorCode();
+    field @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.execute_in_isolated_service_api_enabled") public static final int ERROR_INFERENCE_FAILED = 9; // 0x9
+    field @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.execute_in_isolated_service_api_enabled") public static final int ERROR_INFERENCE_MODEL_NOT_FOUND = 8; // 0x8
+    field @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.execute_in_isolated_service_api_enabled") public static final int ERROR_INVALID_TRAINING_MANIFEST = 7; // 0x7
     field public static final int ERROR_ISOLATED_SERVICE_FAILED = 1; // 0x1
+    field @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.execute_in_isolated_service_api_enabled") public static final int ERROR_ISOLATED_SERVICE_LOADING_FAILED = 3; // 0x3
+    field @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.execute_in_isolated_service_api_enabled") public static final int ERROR_ISOLATED_SERVICE_MANIFEST_PARSING_FAILED = 4; // 0x4
+    field @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.execute_in_isolated_service_api_enabled") public static final int ERROR_ISOLATED_SERVICE_TIMEOUT = 5; // 0x5
     field public static final int ERROR_PERSONALIZATION_DISABLED = 2; // 0x2
+    field @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.execute_in_isolated_service_api_enabled") public static final int ERROR_SCHEDULE_TRAINING_FAILED = 6; // 0x6
   }
 
   @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.on_device_personalization_apis_enabled") public class OnDevicePersonalizationManager {
     method public void execute(@NonNull android.content.ComponentName, @NonNull android.os.PersistableBundle, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.adservices.ondevicepersonalization.OnDevicePersonalizationManager.ExecuteResult,java.lang.Exception>);
+    method @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.execute_in_isolated_service_api_enabled") public void executeInIsolatedService(@NonNull android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceResponse,java.lang.Exception>);
     method public void requestSurfacePackage(@NonNull android.adservices.ondevicepersonalization.SurfacePackageToken, @NonNull android.os.IBinder, int, int, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.view.SurfaceControlViewHost.SurfacePackage,java.lang.Exception>);
   }
 
@@ -214,6 +272,7 @@
   }
 
   @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.on_device_personalization_apis_enabled") public final class RenderInput {
+    ctor @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.data_class_missing_ctors_and_getters_enabled") public RenderInput(int, int, @Nullable android.adservices.ondevicepersonalization.RenderingConfig);
     method public int getHeight();
     method @Nullable public android.adservices.ondevicepersonalization.RenderingConfig getRenderingConfig();
     method public int getWidth();
@@ -284,6 +343,7 @@
   }
 
   @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.on_device_personalization_apis_enabled") public final class TrainingExamplesInput {
+    ctor @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.data_class_missing_ctors_and_getters_enabled") public TrainingExamplesInput(@NonNull String, @NonNull String, @Nullable byte[], @Nullable String);
     method @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.fcp_model_version_enabled") @Nullable public String getCollectionName();
     method @NonNull public String getPopulationName();
     method @Nullable public byte[] getResumptionToken();
@@ -330,6 +390,7 @@
   }
 
   @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.on_device_personalization_apis_enabled") public final class WebTriggerInput {
+    ctor @FlaggedApi("com.android.adservices.ondevicepersonalization.flags.data_class_missing_ctors_and_getters_enabled") public WebTriggerInput(@NonNull android.net.Uri, @NonNull String, @NonNull byte[]);
     method @NonNull public String getAppPackageName();
     method @NonNull public byte[] getData();
     method @NonNull public android.net.Uri getDestinationUrl();
diff --git a/framework/java/android/adservices/ondevicepersonalization/AppInfo.java b/framework/java/android/adservices/ondevicepersonalization/AppInfo.java
index 57ce7f9..4d462b8 100644
--- a/framework/java/android/adservices/ondevicepersonalization/AppInfo.java
+++ b/framework/java/android/adservices/ondevicepersonalization/AppInfo.java
@@ -32,9 +32,17 @@
 @DataClass(genHiddenBuilder = true, genEqualsHashCode = true)
 public final class AppInfo implements Parcelable {
     /** Whether the app is installed. */
-    @NonNull boolean mInstalled = false;
+    private boolean mInstalled = false;
 
-
+    /**
+     * Creates a new AppInfo.
+     *
+     * @param installed {@code true} if the app is installed.
+     */
+    @FlaggedApi(Flags.FLAG_DATA_CLASS_MISSING_CTORS_AND_GETTERS_ENABLED)
+    public AppInfo(boolean installed) {
+        this.mInstalled = installed;
+    }
 
     // Code below generated by codegen v1.0.23.
     //
@@ -48,17 +56,6 @@
     //   Settings > Editor > Code Style > Formatter Control
     //@formatter:off
 
-
-    @DataClass.Generated.Member
-    /* package-private */ AppInfo(
-            @NonNull boolean installed) {
-        this.mInstalled = installed;
-        AnnotationValidations.validate(
-                NonNull.class, null, mInstalled);
-
-        // onConstructed(); // You can define this method to get a callback
-    }
-
     /**
      * Whether the app is installed.
      */
diff --git a/framework/java/android/adservices/ondevicepersonalization/Constants.java b/framework/java/android/adservices/ondevicepersonalization/Constants.java
index f9aae84..09ad701 100644
--- a/framework/java/android/adservices/ondevicepersonalization/Constants.java
+++ b/framework/java/android/adservices/ondevicepersonalization/Constants.java
@@ -30,9 +30,42 @@
     public static final int STATUS_NAME_NOT_FOUND = 101;
     public static final int STATUS_CLASS_NOT_FOUND = 102;
     public static final int STATUS_SERVICE_FAILED = 103;
+
+    /**
+     * Internal code that tracks user privacy is not eligible to run operation. DO NOT expose this
+     * status externally.
+     */
     public static final int STATUS_PERSONALIZATION_DISABLED = 104;
+
     public static final int STATUS_KEY_NOT_FOUND = 105;
 
+    /** Internal error code that tracks failure to read ODP manifest settings. */
+    public static final int STATUS_MANIFEST_PARSING_FAILED = 106;
+
+    /** Internal error code that tracks misconfigured ODP manifest settings. */
+    public static final int STATUS_MANIFEST_MISCONFIGURED = 107;
+
+    /** Internal error code that tracks errors in loading the Isolated Service. */
+    public static final int STATUS_ISOLATED_SERVICE_LOADING_FAILED = 108;
+
+    /** Internal error code that tracks error when Isolated Service times out. */
+    public static final int STATUS_ISOLATED_SERVICE_TIMEOUT = 109;
+
+    /** Internal error code that tracks error when the FCP manifest is invalid or missing. */
+    public static final int STATUS_FCP_MANIFEST_INVALID = 110;
+
+    /** Internal code that tracks empty result returned from data storage. */
+    public static final int STATUS_SUCCESS_EMPTY_RESULT = 111;
+
+    /** Internal code that tracks timeout exception when run operation. */
+    public static final int STATUS_TIMEOUT = 112;
+
+    /** Internal code that tracks remote exception when run operation. */
+    public static final int STATUS_REMOTE_EXCEPTION = 113;
+    /** Internal code that tracks method not found. */
+    public static final int STATUS_METHOD_NOT_FOUND = 114;
+    public static final int STATUS_CALLER_NOT_ALLOWED = 115;
+
     // Operations implemented by IsolatedService.
     public static final int OP_EXECUTE = 1;
     public static final int OP_DOWNLOAD = 2;
@@ -64,6 +97,8 @@
     public static final String EXTRA_MIME_TYPE = "android.ondevicepersonalization.extra.mime_type";
     public static final String EXTRA_OUTPUT_DATA =
             "android.ondevicepersonalization.extra.output_data";
+    public static final String EXTRA_OUTPUT_BEST_VALUE =
+            "android.ondevicepersonalization.extra.output_best_value";
     public static final String EXTRA_RESPONSE_DATA =
             "android.ondevicepersonalization.extra.response_data";
     public static final String EXTRA_RESULT = "android.ondevicepersonalization.extra.result";
@@ -105,6 +140,7 @@
     public static final int API_NAME_MODEL_MANAGER_RUN = 20;
     public static final int API_NAME_FEDERATED_COMPUTE_CANCEL = 21;
     public static final int API_NAME_NOTIFY_MEASUREMENT_EVENT = 22;
+    public static final int API_NAME_ADSERVICES_GET_COMMON_STATES = 23;
 
     // Data Access Service operations.
     public static final int DATA_ACCESS_OP_REMOTE_DATA_LOOKUP = 1;
diff --git a/framework/java/android/adservices/ondevicepersonalization/DownloadCompletedInput.java b/framework/java/android/adservices/ondevicepersonalization/DownloadCompletedInput.java
index 147d2da..05714b0 100644
--- a/framework/java/android/adservices/ondevicepersonalization/DownloadCompletedInput.java
+++ b/framework/java/android/adservices/ondevicepersonalization/DownloadCompletedInput.java
@@ -21,8 +21,8 @@
 import android.annotation.Nullable;
 
 import com.android.adservices.ondevicepersonalization.flags.Flags;
-import com.android.ondevicepersonalization.internal.util.AnnotationValidations;
-import com.android.ondevicepersonalization.internal.util.DataClass;
+
+import java.util.Objects;
 
 /**
  * The input data for {@link
@@ -30,7 +30,6 @@
  *
  */
 @FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED)
-@DataClass(genHiddenBuilder = true, genEqualsHashCode = true)
 public final class DownloadCompletedInput {
     /**
      * A {@link KeyValueStore} that contains the downloaded content.
@@ -38,45 +37,25 @@
     @NonNull KeyValueStore mDownloadedContents;
 
 
-
-    // Code below generated by codegen v1.0.23.
-    //
-    // DO NOT MODIFY!
-    // CHECKSTYLE:OFF Generated code
-    //
-    // To regenerate run:
-    // $ codegen $ANDROID_BUILD_TOP/packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/DownloadCompletedInput.java
-    //
-    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
-    //   Settings > Editor > Code Style > Formatter Control
-    //@formatter:off
-
-
-    @DataClass.Generated.Member
-    /* package-private */ DownloadCompletedInput(
+    /** Creates a {@link DownloadCompletedInput}
+     *
+     * @param downloadedContents a {@link KeyValueStore} that contains the downloaded contents.
+     */
+    @FlaggedApi(Flags.FLAG_DATA_CLASS_MISSING_CTORS_AND_GETTERS_ENABLED)
+    public DownloadCompletedInput(
             @NonNull KeyValueStore downloadedContents) {
-        this.mDownloadedContents = downloadedContents;
-        AnnotationValidations.validate(
-                NonNull.class, null, mDownloadedContents);
-
-        // onConstructed(); // You can define this method to get a callback
+        this.mDownloadedContents = Objects.requireNonNull(downloadedContents);
     }
 
     /**
      * Map containing downloaded keys and values
      */
-    @DataClass.Generated.Member
     public @NonNull KeyValueStore getDownloadedContents() {
         return mDownloadedContents;
     }
 
     @Override
-    @DataClass.Generated.Member
     public boolean equals(@Nullable Object o) {
-        // You can override field equality logic by defining either of the methods like:
-        // boolean fieldNameEquals(DownloadCompletedInput other) { ... }
-        // boolean fieldNameEquals(FieldType otherValue) { ... }
-
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         @SuppressWarnings("unchecked")
@@ -87,83 +66,29 @@
     }
 
     @Override
-    @DataClass.Generated.Member
     public int hashCode() {
-        // You can override field hashCode logic by defining methods like:
-        // int fieldNameHashCode() { ... }
-
         int _hash = 1;
         _hash = 31 * _hash + java.util.Objects.hashCode(mDownloadedContents);
         return _hash;
     }
 
+    // TODO(b/353356413): Remove builder after it is not used in CTS.
     /**
      * A builder for {@link DownloadCompletedInput}
      * @hide
      */
-    @SuppressWarnings("WeakerAccess")
-    @DataClass.Generated.Member
     public static final class Builder {
-
         private @NonNull KeyValueStore mDownloadedContents;
 
-        private long mBuilderFieldsSet = 0L;
-
-        public Builder() {
-        }
-
-        /**
-         * Creates a new Builder.
-         *
-         * @param downloadedContents
-         *   Map containing downloaded keys and values
-         */
         public Builder(
                 @NonNull KeyValueStore downloadedContents) {
             mDownloadedContents = downloadedContents;
-            AnnotationValidations.validate(
-                    NonNull.class, null, mDownloadedContents);
         }
 
-        /**
-         * Map containing downloaded keys and values
-         */
-        @DataClass.Generated.Member
-        public @NonNull Builder setDownloadedContents(@NonNull KeyValueStore value) {
-            checkNotUsed();
-            mBuilderFieldsSet |= 0x1;
-            mDownloadedContents = value;
-            return this;
-        }
-
-        /** Builds the instance. This builder should not be touched after calling this! */
         public @NonNull DownloadCompletedInput build() {
-            checkNotUsed();
-            mBuilderFieldsSet |= 0x2; // Mark builder used
-
             DownloadCompletedInput o = new DownloadCompletedInput(
                     mDownloadedContents);
             return o;
         }
-
-        private void checkNotUsed() {
-            if ((mBuilderFieldsSet & 0x2) != 0) {
-                throw new IllegalStateException(
-                        "This Builder should not be reused. Use a new Builder instance instead");
-            }
-        }
     }
-
-    @DataClass.Generated(
-            time = 1706205792643L,
-            codegenVersion = "1.0.23",
-            sourceFile = "packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/DownloadCompletedInput.java",
-            inputSignatures = " @android.annotation.NonNull android.adservices.ondevicepersonalization.KeyValueStore mDownloadedContents\nclass DownloadCompletedInput extends java.lang.Object implements []\[email protected](genHiddenBuilder=true, genEqualsHashCode=true)")
-    @Deprecated
-    private void __metadata() {}
-
-
-    //@formatter:on
-    // End of generated code
-
 }
diff --git a/framework/java/android/adservices/ondevicepersonalization/EventInput.java b/framework/java/android/adservices/ondevicepersonalization/EventInput.java
index 0ee3f67..f3f0bee 100644
--- a/framework/java/android/adservices/ondevicepersonalization/EventInput.java
+++ b/framework/java/android/adservices/ondevicepersonalization/EventInput.java
@@ -22,15 +22,14 @@
 import android.os.PersistableBundle;
 
 import com.android.adservices.ondevicepersonalization.flags.Flags;
-import com.android.ondevicepersonalization.internal.util.AnnotationValidations;
-import com.android.ondevicepersonalization.internal.util.DataClass;
+
+import java.util.Objects;
 
 /**
  * The input data for {@link
  * IsolatedWorker#onEvent(EventInput, android.os.OutcomeReceiver)}.
  */
 @FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED)
-@DataClass(genBuilder = false, genHiddenConstructor = true, genEqualsHashCode = true)
 public final class EventInput {
     /**
      * The {@link RequestLogRecord} that was returned as a result of
@@ -50,21 +49,6 @@
         this(parcel.getRequestLogRecord(), parcel.getParameters());
     }
 
-
-
-    // Code below generated by codegen v1.0.23.
-    //
-    // DO NOT MODIFY!
-    // CHECKSTYLE:OFF Generated code
-    //
-    // To regenerate run:
-    // $ codegen $ANDROID_BUILD_TOP/packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/EventInput.java
-    //
-    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
-    //   Settings > Editor > Code Style > Formatter Control
-    //@formatter:off
-
-
     /**
      * Creates a new EventInput.
      *
@@ -75,25 +59,19 @@
      *   The Event URL parameters that the service passed to {@link
      *   EventUrlProvider#createEventTrackingUrlWithResponse(PersistableBundle, byte[], String)}
      *   or {@link EventUrlProvider#createEventTrackingUrlWithRedirect(PersistableBundle, Uri)}.
-     * @hide
      */
-    @DataClass.Generated.Member
+    @FlaggedApi(Flags.FLAG_DATA_CLASS_MISSING_CTORS_AND_GETTERS_ENABLED)
     public EventInput(
             @Nullable RequestLogRecord requestLogRecord,
             @NonNull PersistableBundle parameters) {
         this.mRequestLogRecord = requestLogRecord;
-        this.mParameters = parameters;
-        AnnotationValidations.validate(
-                NonNull.class, null, mParameters);
-
-        // onConstructed(); // You can define this method to get a callback
+        this.mParameters = Objects.requireNonNull(parameters);
     }
 
     /**
      * The {@link RequestLogRecord} that was returned as a result of
      * {@link IsolatedWorker#onExecute(ExecuteInput, android.os.OutcomeReceiver)}.
      */
-    @DataClass.Generated.Member
     public @Nullable RequestLogRecord getRequestLogRecord() {
         return mRequestLogRecord;
     }
@@ -103,18 +81,12 @@
      * EventUrlProvider#createEventTrackingUrlWithResponse(PersistableBundle, byte[], String)}
      * or {@link EventUrlProvider#createEventTrackingUrlWithRedirect(PersistableBundle, Uri)}.
      */
-    @DataClass.Generated.Member
     public @NonNull PersistableBundle getParameters() {
         return mParameters;
     }
 
     @Override
-    @DataClass.Generated.Member
     public boolean equals(@Nullable Object o) {
-        // You can override field equality logic by defining either of the methods like:
-        // boolean fieldNameEquals(EventInput other) { ... }
-        // boolean fieldNameEquals(FieldType otherValue) { ... }
-
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         @SuppressWarnings("unchecked")
@@ -126,27 +98,10 @@
     }
 
     @Override
-    @DataClass.Generated.Member
     public int hashCode() {
-        // You can override field hashCode logic by defining methods like:
-        // int fieldNameHashCode() { ... }
-
         int _hash = 1;
         _hash = 31 * _hash + java.util.Objects.hashCode(mRequestLogRecord);
         _hash = 31 * _hash + java.util.Objects.hashCode(mParameters);
         return _hash;
     }
-
-    @DataClass.Generated(
-            time = 1698882321696L,
-            codegenVersion = "1.0.23",
-            sourceFile = "packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/EventInput.java",
-            inputSignatures = "private @android.annotation.Nullable android.adservices.ondevicepersonalization.RequestLogRecord mRequestLogRecord\nprivate @android.annotation.NonNull android.os.PersistableBundle mParameters\nclass EventInput extends java.lang.Object implements []\[email protected](genBuilder=false, genHiddenConstructor=true, genEqualsHashCode=true)")
-    @Deprecated
-    private void __metadata() {}
-
-
-    //@formatter:on
-    // End of generated code
-
 }
diff --git a/framework/java/android/adservices/ondevicepersonalization/ExecuteInIsolatedServiceRequest.java b/framework/java/android/adservices/ondevicepersonalization/ExecuteInIsolatedServiceRequest.java
new file mode 100644
index 0000000..396132e
--- /dev/null
+++ b/framework/java/android/adservices/ondevicepersonalization/ExecuteInIsolatedServiceRequest.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2024 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.adservices.ondevicepersonalization;
+
+import static android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceResponse.DEFAULT_BEST_VALUE;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.os.PersistableBundle;
+
+import com.android.adservices.ondevicepersonalization.flags.Flags;
+import com.android.ondevicepersonalization.internal.util.AnnotationValidations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/** The request of {@link OnDevicePersonalizationManager#executeInIsolatedService}. */
+@FlaggedApi(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+public class ExecuteInIsolatedServiceRequest {
+    /** The {@link ComponentName} of the {@link IsolatedService}. */
+    @NonNull private ComponentName mService;
+
+    /**
+     * A {@link PersistableBundle} that is passed from the calling app to the {@link
+     * IsolatedService}. The expected contents of this parameter are defined by the {@link
+     * IsolatedService}. The platform does not interpret this parameter.
+     */
+    @NonNull private PersistableBundle mAppParams;
+
+    /**
+     * The set of spec to indicate output of {@link IsolatedService}. It's mainly used by platform.
+     * If {@link OutputSpec} is set to {@link OutputSpec#DEFAULT}, OnDevicePersonalization will
+     * ignore result returned by {@link IsolatedService}. If {@link OutputSpec} is built with {@link
+     * OutputSpec#buildBestValueSpec}, OnDevicePersonalization will verify {@link
+     * ExecuteOutput#getBestValue()} returned by {@link IsolatedService} within the max value range
+     * set in {@link OutputSpec#getMaxIntValue} and add noise.
+     */
+    @NonNull private OutputSpec mOutputSpec;
+
+    /**
+     * The set of spec to indicate output of {@link IsolatedService}. It's mainly used by platform.
+     * If {@link OutputSpec} is set to {@link OutputSpec#DEFAULT}, OnDevicePersonalization will
+     * ignore result returned by {@link IsolatedService}. If {@link OutputSpec} is built with {@link
+     * OutputSpec#buildBestValueSpec}, OnDevicePersonalization will verify {@link
+     * ExecuteOutput#getBestValue()} returned by {@link IsolatedService} within the max value range
+     * set in {@link OutputSpec#getMaxIntValue} and add noise.
+     */
+    public static class OutputSpec {
+        /**
+         * The default value of OutputType. If set, OnDevicePersonalization will ignore result
+         * returned by {@link IsolatedService} and {@link ExecuteInIsolatedServiceResponse} doesn't
+         * return any output data.
+         */
+        public static final int OUTPUT_TYPE_NULL = 0;
+
+        /**
+         * If set, {@link ExecuteInIsolatedServiceResponse#getBestValue()} will return an integer
+         * that indicates the index of best values passed in {@link
+         * ExecuteInIsolatedServiceRequest#getAppParams}.
+         */
+        public static final int OUTPUT_TYPE_BEST_VALUE = 1;
+
+        /** @hide */
+        @IntDef(
+                prefix = "OUTPUT_TYPE_",
+                value = {OUTPUT_TYPE_NULL, OUTPUT_TYPE_BEST_VALUE})
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface OutputType {}
+
+        /** Default value is OUTPUT_TYPE_NULL. */
+        @OutputType private final int mOutputType;
+
+        /** Optional. Only set when output option is OUTPUT_TYPE_BEST_VALUE. */
+        @IntRange(from = DEFAULT_BEST_VALUE)
+        private final int mMaxIntValue;
+
+        /** The default value of {@link OutputSpec}. */
+        @NonNull
+        public static final OutputSpec DEFAULT =
+                new OutputSpec(OUTPUT_TYPE_NULL, DEFAULT_BEST_VALUE);
+
+        private OutputSpec(int outputType, int maxIntValue) {
+            mMaxIntValue = maxIntValue;
+            mOutputType = outputType;
+        }
+
+        /**
+         * Creates the output spec to get best value out of {@code maxIntValue}. If set this, caller
+         * can call {@link ExecuteInIsolatedServiceResponse#getBestValue} to get result.
+         *
+         * @param maxIntValue the maximum value {@link IsolatedWorker} can return to caller app.
+         */
+        public @NonNull static OutputSpec buildBestValueSpec(@IntRange(from = 0) int maxIntValue) {
+            AnnotationValidations.validate(IntRange.class, null, maxIntValue, "from", 0);
+            return new OutputSpec(OUTPUT_TYPE_BEST_VALUE, maxIntValue);
+        }
+
+        /**
+         * Returns the output type of {@link IsolatedService}. The default value is {@link
+         * OutputSpec#OUTPUT_TYPE_NULL}.
+         */
+        public @OutputType int getOutputType() {
+            return mOutputType;
+        }
+
+        /**
+         * Returns the value set in {@link OutputSpec#buildBestValueSpec}. The value is expected to
+         * be {@link ExecuteInIsolatedServiceResponse#DEFAULT_BEST_VALUE} if {@link #getOutputType}
+         * is {@link OutputSpec#OUTPUT_TYPE_NULL}.
+         */
+        public @IntRange(from = DEFAULT_BEST_VALUE) int getMaxIntValue() {
+            return mMaxIntValue;
+        }
+    }
+
+    /* package-private */ ExecuteInIsolatedServiceRequest(
+            @NonNull ComponentName service,
+            @NonNull PersistableBundle appParams,
+            @NonNull OutputSpec outputSpec) {
+        Objects.requireNonNull(service);
+        Objects.requireNonNull(appParams);
+        Objects.requireNonNull(outputSpec);
+        this.mService = service;
+        this.mAppParams = appParams;
+        this.mOutputSpec = outputSpec;
+    }
+
+    /** The {@link ComponentName} of the {@link IsolatedService}. */
+    public @NonNull ComponentName getService() {
+        return mService;
+    }
+
+    /**
+     * A {@link PersistableBundle} that is passed from the calling app to the {@link
+     * IsolatedService}. The expected contents of this parameter are defined by the {@link
+     * IsolatedService}. The platform does not interpret this parameter.
+     */
+    public @NonNull PersistableBundle getAppParams() {
+        return mAppParams;
+    }
+
+    /**
+     * The set of spec to indicate output of {@link IsolatedService}. It's mainly used by platform.
+     * For example, platform calls {@link OutputSpec#getOutputType} and validates the result
+     * received from {@link IsolatedService}.
+     */
+    public @NonNull OutputSpec getOutputSpec() {
+        return mOutputSpec;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        // You can override field equality logic by defining either of the methods like:
+        // boolean fieldNameEquals(ExecuteInIsolatedServiceRequest other) { ... }
+        // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        ExecuteInIsolatedServiceRequest that = (ExecuteInIsolatedServiceRequest) o;
+        //noinspection PointlessBooleanExpression
+        return java.util.Objects.equals(mService, that.mService)
+                && java.util.Objects.equals(mAppParams, that.mAppParams)
+                && java.util.Objects.equals(mOutputSpec, that.mOutputSpec);
+    }
+
+    @Override
+    public int hashCode() {
+        // You can override field hashCode logic by defining methods like:
+        // int fieldNameHashCode() { ... }
+
+        int _hash = 1;
+        _hash = 31 * _hash + java.util.Objects.hashCode(mService);
+        _hash = 31 * _hash + java.util.Objects.hashCode(mAppParams);
+        _hash = 31 * _hash + java.util.Objects.hashCode(mOutputSpec);
+        return _hash;
+    }
+
+    /** A builder for {@link ExecuteInIsolatedServiceRequest} */
+    public static final class Builder {
+
+        private @NonNull ComponentName mService;
+        private @NonNull PersistableBundle mAppParams = PersistableBundle.EMPTY;
+        private @NonNull OutputSpec mOutputSpec = OutputSpec.DEFAULT;
+
+        /**
+         * Creates a new Builder.
+         *
+         * @param service The {@link ComponentName} of the {@link IsolatedService}.
+         */
+        public Builder(@NonNull ComponentName service) {
+            Objects.requireNonNull(service);
+            mService = service;
+        }
+
+        /**
+         * A {@link PersistableBundle} that is passed from the calling app to the {@link
+         * IsolatedService}. The expected contents of this parameter are defined by the {@link
+         * IsolatedService}. The platform does not interpret this parameter.
+         */
+        public @NonNull Builder setAppParams(@NonNull PersistableBundle value) {
+            Objects.requireNonNull(value);
+            mAppParams = value;
+            return this;
+        }
+
+        /**
+         * The set of spec to indicate output of {@link IsolatedService}. It's mainly used by
+         * platform. If {@link OutputSpec} is set to {@link OutputSpec#DEFAULT},
+         * OnDevicePersonalization will ignore result returned by {@link IsolatedService}. If {@link
+         * OutputSpec} is built with {@link OutputSpec#buildBestValueSpec}, OnDevicePersonalization
+         * will verify {@link ExecuteOutput#getBestValue()} returned by {@link IsolatedService}
+         * within the max value range set in {@link OutputSpec#getMaxIntValue} and add noise.
+         */
+        public @NonNull Builder setOutputSpec(@NonNull OutputSpec value) {
+            Objects.requireNonNull(value);
+            mOutputSpec = value;
+            return this;
+        }
+
+        /** Builds the instance. */
+        public @NonNull ExecuteInIsolatedServiceRequest build() {
+            return new ExecuteInIsolatedServiceRequest(mService, mAppParams, mOutputSpec);
+        }
+    }
+}
diff --git a/framework/java/android/adservices/ondevicepersonalization/ExecuteInIsolatedServiceResponse.java b/framework/java/android/adservices/ondevicepersonalization/ExecuteInIsolatedServiceResponse.java
new file mode 100644
index 0000000..b528b94
--- /dev/null
+++ b/framework/java/android/adservices/ondevicepersonalization/ExecuteInIsolatedServiceResponse.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2024 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.adservices.ondevicepersonalization;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
+import android.annotation.Nullable;
+
+import com.android.adservices.ondevicepersonalization.flags.Flags;
+import com.android.ondevicepersonalization.internal.util.AnnotationValidations;
+
+/** The response of {@link OnDevicePersonalizationManager#executeInIsolatedService}. */
+@FlaggedApi(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+public class ExecuteInIsolatedServiceResponse {
+    /**
+     * An opaque reference to content that can be displayed in a {@link android.view.SurfaceView}.
+     * This may be {@code null} if the {@link IsolatedService} has not generated any content to be
+     * displayed within the calling app.
+     */
+    @Nullable private final SurfacePackageToken mSurfacePackageToken;
+
+    /**
+     * The default value of {@link ExecuteInIsolatedServiceResponse#getBestValue} if {@link
+     * IsolatedService} didn't return any content.
+     */
+    public static final int DEFAULT_BEST_VALUE = -1;
+
+    /**
+     * The int value that was returned by the {@link IsolatedService} and applied noise. If {@link
+     * IsolatedService} didn't return any content, the default value is {@link #DEFAULT_BEST_VALUE}.
+     * If {@link IsolatedService} returns an integer value, we will apply the noise to the value and
+     * the range of this value is between 0 and {@link
+     * ExecuteInIsolatedServiceRequest.OutputSpec#getMaxIntValue()}.
+     */
+    private int mBestValue = DEFAULT_BEST_VALUE;
+
+    /**
+     * Creates a new ExecuteInIsolatedServiceResponse.
+     *
+     * @param surfacePackageToken an opaque reference to content that can be displayed in a {@link
+     *     android.view.SurfaceView}. This may be {@code null} if the {@link IsolatedService} has
+     *     not generated any content to be displayed within the calling app.
+     * @param bestValue an int value that was returned by the {@link IsolatedService} and applied
+     *     noise.If {@link ExecuteInIsolatedServiceRequest} output type is set to {@link
+     *     ExecuteInIsolatedServiceRequest.OutputSpec#OUTPUT_TYPE_NULL}, the platform ignores the
+     *     data returned by {@link IsolatedService} and returns the default value {@link
+     *     #DEFAULT_BEST_VALUE}. If {@link ExecuteInIsolatedServiceRequest} output type is set to
+     *     {@link ExecuteInIsolatedServiceRequest.OutputSpec#OUTPUT_TYPE_BEST_VALUE}, the platform
+     *     validates {@link ExecuteOutput#getBestValue} between 0 and {@link
+     *     ExecuteInIsolatedServiceRequest.OutputSpec#getMaxIntValue} and applies noise to result.
+     */
+    public ExecuteInIsolatedServiceResponse(
+            @Nullable SurfacePackageToken surfacePackageToken,
+            @IntRange(from = DEFAULT_BEST_VALUE) int bestValue) {
+        AnnotationValidations.validate(IntRange.class, null, bestValue, "from", DEFAULT_BEST_VALUE);
+        mSurfacePackageToken = surfacePackageToken;
+        mBestValue = bestValue;
+    }
+
+    /** @hide */
+    public ExecuteInIsolatedServiceResponse(@Nullable SurfacePackageToken surfacePackageToken) {
+        mSurfacePackageToken = surfacePackageToken;
+    }
+
+    /**
+     * Returns a {@link SurfacePackageToken}, which is an opaque reference to content that can be
+     * displayed in a {@link android.view.SurfaceView}. This may be {@code null} if the {@link
+     * IsolatedService} has not generated any content to be displayed within the calling app.
+     */
+    @Nullable
+    public SurfacePackageToken getSurfacePackageToken() {
+        return mSurfacePackageToken;
+    }
+
+    /**
+     * Returns the int value that was returned by the {@link IsolatedService} and applied noise. If
+     * {@link ExecuteInIsolatedServiceRequest} output type is set to {@link
+     * ExecuteInIsolatedServiceRequest.OutputSpec#OUTPUT_TYPE_NULL}, the platform ignores the data
+     * returned by {@link IsolatedService} and returns the default value {@link
+     * #DEFAULT_BEST_VALUE}. If {@link ExecuteInIsolatedServiceRequest} output type is set to {@link
+     * ExecuteInIsolatedServiceRequest.OutputSpec#OUTPUT_TYPE_BEST_VALUE}, the platform validates
+     * {@link ExecuteOutput#getBestValue} between 0 and {@link
+     * ExecuteInIsolatedServiceRequest.OutputSpec#getMaxIntValue()} and applies noise to result.
+     */
+    public @IntRange(from = DEFAULT_BEST_VALUE) int getBestValue() {
+        return mBestValue;
+    }
+}
diff --git a/framework/java/android/adservices/ondevicepersonalization/ExecuteInput.java b/framework/java/android/adservices/ondevicepersonalization/ExecuteInput.java
index cda9262..e3c93ea 100644
--- a/framework/java/android/adservices/ondevicepersonalization/ExecuteInput.java
+++ b/framework/java/android/adservices/ondevicepersonalization/ExecuteInput.java
@@ -34,8 +34,8 @@
 @FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED)
 public final class ExecuteInput {
     @NonNull private final String mAppPackageName;
-    @Nullable private final ByteArrayParceledSlice mSerializedAppParams;
     @NonNull private final Object mAppParamsLock = new Object();
+    @Nullable private ByteArrayParceledSlice mSerializedAppParams;
     @NonNull private volatile PersistableBundle mAppParams = null;
 
     /** @hide */
@@ -44,6 +44,18 @@
         mSerializedAppParams = parcel.getSerializedAppParams();
     }
 
+    /** Creates an {@link ExecuteInput}.
+     *
+     * @param appPackageName the package name of the calling app.
+     * @param appParams the parameters provided by the app to the {@link IsolatedService}. The
+     * service defines the expected keys in this {@link PersistableBundle}.
+     */
+    @FlaggedApi(Flags.FLAG_DATA_CLASS_MISSING_CTORS_AND_GETTERS_ENABLED)
+    public ExecuteInput(@NonNull String appPackageName, @NonNull PersistableBundle appParams) {
+        mAppPackageName = Objects.requireNonNull(appPackageName);
+        mAppParams = Objects.requireNonNull(appParams);
+    }
+
     /**
      * The package name of the calling app.
      */
@@ -68,6 +80,7 @@
                         ? PersistableBundleUtils.fromByteArray(
                                 mSerializedAppParams.getByteArray())
                         : PersistableBundle.EMPTY;
+                mSerializedAppParams = null;
                 return mAppParams;
             } catch (Exception e) {
                 throw new IllegalStateException(e);
diff --git a/framework/java/android/adservices/ondevicepersonalization/aidl/IOnDevicePersonalizationConfigServiceCallback.aidl b/framework/java/android/adservices/ondevicepersonalization/ExecuteOptionsParcel.aidl
similarity index 62%
rename from framework/java/android/adservices/ondevicepersonalization/aidl/IOnDevicePersonalizationConfigServiceCallback.aidl
rename to framework/java/android/adservices/ondevicepersonalization/ExecuteOptionsParcel.aidl
index 74092b4..89a5ddd 100644
--- a/framework/java/android/adservices/ondevicepersonalization/aidl/IOnDevicePersonalizationConfigServiceCallback.aidl
+++ b/framework/java/android/adservices/ondevicepersonalization/ExecuteOptionsParcel.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -14,17 +14,6 @@
  * limitations under the License.
  */
 
-package android.adservices.ondevicepersonalization.aidl;
+package android.adservices.ondevicepersonalization;
 
-import android.os.Bundle;
-
-/**
-  * Callback from a OnDevicePersonalizationConfigService.
-  * @hide
-  */
-oneway interface IOnDevicePersonalizationConfigServiceCallback {
-
-    void onSuccess();
-
-    void onFailure(int errorCode);
-}
\ No newline at end of file
+parcelable ExecuteOptionsParcel;
diff --git a/framework/java/android/adservices/ondevicepersonalization/ExecuteOptionsParcel.java b/framework/java/android/adservices/ondevicepersonalization/ExecuteOptionsParcel.java
new file mode 100644
index 0000000..e008b42
--- /dev/null
+++ b/framework/java/android/adservices/ondevicepersonalization/ExecuteOptionsParcel.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2024 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.adservices.ondevicepersonalization;
+
+import android.annotation.NonNull;
+import android.os.Parcelable;
+
+import com.android.ondevicepersonalization.internal.util.AnnotationValidations;
+import com.android.ondevicepersonalization.internal.util.DataClass;
+
+/** @hide */
+@DataClass(genAidl = false, genBuilder = false)
+public class ExecuteOptionsParcel implements Parcelable {
+    /** Default value is OUTPUT_TYPE_NULL. */
+    @ExecuteInIsolatedServiceRequest.OutputSpec.OutputType private final int mOutputType;
+
+    /** Optional. Only set when output option is OUTPUT_TYPE_BEST_VALUE. */
+    private final int mMaxIntValue;
+
+    public static ExecuteOptionsParcel DEFAULT = new ExecuteOptionsParcel();
+
+    /** @hide */
+    public ExecuteOptionsParcel(@NonNull ExecuteInIsolatedServiceRequest.OutputSpec options) {
+        this(options.getOutputType(), options.getMaxIntValue());
+    }
+
+    /**
+     * Create a default instance of {@link ExecuteOptionsParcel}.
+     *
+     * @hide
+     */
+    private ExecuteOptionsParcel() {
+        this(ExecuteInIsolatedServiceRequest.OutputSpec.OUTPUT_TYPE_NULL, -1);
+    }
+
+    // Code below generated by codegen v1.0.23.
+    //
+    // DO NOT MODIFY!
+    // CHECKSTYLE:OFF Generated code
+    //
+    // To regenerate run:
+    // $ codegen
+    // $ANDROID_BUILD_TOP/packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/ExecuteOptionsParcel.java
+    //
+    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+    //   Settings > Editor > Code Style > Formatter Control
+    // @formatter:off
+
+    /**
+     * Creates a new ExecuteOptionsParcel.
+     *
+     * @param outputType Default value is OUTPUT_TYPE_NULL.
+     * @param maxIntValue Optional. Only set when output option is OUTPUT_TYPE_BEST_VALUE.
+     */
+    @DataClass.Generated.Member
+    public ExecuteOptionsParcel(
+            @ExecuteInIsolatedServiceRequest.OutputSpec.OutputType int outputType,
+            int maxIntValue) {
+        this.mOutputType = outputType;
+        AnnotationValidations.validate(
+                ExecuteInIsolatedServiceRequest.OutputSpec.OutputType.class, null, mOutputType);
+        this.mMaxIntValue = maxIntValue;
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    /** Default value is OUTPUT_TYPE_NULL. */
+    @DataClass.Generated.Member
+    public @ExecuteInIsolatedServiceRequest.OutputSpec.OutputType int getOutputType() {
+        return mOutputType;
+    }
+
+    /** Optional. Only set when output option is OUTPUT_TYPE_BEST_VALUE. */
+    @DataClass.Generated.Member
+    public int getMaxIntValue() {
+        return mMaxIntValue;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
+        // You can override field parcelling by defining methods like:
+        // void parcelFieldName(Parcel dest, int flags) { ... }
+
+        dest.writeInt(mOutputType);
+        dest.writeInt(mMaxIntValue);
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int describeContents() {
+        return 0;
+    }
+
+    /** @hide */
+    @SuppressWarnings({"unchecked", "RedundantCast"})
+    @DataClass.Generated.Member
+    protected ExecuteOptionsParcel(@NonNull android.os.Parcel in) {
+        // You can override field unparcelling by defining methods like:
+        // static FieldType unparcelFieldName(Parcel in) { ... }
+
+        int outputType = in.readInt();
+        int maxIntValue = in.readInt();
+
+        this.mOutputType = outputType;
+        AnnotationValidations.validate(
+                ExecuteInIsolatedServiceRequest.OutputSpec.OutputType.class, null, mOutputType);
+        this.mMaxIntValue = maxIntValue;
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @DataClass.Generated.Member
+    public static final @NonNull Parcelable.Creator<ExecuteOptionsParcel> CREATOR =
+            new Parcelable.Creator<ExecuteOptionsParcel>() {
+                @Override
+                public ExecuteOptionsParcel[] newArray(int size) {
+                    return new ExecuteOptionsParcel[size];
+                }
+
+                @Override
+                public ExecuteOptionsParcel createFromParcel(@NonNull android.os.Parcel in) {
+                    return new ExecuteOptionsParcel(in);
+                }
+            };
+    // @formatter:on
+    // End of generated code
+
+}
diff --git a/framework/java/android/adservices/ondevicepersonalization/ExecuteOutput.java b/framework/java/android/adservices/ondevicepersonalization/ExecuteOutput.java
index 808de96..7c8468f 100644
--- a/framework/java/android/adservices/ondevicepersonalization/ExecuteOutput.java
+++ b/framework/java/android/adservices/ondevicepersonalization/ExecuteOutput.java
@@ -16,9 +16,15 @@
 
 package android.adservices.ondevicepersonalization;
 
+import static android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceResponse.DEFAULT_BEST_VALUE;
+
 import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.os.OutcomeReceiver;
+import android.os.PersistableBundle;
 
 import com.android.adservices.ondevicepersonalization.flags.Flags;
 import com.android.ondevicepersonalization.internal.util.AnnotationValidations;
@@ -28,11 +34,9 @@
 import java.util.List;
 
 /**
- * The result returned by
- * {@link IsolatedWorker#onExecute(ExecuteInput, android.os.OutcomeReceiver)} in response to a call to
- * {@code OnDevicePersonalizationManager#execute(ComponentName, PersistableBundle,
- * java.util.concurrent.Executor, OutcomeReceiver)}
- * from a client app.
+ * The result returned by {@link IsolatedWorker#onExecute(ExecuteInput, android.os.OutcomeReceiver)}
+ * in response to a call to {@code OnDevicePersonalizationManager#execute(ComponentName,
+ * PersistableBundle, java.util.concurrent.Executor, OutcomeReceiver)} from a client app.
  */
 @FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED)
 @DataClass(genBuilder = true, genEqualsHashCode = true)
@@ -64,18 +68,25 @@
     @NonNull private List<EventLogRecord> mEventLogRecords = Collections.emptyList();
 
     /**
-     * A byte array that an {@link IsolatedService} may optionally return to to a calling app,
-     * by setting this field to a non-null value.
-     * The contents of this array will be returned to the caller of
-     * {@link OnDevicePersonalizationManager#execute(ComponentName, PersistableBundle, java.util.concurrent.Executor, OutcomeReceiver)}
-     * if returning data from isolated processes is allowed by policy and the
-     * (calling app package, isolated service package) pair is present in an allowlist that
-     * permits data to be returned.
+     * A byte array that an {@link IsolatedService} may optionally return to a calling app, by
+     * setting this field to a non-null value. The contents of this array will be returned to the
+     * caller of {@link OnDevicePersonalizationManager#execute} if returning data from isolated
+     * processes is allowed by policy and the (calling app package, isolated service package) pair
+     * is present in an allowlist that permits data to be returned.
      */
-    @DataClass.MaySetToNull
-    @Nullable private byte[] mOutputData = null;
+    @DataClass.MaySetToNull @Nullable private byte[] mOutputData = null;
 
-
+    /**
+     * An integer value that an {@link IsolatedService} may optionally return to a calling app, by
+     * setting this field to the value between 0 and {@link
+     * ExecuteInIsolatedServiceRequest.OutputSpec#getMaxIntValue()}. The noise will be added to the
+     * value of this field before returned to the caller of {@link
+     * OnDevicePersonalizationManager#executeInIsolatedService}. In order to get this field, the
+     * (calling app package, isolated service package) pair must be present in an allowlist that
+     * permits data to be returned and {@link
+     * ExecuteInIsolatedServiceRequest.OutputSpec#buildBestValueSpec} is set.
+     */
+    private final int mBestValue;
 
     // Code below generated by codegen v1.0.23.
     //
@@ -95,13 +106,15 @@
             @Nullable RequestLogRecord requestLogRecord,
             @Nullable RenderingConfig renderingConfig,
             @NonNull List<EventLogRecord> eventLogRecords,
-            @Nullable byte[] outputData) {
+            @Nullable byte[] outputData,
+            int bestValue) {
         this.mRequestLogRecord = requestLogRecord;
         this.mRenderingConfig = renderingConfig;
         this.mEventLogRecords = eventLogRecords;
         AnnotationValidations.validate(
                 NonNull.class, null, mEventLogRecords);
         this.mOutputData = outputData;
+        this.mBestValue = bestValue;
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -139,19 +152,33 @@
     }
 
     /**
-     * A byte array that an {@link IsolatedService} may optionally return to to a calling app,
-     * by setting this field to a non-null value.
-     * The contents of this array will be returned to the caller of
-     * {@link OnDevicePersonalizationManager#execute(ComponentName, PersistableBundle, java.util.concurrent.Executor, OutcomeReceiver)}
-     * if returning data from isolated processes is allowed by policy and the
-     * (calling app package, isolated service package) pair is present in an allowlist that
-     * permits data to be returned.
+     * A byte array that an {@link IsolatedService} may optionally return to a calling app, by
+     * setting this field to a non-null value. The contents of this array will be returned to the
+     * caller of {@link OnDevicePersonalizationManager#execute} if returning data from isolated
+     * processes is allowed by policy and the (calling app package, isolated service package) pair
+     * is present in an allowlist that permits data to be returned.
      */
     @DataClass.Generated.Member
     public @Nullable byte[] getOutputData() {
         return mOutputData;
     }
 
+    /**
+     * An integer value that an {@link IsolatedService} may optionally return to a calling app, by
+     * setting this field to the value between 0 and {@link
+     * ExecuteInIsolatedServiceRequest.OutputSpec#getMaxIntValue()}. The noise will be added to the
+     * value of this field before returned to the caller of {@link
+     * OnDevicePersonalizationManager#executeInIsolatedService}. In order to get this field, the
+     * (calling app package, isolated service package) pair must be present in an allowlist that
+     * permits data to be returned and {@link
+     * ExecuteInIsolatedServiceRequest.OutputSpec#buildBestValueSpec} is set.
+     */
+    @FlaggedApi(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    @DataClass.Generated.Member
+    public @IntRange(from = DEFAULT_BEST_VALUE) int getBestValue() {
+        return mBestValue;
+    }
+
     @Override
     @DataClass.Generated.Member
     public boolean equals(@Nullable Object o) {
@@ -168,7 +195,8 @@
                 && java.util.Objects.equals(mRequestLogRecord, that.mRequestLogRecord)
                 && java.util.Objects.equals(mRenderingConfig, that.mRenderingConfig)
                 && java.util.Objects.equals(mEventLogRecords, that.mEventLogRecords)
-                && java.util.Arrays.equals(mOutputData, that.mOutputData);
+                && java.util.Arrays.equals(mOutputData, that.mOutputData)
+                && mBestValue == that.mBestValue;
     }
 
     @Override
@@ -182,6 +210,7 @@
         _hash = 31 * _hash + java.util.Objects.hashCode(mRenderingConfig);
         _hash = 31 * _hash + java.util.Objects.hashCode(mEventLogRecords);
         _hash = 31 * _hash + java.util.Arrays.hashCode(mOutputData);
+        _hash = 31 * _hash + mBestValue;
         return _hash;
     }
 
@@ -196,6 +225,7 @@
         private @Nullable RenderingConfig mRenderingConfig;
         private @NonNull List<EventLogRecord> mEventLogRecords;
         private @Nullable byte[] mOutputData;
+        private int mBestValue = -1;
 
         private long mBuilderFieldsSet = 0L;
 
@@ -209,19 +239,17 @@
          */
         @DataClass.Generated.Member
         public @NonNull Builder setRequestLogRecord(@Nullable RequestLogRecord value) {
-            checkNotUsed();
             mBuilderFieldsSet |= 0x1;
             mRequestLogRecord = value;
             return this;
         }
 
         /**
-         * A {@link RenderingConfig} object that contains information about the content to be rendered
-         * in the client app view. Can be null if no content is to be rendered.
+         * A {@link RenderingConfig} object that contains information about the content to be
+         * rendered in the client app view. Can be null if no content is to be rendered.
          */
         @DataClass.Generated.Member
         public @NonNull Builder setRenderingConfig(@Nullable RenderingConfig value) {
-            checkNotUsed();
             mBuilderFieldsSet |= 0x2;
             mRenderingConfig = value;
             return this;
@@ -237,7 +265,6 @@
          */
         @DataClass.Generated.Member
         public @NonNull Builder setEventLogRecords(@NonNull List<EventLogRecord> value) {
-            checkNotUsed();
             mBuilderFieldsSet |= 0x4;
             mEventLogRecords = value;
             return this;
@@ -252,26 +279,42 @@
         }
 
         /**
-         * A byte array that an {@link IsolatedService} may optionally return to to a calling app,
-         * by setting this field to a non-null value.
-         * The contents of this array will be returned to the caller of
-         * {@link OnDevicePersonalizationManager#execute(ComponentName, PersistableBundle, java.util.concurrent.Executor, OutcomeReceiver)}
-         * if returning data from isolated processes is allowed by policy and the
-         * (calling app package, isolated service package) pair is present in an allowlist that
-         * permits data to be returned.
+         * A byte array that an {@link IsolatedService} may optionally return to a calling app, by
+         * setting this field to a non-null value. The contents of this array will be returned to
+         * the caller of {@link OnDevicePersonalizationManager#execute(ComponentName,
+         * PersistableBundle, java.util.concurrent.Executor, OutcomeReceiver)} if returning data
+         * from isolated processes is allowed by policy and the (calling app package, isolated
+         * service package) pair is present in an allowlist that permits data to be returned.
          */
         @DataClass.Generated.Member
         public @NonNull Builder setOutputData(@Nullable byte... value) {
-            checkNotUsed();
             mBuilderFieldsSet |= 0x8;
             mOutputData = value;
             return this;
         }
 
+        /**
+         * An integer value that an {@link IsolatedService} may optionally return to a calling app,
+         * by setting this field to the value between 0 and {@link
+         * ExecuteInIsolatedServiceRequest.OutputSpec#getMaxIntValue()}. The noise will be added to
+         * the value of this field before returned to the caller of {@link
+         * OnDevicePersonalizationManager#executeInIsolatedService}. In order to get this field, the
+         * (calling app package, isolated service package) pair must be present in an allowlist that
+         * permits data to be returned and {@link
+         * ExecuteInIsolatedServiceRequest.OutputSpec#buildBestValueSpec} is set.
+         */
+        @FlaggedApi(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+        @DataClass.Generated.Member
+        public @NonNull Builder setBestValue(@IntRange(from = 0) int value) {
+            AnnotationValidations.validate(IntRange.class, null, value, "from", 0);
+            mBuilderFieldsSet |= 0x10;
+            mBestValue = value;
+            return this;
+        }
+
         /** Builds the instance. This builder should not be touched after calling this! */
         public @NonNull ExecuteOutput build() {
-            checkNotUsed();
-            mBuilderFieldsSet |= 0x10; // Mark builder used
+            mBuilderFieldsSet |= 0x20; // Mark builder used
 
             if ((mBuilderFieldsSet & 0x1) == 0) {
                 mRequestLogRecord = null;
@@ -285,27 +328,24 @@
             if ((mBuilderFieldsSet & 0x8) == 0) {
                 mOutputData = null;
             }
+            if ((mBuilderFieldsSet & 0x10) == 0) {
+                mBestValue = -1;
+            }
             ExecuteOutput o = new ExecuteOutput(
                     mRequestLogRecord,
                     mRenderingConfig,
                     mEventLogRecords,
-                    mOutputData);
+                    mOutputData,
+                    mBestValue);
             return o;
         }
-
-        private void checkNotUsed() {
-            if ((mBuilderFieldsSet & 0x10) != 0) {
-                throw new IllegalStateException(
-                        "This Builder should not be reused. Use a new Builder instance instead");
-            }
-        }
     }
 
     @DataClass.Generated(
-            time = 1707251143585L,
+            time = 1721951665662L,
             codegenVersion = "1.0.23",
             sourceFile = "packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/ExecuteOutput.java",
-            inputSignatures = "private @com.android.ondevicepersonalization.internal.util.DataClass.MaySetToNull @android.annotation.Nullable android.adservices.ondevicepersonalization.RequestLogRecord mRequestLogRecord\nprivate @com.android.ondevicepersonalization.internal.util.DataClass.MaySetToNull @android.annotation.Nullable android.adservices.ondevicepersonalization.RenderingConfig mRenderingConfig\nprivate @com.android.ondevicepersonalization.internal.util.DataClass.PluralOf(\"eventLogRecord\") @android.annotation.NonNull java.util.List<android.adservices.ondevicepersonalization.EventLogRecord> mEventLogRecords\nprivate @com.android.ondevicepersonalization.internal.util.DataClass.MaySetToNull @android.annotation.Nullable byte[] mOutputData\nclass ExecuteOutput extends java.lang.Object implements []\[email protected](genBuilder=true, genEqualsHashCode=true)")
+            inputSignatures = "private @com.android.ondevicepersonalization.internal.util.DataClass.MaySetToNull @android.annotation.Nullable android.adservices.ondevicepersonalization.RequestLogRecord mRequestLogRecord\nprivate @com.android.ondevicepersonalization.internal.util.DataClass.MaySetToNull @android.annotation.Nullable android.adservices.ondevicepersonalization.RenderingConfig mRenderingConfig\nprivate @com.android.ondevicepersonalization.internal.util.DataClass.PluralOf(\"eventLogRecord\") @android.annotation.NonNull java.util.List<android.adservices.ondevicepersonalization.EventLogRecord> mEventLogRecords\nprivate @com.android.ondevicepersonalization.internal.util.DataClass.MaySetToNull @android.annotation.Nullable byte[] mOutputData\nprivate  int mBestValue\nclass ExecuteOutput extends java.lang.Object implements []\[email protected](genBuilder=true, genEqualsHashCode=true)")
     @Deprecated
     private void __metadata() {}
 
diff --git a/framework/java/android/adservices/ondevicepersonalization/ExecuteOutputParcel.java b/framework/java/android/adservices/ondevicepersonalization/ExecuteOutputParcel.java
index cf797ba..394f2b1 100644
--- a/framework/java/android/adservices/ondevicepersonalization/ExecuteOutputParcel.java
+++ b/framework/java/android/adservices/ondevicepersonalization/ExecuteOutputParcel.java
@@ -67,62 +67,83 @@
      */
     @Nullable private byte[] mOutputData = null;
 
+    /**
+     * An integer value that an {@link IsolatedService} may optionally return to to a calling app,
+     * by setting this field to the value between 0 and max value in {@link
+     * ExecuteInIsolatedServiceRequest.Options}. The value of this field will be returned to the
+     * caller of {@link OnDevicePersonalizationManager#executeInIsolatedService} if returning data
+     * from isolated processes is allowed by policy and the (calling app package, isolated service
+     * package) pair is present in an allowlist that permits data to be returned.
+     *
+     * @hide
+     */
+    private int mBestValue = -1;
+
     /** @hide */
     public ExecuteOutputParcel(@NonNull ExecuteOutput value) {
-        this(value.getRequestLogRecord(), value.getRenderingConfig(), value.getEventLogRecords(),
-                value.getOutputData());
+        this(
+                value.getRequestLogRecord(),
+                value.getRenderingConfig(),
+                value.getEventLogRecords(),
+                value.getOutputData(),
+                value.getBestValue());
     }
 
-
-
     // Code below generated by codegen v1.0.23.
     //
     // DO NOT MODIFY!
     // CHECKSTYLE:OFF Generated code
     //
     // To regenerate run:
-    // $ codegen $ANDROID_BUILD_TOP/packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/ExecuteOutputParcel.java
+    // $ codegen
+    // $ANDROID_BUILD_TOP/packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/ExecuteOutputParcel.java
     //
     // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
     //   Settings > Editor > Code Style > Formatter Control
-    //@formatter:off
-
+    // @formatter:off
 
     /**
      * Creates a new ExecuteOutputParcel.
      *
-     * @param requestLogRecord
-     *   Persistent data to be written to the REQUESTS table after
-     *   {@link IsolatedWorker#onExecute(ExecuteInput, android.os.OutcomeReceiver)}
-     *   completes. If null, no persistent data will be written.
-     * @param renderingConfig
-     *   A {@link RenderingConfig} object that contains information about the content to be rendered
-     *   in the client app view. Can be null if no content is to be rendered.
-     * @param eventLogRecords
-     *   A list of {@link EventLogRecord}. Writes events to the EVENTS table and associates
-     *   them with requests with the specified corresponding {@link RequestLogRecord} from
-     *   {@link EventLogRecord#getRequestLogRecord()}.
-     *   If the event does not contain a {@link RequestLogRecord} emitted by this package, the
-     *   EventLogRecord is not written.
-     * @param outputData
-     *   A byte array returned by an {@link IsolatedService} to a calling app. The contents of
-     *   this array is returned to the caller of
-     *   {@link OnDevicePersonalizationManager#execute(ComponentName, PersistableBundle, java.util.concurrent.Executor, OutcomeReceiver)}
-     *   if the (calling app package, isolated service package) pair is present in an allow list
-     *   that permits data to be returned to the caller.
+     * @param requestLogRecord Persistent data to be written to the REQUESTS table after {@link
+     *     IsolatedWorker#onExecute(ExecuteInput, android.os.OutcomeReceiver)} completes. If null,
+     *     no persistent data will be written.
+     * @param renderingConfig A {@link RenderingConfig} object that contains information about the
+     *     content to be rendered in the client app view. Can be null if no content is to be
+     *     rendered.
+     * @param eventLogRecords A list of {@link EventLogRecord}. Writes events to the EVENTS table
+     *     and associates them with requests with the specified corresponding {@link
+     *     RequestLogRecord} from {@link EventLogRecord#getRequestLogRecord()}. If the event does
+     *     not contain a {@link RequestLogRecord} emitted by this package, the EventLogRecord is not
+     *     written.
+     * @param outputData A byte array returned by an {@link IsolatedService} to a calling app. The
+     *     contents of this array is returned to the caller of {@link
+     *     OnDevicePersonalizationManager#execute(ComponentName, PersistableBundle,
+     *     java.util.concurrent.Executor, OutcomeReceiver)} if the (calling app package, isolated
+     *     service package) pair is present in an allow list that permits data to be returned to the
+     *     caller.
+     * @param bestValue An integer value that an {@link IsolatedService} may optionally return to to
+     *     a calling app, by setting this field to the value between 0 and max value in {@link
+     *     ExecuteInIsolatedServiceRequest.OutputSpec}. The value of this field will be returned to
+     *     the caller of {@link OnDevicePersonalizationManager#executeInIsolatedService} if
+     *     returning data from isolated processes is allowed by policy and the (calling app package,
+     *     isolated service package) pair is present in an allowlist that permits data to be
+     *     returned.
      */
     @DataClass.Generated.Member
     public ExecuteOutputParcel(
             @Nullable RequestLogRecord requestLogRecord,
             @Nullable RenderingConfig renderingConfig,
             @NonNull List<EventLogRecord> eventLogRecords,
-            @Nullable byte[] outputData) {
+            @Nullable byte[] outputData,
+            int bestValue) {
         this.mRequestLogRecord = requestLogRecord;
         this.mRenderingConfig = renderingConfig;
         this.mEventLogRecords = eventLogRecords;
         AnnotationValidations.validate(
                 NonNull.class, null, mEventLogRecords);
         this.mOutputData = outputData;
+        this.mBestValue = bestValue;
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -172,6 +193,21 @@
         return mOutputData;
     }
 
+    /**
+     * An integer value that an {@link IsolatedService} may optionally return to to a calling app,
+     * by setting this field to the value between 0 and max value in {@link
+     * ExecuteInIsolatedServiceRequest.Options}. The value of this field will be returned to the
+     * caller of {@link OnDevicePersonalizationManager#executeInIsolatedService} if returning data
+     * from isolated processes is allowed by policy and the (calling app package, isolated service
+     * package) pair is present in an allowlist that permits data to be returned.
+     *
+     * @hide
+     */
+    @DataClass.Generated.Member
+    public int getBestValue() {
+        return mBestValue;
+    }
+
     @Override
     @DataClass.Generated.Member
     public void writeToParcel(@NonNull android.os.Parcel dest, int flags) {
@@ -186,6 +222,7 @@
         if (mRenderingConfig != null) dest.writeTypedObject(mRenderingConfig, flags);
         dest.writeParcelableList(mEventLogRecords, flags);
         dest.writeByteArray(mOutputData);
+        dest.writeInt(mBestValue);
     }
 
     @Override
@@ -205,6 +242,7 @@
         List<EventLogRecord> eventLogRecords = new java.util.ArrayList<>();
         in.readParcelableList(eventLogRecords, EventLogRecord.class.getClassLoader());
         byte[] outputData = in.createByteArray();
+        int bestValue = in.readInt();
 
         this.mRequestLogRecord = requestLogRecord;
         this.mRenderingConfig = renderingConfig;
@@ -212,6 +250,7 @@
         AnnotationValidations.validate(
                 NonNull.class, null, mEventLogRecords);
         this.mOutputData = outputData;
+        this.mBestValue = bestValue;
 
         // onConstructed(); // You can define this method to get a callback
     }
@@ -231,14 +270,15 @@
     };
 
     @DataClass.Generated(
-            time = 1706684633171L,
+            time = 1721773162236L,
             codegenVersion = "1.0.23",
-            sourceFile = "packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/ExecuteOutputParcel.java",
-            inputSignatures = "private @android.annotation.Nullable android.adservices.ondevicepersonalization.RequestLogRecord mRequestLogRecord\nprivate @android.annotation.Nullable android.adservices.ondevicepersonalization.RenderingConfig mRenderingConfig\nprivate @com.android.ondevicepersonalization.internal.util.DataClass.PluralOf(\"eventLogRecord\") @android.annotation.NonNull java.util.List<android.adservices.ondevicepersonalization.EventLogRecord> mEventLogRecords\nprivate @android.annotation.Nullable byte[] mOutputData\nclass ExecuteOutputParcel extends java.lang.Object implements [android.os.Parcelable]\[email protected](genAidl=false, genBuilder=false)")
+            sourceFile =
+                    "packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/ExecuteOutputParcel.java",
+            inputSignatures =
+                    "private @android.annotation.Nullable android.adservices.ondevicepersonalization.RequestLogRecord mRequestLogRecord\nprivate @android.annotation.Nullable android.adservices.ondevicepersonalization.RenderingConfig mRenderingConfig\nprivate @com.android.ondevicepersonalization.internal.util.DataClass.PluralOf(\"eventLogRecord\") @android.annotation.NonNull java.util.List<android.adservices.ondevicepersonalization.EventLogRecord> mEventLogRecords\nprivate @android.annotation.Nullable byte[] mOutputData\nprivate  int mBestValue\nclass ExecuteOutputParcel extends java.lang.Object implements [android.os.Parcelable]\[email protected](genAidl=false, genBuilder=false)")
     @Deprecated
     private void __metadata() {}
 
-
     //@formatter:on
     // End of generated code
 
diff --git a/framework/java/android/adservices/ondevicepersonalization/FederatedComputeScheduleRequest.java b/framework/java/android/adservices/ondevicepersonalization/FederatedComputeScheduleRequest.java
new file mode 100644
index 0000000..f8282e8
--- /dev/null
+++ b/framework/java/android/adservices/ondevicepersonalization/FederatedComputeScheduleRequest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.
+ */
+
+package android.adservices.ondevicepersonalization;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.adservices.ondevicepersonalization.flags.Flags;
+import com.android.ondevicepersonalization.internal.util.AnnotationValidations;
+import com.android.ondevicepersonalization.internal.util.DataClass;
+
+/**
+ * The input for {@link FederatedComputeScheduler#schedule(FederatedComputeScheduleRequest,
+ * android.os.OutcomeReceiver)}.
+ */
+@DataClass(genEqualsHashCode = true)
+@FlaggedApi(Flags.FLAG_FCP_SCHEDULE_WITH_OUTCOME_RECEIVER_ENABLED)
+public final class FederatedComputeScheduleRequest {
+    /** Parameters related to job scheduling. */
+    @NonNull private FederatedComputeScheduler.Params mParams;
+
+    /**
+     * Population refers to a collection of devices that specific task groups can run on. It should
+     * match task plan configured at remote federated compute server.
+     */
+    @NonNull private String mPopulationName;
+
+    // Code below generated by codegen v1.0.23.
+    //
+    // DO NOT MODIFY!
+    // CHECKSTYLE:OFF Generated code
+    //
+    // To regenerate run:
+    // $ codegen
+    // $ANDROID_BUILD_TOP/packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/FederatedComputeScheduleRequest.java
+    //
+    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+    //   Settings > Editor > Code Style > Formatter Control
+    // @formatter:off
+
+    @DataClass.Generated.Member
+    public FederatedComputeScheduleRequest(
+            @NonNull FederatedComputeScheduler.Params params, @NonNull String populationName) {
+        this.mParams = params;
+        AnnotationValidations.validate(NonNull.class, null, mParams);
+        this.mPopulationName = populationName;
+        AnnotationValidations.validate(NonNull.class, null, mPopulationName);
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    /** Parameters related to job scheduling. */
+    @DataClass.Generated.Member
+    public @NonNull FederatedComputeScheduler.Params getParams() {
+        return mParams;
+    }
+
+    /**
+     * Population refers to a collection of devices that specific task groups can run on. It should
+     * match task plan configured at remote federated compute server.
+     */
+    @DataClass.Generated.Member
+    public @NonNull String getPopulationName() {
+        return mPopulationName;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public boolean equals(@Nullable Object o) {
+        // You can override field equality logic by defining either of the methods like:
+        // boolean fieldNameEquals(FederatedComputeScheduleRequest other) { ... }
+        // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        @SuppressWarnings("unchecked")
+        FederatedComputeScheduleRequest that = (FederatedComputeScheduleRequest) o;
+        //noinspection PointlessBooleanExpression
+        return true
+                && java.util.Objects.equals(mParams, that.mParams)
+                && java.util.Objects.equals(mPopulationName, that.mPopulationName);
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int hashCode() {
+        // You can override field hashCode logic by defining methods like:
+        // int fieldNameHashCode() { ... }
+
+        int _hash = 1;
+        _hash = 31 * _hash + java.util.Objects.hashCode(mParams);
+        _hash = 31 * _hash + java.util.Objects.hashCode(mPopulationName);
+        return _hash;
+    }
+
+    @DataClass.Generated(
+            time = 1724192543514L,
+            codegenVersion = "1.0.23",
+            sourceFile =
+                    "packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/FederatedComputeScheduleRequest.java",
+            inputSignatures =
+                    "private @android.annotation.NonNull"
+                        + " android.adservices.ondevicepersonalization.FederatedComputeScheduler.Params"
+                        + " mParams\n"
+                        + "private @android.annotation.NonNull java.lang.String mPopulationName\n"
+                        + "class FederatedComputeScheduleRequest extends java.lang.Object"
+                        + " implements []\n"
+                        + "@com.android.ondevicepersonalization.internal.util.DataClass(genEqualsHashCode=true)")
+    @Deprecated
+    private void __metadata() {}
+
+    // @formatter:on
+    // End of generated code
+}
diff --git a/framework/java/android/adservices/ondevicepersonalization/FederatedComputeScheduleResponse.java b/framework/java/android/adservices/ondevicepersonalization/FederatedComputeScheduleResponse.java
new file mode 100644
index 0000000..ae905ce
--- /dev/null
+++ b/framework/java/android/adservices/ondevicepersonalization/FederatedComputeScheduleResponse.java
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+
+package android.adservices.ondevicepersonalization;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+
+import com.android.adservices.ondevicepersonalization.flags.Flags;
+import com.android.ondevicepersonalization.internal.util.AnnotationValidations;
+import com.android.ondevicepersonalization.internal.util.DataClass;
+
+/**
+ * The result returned by {@link FederatedComputeScheduler#schedule(FederatedComputeScheduleRequest,
+ * android.os.OutcomeReceiver)} when successful.
+ */
+@DataClass(genEqualsHashCode = true)
+@FlaggedApi(Flags.FLAG_FCP_SCHEDULE_WITH_OUTCOME_RECEIVER_ENABLED)
+public final class FederatedComputeScheduleResponse {
+
+    @NonNull private FederatedComputeScheduleRequest mFederatedComputeScheduleRequest;
+
+    // Code below generated by codegen v1.0.23.
+    //
+    // DO NOT MODIFY!
+    // CHECKSTYLE:OFF Generated code
+    //
+    // To regenerate run:
+    // $ codegen
+    // $ANDROID_BUILD_TOP/packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/FederatedComputeScheduleResponse.java
+    //
+    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+    //   Settings > Editor > Code Style > Formatter Control
+    // @formatter:off
+
+    @DataClass.Generated.Member
+    public FederatedComputeScheduleResponse(
+            @NonNull FederatedComputeScheduleRequest federatedComputeScheduleRequest) {
+        this.mFederatedComputeScheduleRequest = federatedComputeScheduleRequest;
+        AnnotationValidations.validate(NonNull.class, null, mFederatedComputeScheduleRequest);
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    /** The request associated with this response. */
+    @DataClass.Generated.Member
+    public @NonNull FederatedComputeScheduleRequest getFederatedComputeScheduleRequest() {
+        return mFederatedComputeScheduleRequest;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public boolean equals(@android.annotation.Nullable Object o) {
+        // You can override field equality logic by defining either of the methods like:
+        // boolean fieldNameEquals(FederatedComputeScheduleResponse other) { ... }
+        // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        @SuppressWarnings("unchecked")
+        FederatedComputeScheduleResponse that = (FederatedComputeScheduleResponse) o;
+        //noinspection PointlessBooleanExpression
+        return true
+                && java.util.Objects.equals(
+                        mFederatedComputeScheduleRequest, that.mFederatedComputeScheduleRequest);
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int hashCode() {
+        // You can override field hashCode logic by defining methods like:
+        // int fieldNameHashCode() { ... }
+
+        int _hash = 1;
+        _hash = 31 * _hash + java.util.Objects.hashCode(mFederatedComputeScheduleRequest);
+        return _hash;
+    }
+
+    @DataClass.Generated(
+            time = 1725476292347L,
+            codegenVersion = "1.0.23",
+            sourceFile =
+                    "packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/FederatedComputeScheduleResponse.java",
+            inputSignatures =
+                    "private @android.annotation.NonNull java.lang.String mPopulationName\n"
+                        + "class FederatedComputeScheduleResponse extends java.lang.Object"
+                        + " implements []\n"
+                        + "@com.android.ondevicepersonalization.internal.util.DataClass(genEqualsHashCode=true)")
+    @Deprecated
+    private void __metadata() {}
+
+    // @formatter:on
+    // End of generated code
+
+}
diff --git a/framework/java/android/adservices/ondevicepersonalization/FederatedComputeScheduler.java b/framework/java/android/adservices/ondevicepersonalization/FederatedComputeScheduler.java
index 81e9c69..17bddc2 100644
--- a/framework/java/android/adservices/ondevicepersonalization/FederatedComputeScheduler.java
+++ b/framework/java/android/adservices/ondevicepersonalization/FederatedComputeScheduler.java
@@ -23,12 +23,14 @@
 import android.annotation.NonNull;
 import android.annotation.WorkerThread;
 import android.federatedcompute.common.TrainingOptions;
+import android.os.OutcomeReceiver;
 import android.os.RemoteException;
 
 import com.android.adservices.ondevicepersonalization.flags.Flags;
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Handles scheduling federated compute jobs. See {@link
@@ -37,6 +39,8 @@
 @FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED)
 public class FederatedComputeScheduler {
     private static final String TAG = FederatedComputeScheduler.class.getSimpleName();
+
+    private static final int FEDERATED_COMPUTE_SCHEDULE_TIMEOUT_SECONDS = 30;
     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
 
     private final IFederatedComputeService mFcService;
@@ -53,31 +57,33 @@
     // TODO(b/269665435): add sample code snippet.
     /**
      * Schedules a federated compute job. In {@link IsolatedService#onRequest}, the app can call
-     * {@link IsolatedService#getFederatedComputeScheduler} to pass scheduler when construct {@link
-     * IsolatedWorker}.
+     * {@link IsolatedService#getFederatedComputeScheduler} to pass the scheduler when constructing
+     * the {@link IsolatedWorker}.
      *
      * @param params parameters related to job scheduling.
-     * @param input the configuration of the federated compute. It should be consistent with the
+     * @param input the configuration of the federated computation. It should be consistent with the
      *     federated compute server setup.
      */
     @WorkerThread
     public void schedule(@NonNull Params params, @NonNull FederatedComputeInput input) {
-        final long startTimeMillis = System.currentTimeMillis();
-        int responseCode = Constants.STATUS_INTERNAL_ERROR;
         if (mFcService == null) {
+            logApiCallStats(
+                    Constants.API_NAME_FEDERATED_COMPUTE_SCHEDULE,
+                    0,
+                    Constants.STATUS_INTERNAL_ERROR);
             throw new IllegalStateException(
                     "FederatedComputeScheduler not available for this instance.");
         }
-
-        android.federatedcompute.common.TrainingInterval trainingInterval =
-                convertTrainingInterval(params.getTrainingInterval());
+        final long startTimeMillis = System.currentTimeMillis();
         TrainingOptions trainingOptions =
                 new TrainingOptions.Builder()
                         .setPopulationName(input.getPopulationName())
-                        .setTrainingInterval(trainingInterval)
+                        .setTrainingInterval(convertTrainingInterval(params.getTrainingInterval()))
                         .build();
+
         CountDownLatch latch = new CountDownLatch(1);
         final int[] err = {0};
+        int responseCode = Constants.STATUS_INTERNAL_ERROR;
         try {
             mFcService.schedule(
                     trainingOptions,
@@ -93,16 +99,24 @@
                             latch.countDown();
                         }
                     });
-            latch.await();
+
+            boolean countedDown =
+                    latch.await(FEDERATED_COMPUTE_SCHEDULE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
             if (err[0] != 0) {
-                // Fail silently for now. TODO(b/346827691): update schedule/cancel API to return
-                // error status to caller.
-                sLogger.e("Internal failure occurred while scheduling job, error code %d", err[0]);
-                responseCode = Constants.STATUS_INTERNAL_ERROR;
+                sLogger.e(
+                        TAG + " : Internal failure occurred while scheduling job, error code %d",
+                        err[0]);
+                responseCode = err[0];
+                return;
+            } else if (!countedDown) {
+                sLogger.d(TAG + " : timed out waiting for schedule operation to complete.");
+                responseCode = Constants.STATUS_TIMEOUT;
                 return;
             }
             responseCode = Constants.STATUS_SUCCESS;
         } catch (RemoteException | InterruptedException e) {
+            responseCode = Constants.STATUS_REMOTE_EXCEPTION;
             sLogger.e(TAG + ": Failed to schedule federated compute job", e);
             throw new IllegalStateException(e);
         } finally {
@@ -114,9 +128,96 @@
     }
 
     /**
+     * Schedules a federated compute job. In {@link IsolatedService#onRequest}, the app can call
+     * {@link IsolatedService#getFederatedComputeScheduler} to pass the scheduler when constructing
+     * the {@link IsolatedWorker}.
+     *
+     * @param federatedComputeScheduleRequest input parameters related to job scheduling.
+     * @param outcomeReceiver This either returns a {@link FederatedComputeScheduleResponse} on
+     *     success, or {@link Exception} on failure. The exception type is {@link
+     *     OnDevicePersonalizationException} with error code {@link
+     *     OnDevicePersonalizationException#ERROR_INVALID_TRAINING_MANIFEST} if the manifest is
+     *     missing the federated compute server URL or {@link
+     *     OnDevicePersonalizationException#ERROR_SCHEDULE_TRAINING_FAILED} when scheduling fails
+     *     for other reasons.
+     */
+    @WorkerThread
+    @FlaggedApi(Flags.FLAG_FCP_SCHEDULE_WITH_OUTCOME_RECEIVER_ENABLED)
+    public void schedule(
+            @NonNull FederatedComputeScheduleRequest federatedComputeScheduleRequest,
+            @NonNull OutcomeReceiver<FederatedComputeScheduleResponse, Exception> outcomeReceiver) {
+        if (mFcService == null) {
+            logApiCallStats(
+                    Constants.API_NAME_FEDERATED_COMPUTE_SCHEDULE,
+                    0,
+                    Constants.STATUS_INTERNAL_ERROR);
+            outcomeReceiver.onError(
+                    new IllegalStateException(
+                            "FederatedComputeScheduler not available for this instance."));
+        }
+
+        final long startTimeMillis = System.currentTimeMillis();
+        TrainingOptions trainingOptions =
+                new TrainingOptions.Builder()
+                        .setPopulationName(federatedComputeScheduleRequest.getPopulationName())
+                        .setTrainingInterval(
+                                convertTrainingInterval(
+                                        federatedComputeScheduleRequest
+                                                .getParams()
+                                                .getTrainingInterval()))
+                        .build();
+        try {
+            mFcService.schedule(
+                    trainingOptions,
+                    new IFederatedComputeCallback.Stub() {
+                        @Override
+                        public void onSuccess() {
+                            logApiCallStats(
+                                    Constants.API_NAME_FEDERATED_COMPUTE_SCHEDULE,
+                                    System.currentTimeMillis() - startTimeMillis,
+                                    Constants.STATUS_SUCCESS);
+                            outcomeReceiver.onResult(
+                                    new FederatedComputeScheduleResponse(
+                                            federatedComputeScheduleRequest));
+                        }
+
+                        @Override
+                        public void onFailure(int errorCode) {
+                            logApiCallStats(
+                                    Constants.API_NAME_FEDERATED_COMPUTE_SCHEDULE,
+                                    System.currentTimeMillis() - startTimeMillis,
+                                    errorCode);
+                            outcomeReceiver.onError(
+                                    new OnDevicePersonalizationException(
+                                            translateErrorCode(errorCode)));
+                        }
+                    });
+        } catch (RemoteException e) {
+            sLogger.e(TAG + ": Failed to schedule federated compute job", e);
+            logApiCallStats(
+                    Constants.API_NAME_FEDERATED_COMPUTE_SCHEDULE,
+                    System.currentTimeMillis() - startTimeMillis,
+                    Constants.STATUS_REMOTE_EXCEPTION);
+            outcomeReceiver.onError(e);
+        }
+    }
+
+    /**
+     * Translate the failed error code from the {@link IFederatedComputeService} to appropriate API
+     * surface error code.
+     */
+    private static int translateErrorCode(int i) {
+        // Returns invalid/missing manifest or general error code to caller. The general error code
+        // includes personalization disable and all other errors populated from FCP service.
+        return i == Constants.STATUS_FCP_MANIFEST_INVALID
+                ? OnDevicePersonalizationException.ERROR_INVALID_TRAINING_MANIFEST
+                : OnDevicePersonalizationException.ERROR_SCHEDULE_TRAINING_FAILED;
+    }
+
+    /**
      * Cancels a federated compute job with input training params. In {@link
      * IsolatedService#onRequest}, the app can call {@link
-     * IsolatedService#getFederatedComputeScheduler} to pass scheduler when construct {@link
+     * IsolatedService#getFederatedComputeScheduler} to pass scheduler when constructing the {@link
      * IsolatedWorker}.
      *
      * @param input the configuration of the federated compute. It should be consistent with the
@@ -127,6 +228,10 @@
         final long startTimeMillis = System.currentTimeMillis();
         int responseCode = Constants.STATUS_INTERNAL_ERROR;
         if (mFcService == null) {
+            logApiCallStats(
+                    Constants.API_NAME_FEDERATED_COMPUTE_CANCEL,
+                    System.currentTimeMillis() - startTimeMillis,
+                    responseCode);
             throw new IllegalStateException(
                     "FederatedComputeScheduler not available for this instance.");
         }
@@ -142,18 +247,23 @@
                         }
 
                         @Override
-                        public void onFailure(int i) {
-                            err[0] = i;
+                        public void onFailure(int errorCode) {
+                            err[0] = errorCode;
                             latch.countDown();
                         }
                     });
-            latch.await();
+            boolean countedDown =
+                    latch.await(FEDERATED_COMPUTE_SCHEDULE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
             if (err[0] != 0) {
                 sLogger.e("Internal failure occurred while cancelling job, error code %d", err[0]);
                 responseCode = Constants.STATUS_INTERNAL_ERROR;
                 // Fail silently for now. TODO(b/346827691): update schedule/cancel API to return
                 // error status to caller.
                 return;
+            } else if (!countedDown) {
+                sLogger.d(TAG + " : timed out waiting for cancel operation to complete.");
+                responseCode = Constants.STATUS_INTERNAL_ERROR;
+                return;
             }
             responseCode = Constants.STATUS_SUCCESS;
         } catch (RemoteException | InterruptedException e) {
@@ -167,7 +277,7 @@
         }
     }
 
-    private android.federatedcompute.common.TrainingInterval convertTrainingInterval(
+    private static android.federatedcompute.common.TrainingInterval convertTrainingInterval(
             TrainingInterval interval) {
         return new android.federatedcompute.common.TrainingInterval.Builder()
                 .setMinimumIntervalMillis(interval.getMinimumInterval().toMillis())
@@ -175,7 +285,7 @@
                 .build();
     }
 
-    private @android.federatedcompute.common.TrainingInterval.SchedulingMode int
+    private static @android.federatedcompute.common.TrainingInterval.SchedulingMode int
             convertSchedulingMode(TrainingInterval interval) {
         switch (interval.getSchedulingMode()) {
             case TrainingInterval.SCHEDULING_MODE_ONE_TIME:
@@ -188,6 +298,7 @@
         }
     }
 
+    /** Helper method to log call stats based on response code. */
     private void logApiCallStats(int apiName, long duration, int responseCode) {
         try {
             mDataAccessService.logApiCallStats(apiName, duration, responseCode);
diff --git a/framework/java/android/adservices/ondevicepersonalization/IsolatedService.java b/framework/java/android/adservices/ondevicepersonalization/IsolatedService.java
index 13cc7c2..47a45f5 100644
--- a/framework/java/android/adservices/ondevicepersonalization/IsolatedService.java
+++ b/framework/java/android/adservices/ondevicepersonalization/IsolatedService.java
@@ -35,6 +35,7 @@
 import android.os.SystemClock;
 
 import com.android.adservices.ondevicepersonalization.flags.Flags;
+import com.android.ondevicepersonalization.internal.util.ExceptionInfo;
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 import com.android.ondevicepersonalization.internal.util.OdpParceledListSlice;
 
@@ -59,6 +60,7 @@
 public abstract class IsolatedService extends Service {
     private static final String TAG = IsolatedService.class.getSimpleName();
     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
+    private static final int MAX_EXCEPTION_CHAIN_DEPTH = 3;
     private IBinder mBinder;
 
     /** Creates a binder for an {@link IsolatedService}. */
@@ -272,11 +274,7 @@
                                 resultCallback, requestToken, v -> new WebTriggerOutputParcel(v)));
             } catch (Exception e) {
                 sLogger.e(e, TAG + ": Exception during Isolated Service web trigger operation.");
-                try {
-                    resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
-                } catch (RemoteException re) {
-                    sLogger.e(re, TAG + ": Isolated Service Callback failed.");
-                }
+                sendError(resultCallback, Constants.STATUS_INTERNAL_ERROR, e);
             }
         }
 
@@ -311,11 +309,7 @@
             } catch (Exception e) {
                 sLogger.e(e,
                         TAG + ": Exception during Isolated Service training example operation.");
-                try {
-                    resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
-                } catch (RemoteException re) {
-                    sLogger.e(re, TAG + ": Isolated Service Callback failed.");
-                }
+                sendError(resultCallback, Constants.STATUS_INTERNAL_ERROR, e);
             }
         }
 
@@ -340,11 +334,7 @@
                                 resultCallback, requestToken, v -> new EventOutputParcel(v)));
             } catch (Exception e) {
                 sLogger.e(e, TAG + ": Exception during Isolated Service web view event operation.");
-                try {
-                    resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
-                } catch (RemoteException re) {
-                    sLogger.e(re, TAG + ": Isolated Service Callback failed.");
-                }
+                sendError(resultCallback, Constants.STATUS_INTERNAL_ERROR, e);
             }
         }
 
@@ -370,11 +360,7 @@
                                 resultCallback, requestToken, v -> new RenderOutputParcel(v)));
             } catch (Exception e) {
                 sLogger.e(e, TAG + ": Exception during Isolated Service render operation.");
-                try {
-                    resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
-                } catch (RemoteException re) {
-                    sLogger.e(re, TAG + ": Isolated Service Callback failed.");
-                }
+                sendError(resultCallback, Constants.STATUS_INTERNAL_ERROR, e);
             }
         }
 
@@ -397,9 +383,7 @@
                             "Failed to get IDataAccessService binder from the input params!")));
 
                 DownloadCompletedInput input =
-                        new DownloadCompletedInput.Builder()
-                                .setDownloadedContents(downloadedContents)
-                                .build();
+                        new DownloadCompletedInput(downloadedContents);
 
                 IDataAccessService binder = getDataAccessService(params);
 
@@ -415,11 +399,7 @@
                                 v -> new DownloadCompletedOutputParcel(v)));
             } catch (Exception e) {
                 sLogger.e(e, TAG + ": Exception during Isolated Service download operation.");
-                try {
-                    resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
-                } catch (RemoteException re) {
-                    sLogger.e(re, TAG + ": Isolated Service Callback failed.");
-                }
+                sendError(resultCallback, Constants.STATUS_INTERNAL_ERROR, e);
             }
         }
 
@@ -497,15 +477,20 @@
                                 resultCallback, requestToken, v -> new ExecuteOutputParcel(v)));
             } catch (Exception e) {
                 sLogger.e(e, TAG + ": Exception during Isolated Service execute operation.");
-                try {
-                    resultCallback.onError(Constants.STATUS_INTERNAL_ERROR, 0);
-                } catch (RemoteException re) {
-                    sLogger.e(re, TAG + ": Isolated Service Callback failed.");
-                }
+                sendError(resultCallback, Constants.STATUS_INTERNAL_ERROR, e);
             }
         }
     }
 
+    private void sendError(IIsolatedServiceCallback resultCallback, int errorCode, Throwable t) {
+        try {
+            resultCallback.onError(
+                    errorCode, 0, ExceptionInfo.toByteArray(t, MAX_EXCEPTION_CHAIN_DEPTH));
+        } catch (RemoteException re) {
+            sLogger.e(re, TAG + ": Isolated Service Callback failed.");
+        }
+    }
+
     private static class WrappedCallback<T, U extends Parcelable>
                 implements OutcomeReceiver<T, IsolatedServiceException> {
         @NonNull private final IIsolatedServiceCallback mCallback;
@@ -526,11 +511,7 @@
             long elapsedTimeMillis =
                     SystemClock.elapsedRealtime() - mRequestToken.getStartTimeMillis();
             if (result == null) {
-                try {
-                    mCallback.onError(Constants.STATUS_SERVICE_FAILED, 0);
-                } catch (RemoteException e) {
-                    sLogger.w(TAG + ": Callback failed.", e);
-                }
+                sendError(0, new IllegalArgumentException("missing result"));
             } else {
                 Bundle bundle = new Bundle();
                 U wrappedResult = mConverter.apply(result);
@@ -549,9 +530,16 @@
 
         @Override
         public void onError(IsolatedServiceException e) {
+            sendError(e.getErrorCode(), e);
+        }
+
+        private void sendError(int isolatedServiceErrorCode, Throwable t) {
             try {
                 // TODO(b/324478256): Log and report the error code from e.
-                mCallback.onError(Constants.STATUS_SERVICE_FAILED, e.getErrorCode());
+                mCallback.onError(
+                        Constants.STATUS_SERVICE_FAILED,
+                        isolatedServiceErrorCode,
+                        ExceptionInfo.toByteArray(t, MAX_EXCEPTION_CHAIN_DEPTH));
             } catch (RemoteException re) {
                 sLogger.w(TAG + ": Callback failed.", re);
             }
diff --git a/framework/java/android/adservices/ondevicepersonalization/IsolatedServiceException.java b/framework/java/android/adservices/ondevicepersonalization/IsolatedServiceException.java
index b280b10..03e9544 100644
--- a/framework/java/android/adservices/ondevicepersonalization/IsolatedServiceException.java
+++ b/framework/java/android/adservices/ondevicepersonalization/IsolatedServiceException.java
@@ -17,7 +17,7 @@
 package android.adservices.ondevicepersonalization;
 
 import android.annotation.FlaggedApi;
-import android.annotation.IntRange;
+import android.annotation.Nullable;
 
 import com.android.adservices.ondevicepersonalization.flags.Flags;
 
@@ -29,7 +29,7 @@
  */
 @FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED)
 public final class IsolatedServiceException extends Exception {
-    @IntRange(from = 1, to = 127) private final int mErrorCode;
+    private final int mErrorCode;
 
     /**
      * Creates an {@link IsolatedServiceException} with an error code to be logged. The meaning of
@@ -38,16 +38,48 @@
      *
      * @param errorCode An error code defined by the {@link IsolatedService}.
      */
-    public IsolatedServiceException(@IntRange(from = 1, to = 127) int errorCode) {
-        super("IsolatedServiceException: Error " + errorCode);
+    public IsolatedServiceException(int errorCode) {
+        this(errorCode, "IsolatedServiceException: Error " + errorCode, null);
+    }
+
+    /**
+     * Creates an {@link IsolatedServiceException} with an error code to be logged. The meaning of
+     * the error code is defined by the {@link IsolatedService}. The platform does not interpret
+     * the error code.
+     *
+     * @param errorCode An error code defined by the {@link IsolatedService}.
+     * @param cause the cause of this exception.
+     */
+    @FlaggedApi(Flags.FLAG_DATA_CLASS_MISSING_CTORS_AND_GETTERS_ENABLED)
+    public IsolatedServiceException(
+            int errorCode,
+            @Nullable Throwable cause) {
+        this(errorCode, "IsolatedServiceException: Error " + errorCode, cause);
+    }
+
+    /**
+     * Creates an {@link IsolatedServiceException} with an error code to be logged. The meaning of
+     * the error code is defined by the {@link IsolatedService}. The platform does not interpret
+     * the error code.
+     *
+     * @param errorCode An error code defined by the {@link IsolatedService}.
+     * @param message the exception message.
+     * @param cause the cause of this exception.
+     */
+    @FlaggedApi(Flags.FLAG_DATA_CLASS_MISSING_CTORS_AND_GETTERS_ENABLED)
+    public IsolatedServiceException(
+            int errorCode,
+            @Nullable String message,
+            @Nullable Throwable cause) {
+        super(message, cause);
         mErrorCode = errorCode;
     }
 
     /**
      * Returns the error code for this exception.
-     * @hide
      */
-    public @IntRange(from = 1, to = 127) int getErrorCode() {
+    @FlaggedApi(Flags.FLAG_DATA_CLASS_MISSING_CTORS_AND_GETTERS_ENABLED)
+    public int getErrorCode() {
         return mErrorCode;
     }
 }
diff --git a/framework/java/android/adservices/ondevicepersonalization/ModelManager.java b/framework/java/android/adservices/ondevicepersonalization/ModelManager.java
index 29f6e01..a83e910 100644
--- a/framework/java/android/adservices/ondevicepersonalization/ModelManager.java
+++ b/framework/java/android/adservices/ondevicepersonalization/ModelManager.java
@@ -67,6 +67,12 @@
             @NonNull OutcomeReceiver<InferenceOutput, Exception> receiver) {
         final long startTimeMillis = System.currentTimeMillis();
         Objects.requireNonNull(input);
+        if (input.getInputData().length == 0) {
+            throw new IllegalArgumentException("Input data can not be empty");
+        }
+        if (input.getExpectedOutputStructure().getDataOutputs().isEmpty()) {
+            throw new IllegalArgumentException("Expected output data structure can not be empty");
+        }
         Bundle bundle = new Bundle();
         bundle.putBinder(Constants.EXTRA_DATA_ACCESS_SERVICE_BINDER, mDataService.asBinder());
         bundle.putParcelable(Constants.EXTRA_INFERENCE_INPUT, new InferenceInputParcel(input));
@@ -108,12 +114,20 @@
                             executor.execute(
                                     () -> {
                                         long endTimeMillis = System.currentTimeMillis();
-                                        receiver.onError(
-                                            new IllegalStateException("Error: " + errorCode));
+                                        if (OnDevicePersonalizationException.isValidErrorCode(
+                                                errorCode)) {
+                                            receiver.onError(
+                                                    new OnDevicePersonalizationException(
+                                                            errorCode));
+                                        } else {
+                                            receiver.onError(
+                                                    new IllegalStateException(
+                                                            "Error: " + errorCode));
+                                        }
                                         logApiCallStats(
                                                 Constants.API_NAME_MODEL_MANAGER_RUN,
                                                 endTimeMillis - startTimeMillis,
-                                                Constants.STATUS_INTERNAL_ERROR);
+                                                errorCode);
                                     });
                         }
                     });
diff --git a/framework/java/android/adservices/ondevicepersonalization/OnDevicePersonalizationConfigManager.java b/framework/java/android/adservices/ondevicepersonalization/OnDevicePersonalizationConfigManager.java
index df3fc3b..92dba02 100644
--- a/framework/java/android/adservices/ondevicepersonalization/OnDevicePersonalizationConfigManager.java
+++ b/framework/java/android/adservices/ondevicepersonalization/OnDevicePersonalizationConfigManager.java
@@ -18,24 +18,16 @@
 
 import static android.adservices.ondevicepersonalization.OnDevicePersonalizationPermissions.MODIFY_ONDEVICEPERSONALIZATION_STATE;
 
-import android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationConfigService;
-import android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationConfigServiceCallback;
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.RequiresPermission;
 import android.annotation.SystemApi;
 import android.content.Context;
-import android.os.Binder;
 import android.os.OutcomeReceiver;
 
 import com.android.adservices.ondevicepersonalization.flags.Flags;
-import com.android.federatedcompute.internal.util.AbstractServiceBinder;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 
 /**
@@ -50,117 +42,19 @@
     /** @hide */
     public static final String ON_DEVICE_PERSONALIZATION_CONFIG_SERVICE =
             "on_device_personalization_config_service";
-    private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
-    private static final String TAG = OnDevicePersonalizationConfigManager.class.getSimpleName();
-
-    private static final String ODP_CONFIG_SERVICE_PACKAGE_SUFFIX =
-            "com.android.ondevicepersonalization.services";
-
-    private static final String ALT_ODP_CONFIG_SERVICE_PACKAGE_SUFFIX =
-            "com.google.android.ondevicepersonalization.services";
-    private static final String ODP_CONFIG_SERVICE_INTENT =
-            "android.OnDevicePersonalizationConfigService";
-
-    private final AbstractServiceBinder<IOnDevicePersonalizationConfigService> mServiceBinder;
 
     /** @hide */
-    public OnDevicePersonalizationConfigManager(@NonNull Context context) {
-        this(
-                AbstractServiceBinder.getServiceBinderByIntent(
-                        context,
-                        ODP_CONFIG_SERVICE_INTENT,
-                        List.of(
-                                ODP_CONFIG_SERVICE_PACKAGE_SUFFIX,
-                                ALT_ODP_CONFIG_SERVICE_PACKAGE_SUFFIX),
-                        IOnDevicePersonalizationConfigService.Stub::asInterface));
-    }
-
-    /** @hide */
-    @VisibleForTesting
-    public OnDevicePersonalizationConfigManager(
-            AbstractServiceBinder<IOnDevicePersonalizationConfigService> serviceBinder) {
-        this.mServiceBinder = serviceBinder;
-    }
+    public OnDevicePersonalizationConfigManager(@NonNull Context context) {}
 
     /**
-     * API users are expected to call this to modify personalization status for
-     * On Device Personalization. The status is persisted both in memory and to the disk.
-     * When reboot, the in-memory status will be restored from the disk.
-     * Personalization is disabled by default.
-     *
-     * @param enabled boolean whether On Device Personalization should be enabled.
-     * @param executor The {@link Executor} on which to invoke the callback.
-     * @param receiver This either returns null on success or {@link Exception} on failure.
-     *
-     *     In case of an error, the receiver returns one of the following exceptions:
-     *     Returns an {@link IllegalStateException} if the callback is unable to send back results.
-     *     Returns a {@link SecurityException} if the caller is unauthorized to modify
-     *     personalization status.
+     * Deprecated. This API is a no-op. ODP automatically determines whether personalization
+     * should be enabled using
+     * {@link android.adservices.common.AdServicesCommonManager}.
      */
     @RequiresPermission(MODIFY_ONDEVICEPERSONALIZATION_STATE)
     public void setPersonalizationEnabled(boolean enabled,
                                           @NonNull @CallbackExecutor Executor executor,
                                           @NonNull OutcomeReceiver<Void, Exception> receiver) {
-        CountDownLatch latch = new CountDownLatch(1);
-        try {
-            IOnDevicePersonalizationConfigService service = mServiceBinder.getService(executor);
-            service.setPersonalizationStatus(enabled,
-                    new IOnDevicePersonalizationConfigServiceCallback.Stub() {
-                        @Override
-                        public void onSuccess() {
-                            final long token = Binder.clearCallingIdentity();
-                            try {
-                                executor.execute(() -> {
-                                    receiver.onResult(null);
-                                    latch.countDown();
-                                });
-                            } finally {
-                                Binder.restoreCallingIdentity(token);
-                            }
-                        }
-
-                        @Override
-                        public void onFailure(int errorCode) {
-                            final long token = Binder.clearCallingIdentity();
-                            try {
-                                executor.execute(() -> {
-                                    sLogger.w(TAG + ": Unexpected failure from ODP"
-                                            + "config service with error code: " + errorCode);
-                                    receiver.onError(
-                                            new IllegalStateException("Unexpected failure."));
-                                    latch.countDown();
-                                });
-                            } finally {
-                                Binder.restoreCallingIdentity(token);
-                            }
-                        }
-                    });
-        } catch (IllegalArgumentException | NullPointerException e) {
-            latch.countDown();
-            throw e;
-        } catch (SecurityException e) {
-            sLogger.w(TAG + ": Unauthorized call to ODP config service.");
-            receiver.onError(e);
-            latch.countDown();
-        } catch (Exception e) {
-            sLogger.w(TAG + ": Unexpected exception during call to ODP config service.");
-            receiver.onError(e);
-            latch.countDown();
-        } finally {
-            try {
-                latch.await();
-            } catch (InterruptedException e) {
-                sLogger.e(TAG + ": Failed to set personalization.", e);
-                receiver.onError(e);
-            }
-            unbindFromService();
-        }
-    }
-
-    /**
-     * Unbind from config service.
-     */
-    private void unbindFromService() {
-        mServiceBinder.unbindFromService();
+        executor.execute(() -> receiver.onResult(null));
     }
 }
diff --git a/framework/java/android/adservices/ondevicepersonalization/OnDevicePersonalizationException.java b/framework/java/android/adservices/ondevicepersonalization/OnDevicePersonalizationException.java
index 631e421..9abea59 100644
--- a/framework/java/android/adservices/ondevicepersonalization/OnDevicePersonalizationException.java
+++ b/framework/java/android/adservices/ondevicepersonalization/OnDevicePersonalizationException.java
@@ -15,7 +15,6 @@
  */
 
 package android.adservices.ondevicepersonalization;
-
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 
@@ -23,6 +22,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.Set;
 
 /**
  * Exception thrown by OnDevicePersonalization APIs.
@@ -41,42 +41,80 @@
      */
     public static final int ERROR_PERSONALIZATION_DISABLED = 2;
 
-    /**
-     * The ODP module was unable to load the {@link IsolatedService}.
-     * @hide
+    /** The ODP module was unable to load the {@link IsolatedService}.
+     *
+     * <p> Retrying may be successful for platform internal errors.
      */
-    public static final int  ERROR_ISOLATED_SERVICE_LOADING_FAILED = 3;
+    @FlaggedApi(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public static final int ERROR_ISOLATED_SERVICE_LOADING_FAILED = 3;
 
     /**
      * The ODP specific manifest settings for the {@link IsolatedService} are either missing or
      * misconfigured.
-     * @hide
      */
+    @FlaggedApi(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
     public static final int ERROR_ISOLATED_SERVICE_MANIFEST_PARSING_FAILED = 4;
 
-    /**
-     * The {@link IsolatedService} was invoked but timed out before returning successfully.
-     * @hide
+    /** The {@link IsolatedService} was invoked but timed out before returning successfully.
+     *
+     * <p> This is likely due to an issue with the {@link IsolatedWorker} implementation taking too
+     * long and retries are likely to fail.
      */
+    @FlaggedApi(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
     public static final int ERROR_ISOLATED_SERVICE_TIMEOUT = 5;
 
-    /**
-     * The {@link IsolatedService}'s output failed validation checks.
-     * @hide
+    /** The {@link IsolatedService}'s call to {@link FederatedComputeScheduler#schedule} failed.
+     *
+     <p> Retrying may be successful if the issue is due to a platform internal error.
      */
-    public static final int ERROR_OUTPUT_VALIDATION_FAILED = 6;
+    @FlaggedApi(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public static final int ERROR_SCHEDULE_TRAINING_FAILED = 6;
 
     /**
-     * The {@link IsolatedService}'s call to {@link FederatedComputeScheduler} failed.
-     * @hide
+     * The {@link IsolatedService}'s call to {@link FederatedComputeScheduler#schedule} failed due
+     * to missing or misconfigured federated compute settings URL in the manifest.
      */
-    public static final int ERROR_ISOLATED_SERVICE_FAILED_TRAINING = 7;
+    @FlaggedApi(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public static final int ERROR_INVALID_TRAINING_MANIFEST = 7;
+
+    /** Inference failed due to {@link ModelManager} not finding the downloaded model. */
+    @FlaggedApi(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public static final int ERROR_INFERENCE_MODEL_NOT_FOUND = 8;
+
+    /** {@link ModelManager} failed to run inference.
+     *
+     <p> Retrying may be successful if the issue is due to a platform internal error.
+     */
+    @FlaggedApi(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public static final int ERROR_INFERENCE_FAILED = 9;
 
     /** @hide */
-    @IntDef(prefix = "ERROR_", value = {
-            ERROR_ISOLATED_SERVICE_FAILED,
-            ERROR_PERSONALIZATION_DISABLED
-    })
+    private static final Set<Integer> VALID_ERROR_CODE =
+            Set.of(
+                    ERROR_ISOLATED_SERVICE_FAILED,
+                    ERROR_PERSONALIZATION_DISABLED,
+                    ERROR_ISOLATED_SERVICE_LOADING_FAILED,
+                    ERROR_ISOLATED_SERVICE_MANIFEST_PARSING_FAILED,
+                    ERROR_ISOLATED_SERVICE_TIMEOUT,
+                    ERROR_SCHEDULE_TRAINING_FAILED,
+                    ERROR_INVALID_TRAINING_MANIFEST,
+                    ERROR_INFERENCE_MODEL_NOT_FOUND,
+                    ERROR_INFERENCE_FAILED);
+
+    /** @hide */
+    @IntDef(
+            prefix = "ERROR_",
+            value = {
+                ERROR_ISOLATED_SERVICE_FAILED,
+                ERROR_PERSONALIZATION_DISABLED,
+                ERROR_ISOLATED_SERVICE_LOADING_FAILED,
+                ERROR_ISOLATED_SERVICE_MANIFEST_PARSING_FAILED,
+                ERROR_ISOLATED_SERVICE_TIMEOUT,
+                ERROR_SCHEDULE_TRAINING_FAILED,
+                ERROR_INVALID_TRAINING_MANIFEST,
+                ERROR_INFERENCE_MODEL_NOT_FOUND,
+                ERROR_INFERENCE_FAILED
+            })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ErrorCode {}
 
@@ -101,8 +139,22 @@
         mErrorCode = errorCode;
     }
 
+    /** @hide */
+    public OnDevicePersonalizationException(
+            @ErrorCode int errorCode, String message, Throwable cause) {
+        super(message, cause);
+        mErrorCode = errorCode;
+    }
+
     /** Returns the error code for this exception. */
     public @ErrorCode int getErrorCode() {
         return mErrorCode;
     }
+
+    /**
+     * @hide Only used by internal error code validation.
+     */
+    public static boolean isValidErrorCode(int errorCode) {
+        return VALID_ERROR_CODE.contains(errorCode);
+    }
 }
diff --git a/framework/java/android/adservices/ondevicepersonalization/OnDevicePersonalizationManager.java b/framework/java/android/adservices/ondevicepersonalization/OnDevicePersonalizationManager.java
index 5fa2297..28f8b7e 100644
--- a/framework/java/android/adservices/ondevicepersonalization/OnDevicePersonalizationManager.java
+++ b/framework/java/android/adservices/ondevicepersonalization/OnDevicePersonalizationManager.java
@@ -16,6 +16,7 @@
 
 package android.adservices.ondevicepersonalization;
 
+
 import android.adservices.ondevicepersonalization.aidl.IExecuteCallback;
 import android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationManagingService;
 import android.adservices.ondevicepersonalization.aidl.IRequestSurfacePackageCallback;
@@ -39,6 +40,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.ondevicepersonalization.internal.util.ByteArrayParceledSlice;
+import com.android.ondevicepersonalization.internal.util.ExceptionInfo;
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 import com.android.ondevicepersonalization.internal.util.PersistableBundleUtils;
 
@@ -62,6 +64,7 @@
     /** @hide */
     public static final String ON_DEVICE_PERSONALIZATION_SERVICE =
             "on_device_personalization_service";
+
     private static final String INTENT_FILTER_ACTION = "android.OnDevicePersonalizationService";
     private static final String ODP_MANAGING_SERVICE_PACKAGE_SUFFIX =
             "com.android.ondevicepersonalization.services";
@@ -77,11 +80,22 @@
     private static final String ODP_DISABLED_ERROR_MESSAGE =
             "Personalization disabled by device configuration.";
 
+    private static final String ODP_MANIFEST_ERROR_MESSAGE =
+            "OnDevicePersonalization manifest invalid.";
+
+    private static final String ODP_SERVICE_LOADING_ERROR_MESSAGE =
+            "Failed to load the isolated service.";
+
+    private static final String ODP_SERVICE_TIMEOUT_ERROR_MESSAGE =
+            "The isolated service timed out without returning.";
+
     private static final String TAG = OnDevicePersonalizationManager.class.getSimpleName();
     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
     private final AbstractServiceBinder<IOnDevicePersonalizationManagingService> mServiceBinder;
     private final Context mContext;
 
+    // TODO(b/358624224); deprecate {@link ExecuteResult} after partner migrates to use {@link
+    // #executeInIsolatedService}.
     /**
      * The result of a call to {@link OnDevicePersonalizationManager#execute(ComponentName,
      * PersistableBundle, Executor, OutcomeReceiver)}
@@ -142,45 +156,42 @@
         mServiceBinder = serviceBinder;
     }
 
+    // TODO(b/358624224); deprecate {@link ExecuteResult} after partner migrates to use {@link
+    // #executeInIsolatedService}.
     /**
-     * Executes an {@link IsolatedService} in the OnDevicePersonalization sandbox. The
-     * platform binds to the specified {@link IsolatedService} in an isolated process
-     * and calls {@link IsolatedWorker#onExecute(ExecuteInput, android.os.OutcomeReceiver)}
-     * with the caller-provided parameters. When the {@link IsolatedService} finishes execution,
-     * the platform returns tokens that refer to the results from the service to the caller.
-     * These tokens can be subsequently used to display results in a
-     * {@link android.view.SurfaceView} within the calling app.
+     * Executes an {@link IsolatedService} in the OnDevicePersonalization sandbox. The platform
+     * binds to the specified {@link IsolatedService} in an isolated process and calls {@link
+     * IsolatedWorker#onExecute(ExecuteInput, android.os.OutcomeReceiver)} with the caller-provided
+     * parameters. When the {@link IsolatedService} finishes execution, the platform returns tokens
+     * that refer to the results from the service to the caller. These tokens can be subsequently
+     * used to display results in a {@link android.view.SurfaceView} within the calling app.
      *
      * @param service The {@link ComponentName} of the {@link IsolatedService}.
-     * @param params a {@link PersistableBundle} that is passed from the calling app to the
-     *     {@link IsolatedService}. The expected contents of this parameter are defined
-     *     by the{@link IsolatedService}. The platform does not interpret this parameter.
+     * @param params a {@link PersistableBundle} that is passed from the calling app to the {@link
+     *     IsolatedService}. The expected contents of this parameter are defined by the{@link
+     *     IsolatedService}. The platform does not interpret this parameter.
      * @param executor the {@link Executor} on which to invoke the callback.
-     * @param receiver This returns a {@link ExecuteResult} object on success or an
-     *     {@link Exception} on failure. If the
-     *     {@link IsolatedService} returned a {@link RenderingConfig} to be displayed,
-     *     {@link ExecuteResult#getSurfacePackageToken()} will return a non-null
-     *     {@link SurfacePackageToken}.
-     *     The {@link SurfacePackageToken} object can be used in a subsequent
-     *     {@link #requestSurfacePackage(SurfacePackageToken, IBinder, int, int, int, Executor,
-     *     OutcomeReceiver)} call to display the result in a view. The returned
-     *     {@link SurfacePackageToken} may be null to indicate that no output is expected to be
-     *     displayed for this request. If the {@link IsolatedService} has returned any output data
-     *     and the calling app is allowlisted to receive data from this service, the
-     *     {@link ExecuteResult#getOutputData()} will return a non-null byte array.
-     *
-     *     In case of an error, the receiver returns one of the following exceptions:
-     *     Returns a {@link android.content.pm.PackageManager.NameNotFoundException} if the handler
-     *     package is not installed or does not have a valid ODP manifest.
-     *     Returns {@link ClassNotFoundException} if the handler class is not found.
-     *     Returns an {@link OnDevicePersonalizationException} if execution of the handler fails.
+     * @param receiver This returns a {@link ExecuteResult} object on success or an {@link
+     *     Exception} on failure. If the {@link IsolatedService} returned a {@link RenderingConfig}
+     *     to be displayed, {@link ExecuteResult#getSurfacePackageToken()} will return a non-null
+     *     {@link SurfacePackageToken}. The {@link SurfacePackageToken} object can be used in a
+     *     subsequent {@link #requestSurfacePackage(SurfacePackageToken, IBinder, int, int, int,
+     *     Executor, OutcomeReceiver)} call to display the result in a view. The returned {@link
+     *     SurfacePackageToken} may be null to indicate that no output is expected to be displayed
+     *     for this request. If the {@link IsolatedService} has returned any output data and the
+     *     calling app is allowlisted to receive data from this service, the {@link
+     *     ExecuteResult#getOutputData()} will return a non-null byte array.
+     *     <p>In case of an error, the receiver returns one of the following exceptions: Returns a
+     *     {@link android.content.pm.PackageManager.NameNotFoundException} if the handler package is
+     *     not installed or does not have a valid ODP manifest. Returns {@link
+     *     ClassNotFoundException} if the handler class is not found. Returns an {@link
+     *     OnDevicePersonalizationException} if execution of the handler fails.
      */
     public void execute(
             @NonNull ComponentName service,
             @NonNull PersistableBundle params,
             @NonNull @CallbackExecutor Executor executor,
-            @NonNull OutcomeReceiver<ExecuteResult, Exception> receiver
-    ) {
+            @NonNull OutcomeReceiver<ExecuteResult, Exception> receiver) {
         Objects.requireNonNull(service);
         Objects.requireNonNull(params);
         Objects.requireNonNull(executor);
@@ -223,12 +234,9 @@
                                                                             tokenString);
                                                         }
                                                     }
-                                                    byte[] data =
-                                                            callbackResult.getByteArray(
-                                                                    Constants.EXTRA_OUTPUT_DATA);
                                                     receiver.onResult(
                                                             new ExecuteResult(
-                                                                    surfacePackageToken, data));
+                                                                    surfacePackageToken, null));
                                                 } catch (Exception e) {
                                                     receiver.onError(e);
                                                 }
@@ -240,7 +248,8 @@
                                             service.getPackageName(),
                                             Constants.API_NAME_EXECUTE,
                                             SystemClock.elapsedRealtime() - startTimeMillis,
-                                            calleeMetadata.getServiceEntryTimeMillis() - startTimeMillis,
+                                            calleeMetadata.getServiceEntryTimeMillis()
+                                                    - startTimeMillis,
                                             SystemClock.elapsedRealtime()
                                                     - calleeMetadata.getCallbackInvokeTimeMillis(),
                                             Constants.STATUS_SUCCESS);
@@ -248,17 +257,20 @@
                             }
 
                             @Override
-                            public void onError(int errorCode, int isolatedServiceErrorCode,
-                                    String message, CalleeMetadata calleeMetadata) {
+                            public void onError(
+                                    int errorCode,
+                                    int isolatedServiceErrorCode,
+                                    byte[] serializedExceptionInfo,
+                                    CalleeMetadata calleeMetadata) {
                                 final long token = Binder.clearCallingIdentity();
                                 try {
                                     executor.execute(
-                                            () ->
-                                                    receiver.onError(
-                                                            createException(
-                                                                    errorCode,
-                                                                    isolatedServiceErrorCode,
-                                                                    message)));
+                                            () -> {
+                                                receiver.onError(
+                                                        createException(
+                                                                errorCode, isolatedServiceErrorCode,
+                                                                serializedExceptionInfo, mContext));
+                                            });
                                 } finally {
                                     Binder.restoreCallingIdentity(token);
                                     logApiCallStats(
@@ -266,7 +278,8 @@
                                             service.getPackageName(),
                                             Constants.API_NAME_EXECUTE,
                                             SystemClock.elapsedRealtime() - startTimeMillis,
-                                            calleeMetadata.getServiceEntryTimeMillis() - startTimeMillis,
+                                            calleeMetadata.getServiceEntryTimeMillis()
+                                                    - startTimeMillis,
                                             SystemClock.elapsedRealtime()
                                                     - calleeMetadata.getCallbackInvokeTimeMillis(),
                                             errorCode);
@@ -283,6 +296,7 @@
                         service,
                         wrappedParams,
                         new CallerMetadata.Builder().setStartTimeMillis(startTimeMillis).build(),
+                        ExecuteOptionsParcel.DEFAULT,
                         callbackWrapper);
             } catch (Exception e) {
                 logApiCallStats(
@@ -302,6 +316,159 @@
     }
 
     /**
+     * Executes an {@link IsolatedService} in the OnDevicePersonalization sandbox. The platform
+     * binds to the specified {@link IsolatedService} in an isolated process and calls {@link
+     * IsolatedWorker#onExecute(ExecuteInput, android.os.OutcomeReceiver)} with the caller-provided
+     * parameters. When the {@link IsolatedService} finishes execution, the platform returns tokens
+     * that refer to the results from the service to the caller. These tokens can be subsequently
+     * used to display results in a {@link android.view.SurfaceView} within the calling app.
+     *
+     * @param request the {@link ExecuteInIsolatedServiceRequest} request
+     * @param executor the {@link Executor} on which to invoke the callback.
+     * @param receiver This returns a {@link ExecuteInIsolatedServiceResponse} object on success or
+     *     an {@link Exception} on failure. For success case, refer to {@link
+     *     ExecuteInIsolatedServiceResponse}. For error case, the receiver returns an {@link
+     *     OnDevicePersonalizationException} if execution of the handler fails.
+     */
+    @FlaggedApi(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void executeInIsolatedService(
+            @NonNull ExecuteInIsolatedServiceRequest request,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OutcomeReceiver<ExecuteInIsolatedServiceResponse, Exception> receiver) {
+        Objects.requireNonNull(executor);
+        Objects.requireNonNull(receiver);
+        validateRequest(request);
+        long startTimeMillis = SystemClock.elapsedRealtime();
+
+        try {
+            final IOnDevicePersonalizationManagingService odpService =
+                    mServiceBinder.getService(executor);
+
+            try {
+                IExecuteCallback callbackWrapper =
+                        new IExecuteCallback.Stub() {
+                            @Override
+                            public void onSuccess(
+                                    Bundle callbackResult, CalleeMetadata calleeMetadata) {
+                                final long token = Binder.clearCallingIdentity();
+                                try {
+                                    executor.execute(
+                                            () -> {
+                                                try {
+                                                    SurfacePackageToken surfacePackageToken = null;
+                                                    if (callbackResult != null) {
+                                                        String tokenString =
+                                                                callbackResult.getString(
+                                                                        Constants
+                                                                                .EXTRA_SURFACE_PACKAGE_TOKEN_STRING);
+                                                        if (tokenString != null
+                                                                && !tokenString.isBlank()) {
+                                                            surfacePackageToken =
+                                                                    new SurfacePackageToken(
+                                                                            tokenString);
+                                                        }
+                                                    }
+                                                    int intValue = -1;
+                                                    if (request.getOutputSpec().getOutputType()
+                                                            == ExecuteInIsolatedServiceRequest
+                                                                    .OutputSpec
+                                                                    .OUTPUT_TYPE_BEST_VALUE) {
+                                                        intValue =
+                                                                callbackResult.getInt(
+                                                                        Constants
+                                                                                .EXTRA_OUTPUT_BEST_VALUE);
+                                                    }
+
+                                                    receiver.onResult(
+                                                            new ExecuteInIsolatedServiceResponse(
+                                                                    surfacePackageToken, intValue));
+                                                } catch (Exception e) {
+                                                    receiver.onError(e);
+                                                }
+                                            });
+                                } finally {
+                                    Binder.restoreCallingIdentity(token);
+                                    logApiCallStats(
+                                            odpService,
+                                            request.getService().getPackageName(),
+                                            Constants.API_NAME_EXECUTE,
+                                            SystemClock.elapsedRealtime() - startTimeMillis,
+                                            calleeMetadata.getServiceEntryTimeMillis()
+                                                    - startTimeMillis,
+                                            SystemClock.elapsedRealtime()
+                                                    - calleeMetadata.getCallbackInvokeTimeMillis(),
+                                            Constants.STATUS_SUCCESS);
+                                }
+                            }
+
+                            @Override
+                            public void onError(
+                                    int errorCode,
+                                    int isolatedServiceErrorCode,
+                                    byte[] serializedExceptionInfo,
+                                    CalleeMetadata calleeMetadata) {
+                                final long token = Binder.clearCallingIdentity();
+                                try {
+                                    executor.execute(
+                                            () -> {
+                                                receiver.onError(
+                                                        // We can skip translating to legacy error
+                                                        // codes for the new API.
+                                                        createException(
+                                                                errorCode,
+                                                                isolatedServiceErrorCode,
+                                                                serializedExceptionInfo,
+                                                                mContext,
+                                                                /* translateToLegacyErrorCode= */ false));
+                                            });
+                                } finally {
+                                    Binder.restoreCallingIdentity(token);
+                                    logApiCallStats(
+                                            odpService,
+                                            request.getService().getPackageName(),
+                                            Constants.API_NAME_EXECUTE,
+                                            SystemClock.elapsedRealtime() - startTimeMillis,
+                                            calleeMetadata.getServiceEntryTimeMillis()
+                                                    - startTimeMillis,
+                                            SystemClock.elapsedRealtime()
+                                                    - calleeMetadata.getCallbackInvokeTimeMillis(),
+                                            errorCode);
+                                }
+                            }
+                        };
+
+                Bundle wrappedParams = new Bundle();
+                wrappedParams.putParcelable(
+                        Constants.EXTRA_APP_PARAMS_SERIALIZED,
+                        new ByteArrayParceledSlice(
+                                PersistableBundleUtils.toByteArray(request.getAppParams())));
+                odpService.execute(
+                        mContext.getPackageName(),
+                        request.getService(),
+                        wrappedParams,
+                        new CallerMetadata.Builder().setStartTimeMillis(startTimeMillis).build(),
+                        request.getOutputSpec() == null
+                                ? ExecuteOptionsParcel.DEFAULT
+                                : new ExecuteOptionsParcel(request.getOutputSpec()),
+                        callbackWrapper);
+            } catch (Exception e) {
+                logApiCallStats(
+                        odpService,
+                        request.getService().getPackageName(),
+                        Constants.API_NAME_EXECUTE,
+                        SystemClock.elapsedRealtime() - startTimeMillis,
+                        0,
+                        0,
+                        Constants.STATUS_INTERNAL_ERROR);
+                receiver.onError(e);
+            }
+
+        } catch (Exception e) {
+            receiver.onError(e);
+        }
+    }
+
+    /**
      * Requests a {@link android.view.SurfaceControlViewHost.SurfacePackage} to be inserted into a
      * {@link android.view.SurfaceView} inside the calling app. The surface package will contain an
      * {@link android.view.View} with the content from a result of a prior call to
@@ -382,12 +549,14 @@
                                                     - calleeMetadata.getCallbackInvokeTimeMillis(),
                                             Constants.STATUS_SUCCESS);
                                 }
-
                             }
 
                             @Override
-                            public void onError(int errorCode, int isolatedServiceErrorCode,
-                                    String message, CalleeMetadata calleeMetadata) {
+                            public void onError(
+                                    int errorCode,
+                                    int isolatedServiceErrorCode,
+                                    byte[] serializedExceptionInfo,
+                                    CalleeMetadata calleeMetadata) {
                                 final long token = Binder.clearCallingIdentity();
                                 try {
                                     executor.execute(
@@ -396,11 +565,13 @@
                                                             createException(
                                                                     errorCode,
                                                                     isolatedServiceErrorCode,
-                                                                    message)));
+                                                                    serializedExceptionInfo,
+                                                                    mContext)));
                                 } finally {
                                     Binder.restoreCallingIdentity(token);
                                     logApiCallStats(
-                                            service, "",
+                                            service,
+                                            "",
                                             Constants.API_NAME_REQUEST_SURFACE_PACKAGE,
                                             SystemClock.elapsedRealtime() - startTimeMillis,
                                             0,
@@ -445,13 +616,20 @@
         }
     }
 
-    private static String convertMessage(int errorCode, String message) {
-        // Defer to existing message received from service callback if it is non-empty, else
-        // translate the internal error codes into error messages.
-        if (message != null && !message.isBlank()) {
-            return message;
+    private static void validateRequest(ExecuteInIsolatedServiceRequest request) {
+        Objects.requireNonNull(request.getService());
+        ComponentName service = request.getService();
+        Objects.requireNonNull(service.getPackageName());
+        Objects.requireNonNull(service.getClassName());
+        if (service.getPackageName().isEmpty()) {
+            throw new IllegalArgumentException("missing service package name");
         }
+        if (service.getClassName().isEmpty()) {
+            throw new IllegalArgumentException("missing service class name");
+        }
+    }
 
+    private static String convertMessage(int errorCode) {
         switch (errorCode) {
             case Constants.STATUS_INTERNAL_ERROR:
                 return ODP_INTERNAL_ERROR_MESSAGE;
@@ -459,38 +637,124 @@
                 return ISOLATED_SERVICE_ERROR_MESSAGE;
             case Constants.STATUS_PERSONALIZATION_DISABLED:
                 return ODP_DISABLED_ERROR_MESSAGE;
+            case Constants.STATUS_MANIFEST_PARSING_FAILED: // Intentional fallthrough
+            case Constants.STATUS_MANIFEST_MISCONFIGURED:
+                return ODP_MANIFEST_ERROR_MESSAGE;
+            case Constants.STATUS_ISOLATED_SERVICE_LOADING_FAILED:
+                return ODP_SERVICE_LOADING_ERROR_MESSAGE;
+            case Constants.STATUS_ISOLATED_SERVICE_TIMEOUT:
+                return ODP_SERVICE_TIMEOUT_ERROR_MESSAGE;
             default:
                 sLogger.w(TAG + "Unexpected error code while creating exception: " + errorCode);
                 return "";
         }
     }
 
+    /**
+     * Convert granular error codes returned by the ODP Service to legacy error codes if required.
+     */
+    private static int translateErrorCode(int errorCode, Context context) {
+        if (errorCode < Constants.STATUS_MANIFEST_PARSING_FAILED) {
+            // Return code unchanged since either the error code does not require translation
+            // by virtue of being an old/original error code.
+            return errorCode;
+        }
+        // Translate to appropriate older error code if required.
+        sLogger.d(TAG, "Translating to legacy error codes for package " + context.getPackageName());
+        // TODO (b/342672147): add translation for newer error codes
+        int translatedCode = Constants.STATUS_INTERNAL_ERROR;
+        switch (errorCode) {
+            case Constants.STATUS_MANIFEST_PARSING_FAILED ->
+                    translatedCode = Constants.STATUS_NAME_NOT_FOUND;
+            case Constants.STATUS_MANIFEST_MISCONFIGURED ->
+                    translatedCode = Constants.STATUS_CLASS_NOT_FOUND;
+            case Constants.STATUS_ISOLATED_SERVICE_LOADING_FAILED ->
+                    translatedCode = Constants.STATUS_SERVICE_FAILED;
+            case Constants.STATUS_ISOLATED_SERVICE_TIMEOUT ->
+                    translatedCode = Constants.STATUS_SERVICE_FAILED;
+        }
+        return translatedCode;
+    }
+
+    /**
+     * Helper method to create appropriate Exception that translates error codes to legacy error
+     * codes for compatibility.
+     */
     private static Exception createException(
-            int errorCode, int isolatedServiceErrorCode, String message) {
-        if (errorCode == Constants.STATUS_NAME_NOT_FOUND) {
-            return new PackageManager.NameNotFoundException();
-        } else if (errorCode == Constants.STATUS_CLASS_NOT_FOUND) {
-            return new ClassNotFoundException();
-        } else if (errorCode == Constants.STATUS_SERVICE_FAILED) {
-            if (isolatedServiceErrorCode > 0 && isolatedServiceErrorCode < 128) {
+            int errorCode,
+            int isolatedServiceErrorCode,
+            byte[] serializedExceptionInfo,
+            Context context) {
+        return createException(
+                errorCode,
+                isolatedServiceErrorCode,
+                serializedExceptionInfo,
+                context,
+                /* translateToLegacyErrorCode= */ true);
+    }
+
+    private static Exception createException(
+            int errorCode,
+            int isolatedServiceErrorCode,
+            byte[] serializedExceptionInfo,
+            Context context,
+            boolean translateToLegacyErrorCode) {
+        if (translateToLegacyErrorCode) {
+            errorCode = translateErrorCode(errorCode, context);
+        }
+        Exception cause = ExceptionInfo.fromByteArray(serializedExceptionInfo);
+        switch (errorCode) {
+            case Constants.STATUS_NAME_NOT_FOUND:
+                Exception e = new PackageManager.NameNotFoundException();
+                try {
+                    // NameNotFoundException does not have a constructor that takes a Throwable.
+                    if (cause != null) {
+                        e.initCause(cause);
+                    }
+                } catch (Exception e2) {
+                    sLogger.i(TAG + ": could not update cause", e2);
+                }
+                return e;
+            case Constants.STATUS_CLASS_NOT_FOUND:
+                return new ClassNotFoundException("", cause);
+            case Constants.STATUS_SERVICE_FAILED:
+                return (isolatedServiceErrorCode > 0 && isolatedServiceErrorCode < 128)
+                        ? new OnDevicePersonalizationException(
+                                OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_FAILED,
+                                new IsolatedServiceException(isolatedServiceErrorCode))
+                        : new OnDevicePersonalizationException(
+                                OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_FAILED,
+                                convertMessage(errorCode),
+                                cause);
+            case Constants.STATUS_PERSONALIZATION_DISABLED:
                 return new OnDevicePersonalizationException(
-                        OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_FAILED,
-                        new IsolatedServiceException(isolatedServiceErrorCode));
-            } else {
+                        OnDevicePersonalizationException.ERROR_PERSONALIZATION_DISABLED,
+                        convertMessage(errorCode),
+                        cause);
+            case Constants.STATUS_MANIFEST_PARSING_FAILED:
+                // Intentional fallthrough
+            case Constants.STATUS_MANIFEST_MISCONFIGURED:
                 return new OnDevicePersonalizationException(
-                        OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_FAILED,
-                        convertMessage(errorCode, message));
-            }
-        } else if (errorCode == Constants.STATUS_PERSONALIZATION_DISABLED) {
-            return new OnDevicePersonalizationException(
-                    OnDevicePersonalizationException.ERROR_PERSONALIZATION_DISABLED,
-                    convertMessage(errorCode, message));
-        } else {
-            return new IllegalStateException(convertMessage(errorCode, message));
+                        OnDevicePersonalizationException
+                                .ERROR_ISOLATED_SERVICE_MANIFEST_PARSING_FAILED,
+                        convertMessage(errorCode),
+                        cause);
+            case Constants.STATUS_ISOLATED_SERVICE_LOADING_FAILED:
+                return new OnDevicePersonalizationException(
+                        OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_LOADING_FAILED,
+                        convertMessage(errorCode),
+                        cause);
+            case Constants.STATUS_ISOLATED_SERVICE_TIMEOUT:
+                return new OnDevicePersonalizationException(
+                        OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_TIMEOUT,
+                        convertMessage(errorCode),
+                        cause);
+            default:
+                return new IllegalStateException(convertMessage(errorCode), cause);
         }
     }
 
-    private void logApiCallStats(
+    private static void logApiCallStats(
             IOnDevicePersonalizationManagingService service,
             String sdkPackageName,
             int apiName,
diff --git a/framework/java/android/adservices/ondevicepersonalization/RemoteDataImpl.java b/framework/java/android/adservices/ondevicepersonalization/RemoteDataImpl.java
index aaa32ef..fabc9b7 100644
--- a/framework/java/android/adservices/ondevicepersonalization/RemoteDataImpl.java
+++ b/framework/java/android/adservices/ondevicepersonalization/RemoteDataImpl.java
@@ -51,7 +51,7 @@
         final long startTimeMillis = System.currentTimeMillis();
         int responseCode = Constants.STATUS_SUCCESS;
         try {
-            BlockingQueue<Bundle> asyncResult = new ArrayBlockingQueue<>(1);
+            BlockingQueue<CallbackResult> asyncResult = new ArrayBlockingQueue<>(1);
             Bundle params = new Bundle();
             params.putString(Constants.EXTRA_LOOKUP_KEYS, key);
             mDataAccessService.onRequest(
@@ -60,22 +60,30 @@
                     new IDataAccessServiceCallback.Stub() {
                         @Override
                         public void onSuccess(@NonNull Bundle result) {
-                            if (result != null) {
-                                asyncResult.add(result);
-                            } else {
-                                asyncResult.add(Bundle.EMPTY);
-                            }
+                            asyncResult.add(new CallbackResult(result, 0));
                         }
 
                         @Override
                         public void onError(int errorCode) {
-                            asyncResult.add(Bundle.EMPTY);
+                            asyncResult.add(new CallbackResult(Bundle.EMPTY, errorCode));
                         }
                     });
-            Bundle result = asyncResult.take();
-            ByteArrayParceledSlice data = result.getParcelable(
-                            Constants.EXTRA_RESULT, ByteArrayParceledSlice.class);
-            return (data == null) ? null : data.getByteArray();
+
+            CallbackResult callbackResult = asyncResult.take();
+            if (callbackResult.mErrorCode != 0) {
+                responseCode = Constants.STATUS_INTERNAL_ERROR;
+                return null;
+            }
+            Bundle result = callbackResult.mResult;
+            if (result == null
+                    || result.getParcelable(Constants.EXTRA_RESULT, ByteArrayParceledSlice.class)
+                            == null) {
+                responseCode = Constants.STATUS_SUCCESS_EMPTY_RESULT;
+                return null;
+            }
+            ByteArrayParceledSlice data =
+                    result.getParcelable(Constants.EXTRA_RESULT, ByteArrayParceledSlice.class);
+            return data.getByteArray();
         } catch (InterruptedException | RemoteException e) {
             sLogger.e(TAG + ": Failed to retrieve key from remoteData", e);
             responseCode = Constants.STATUS_INTERNAL_ERROR;
@@ -97,34 +105,36 @@
         final long startTimeMillis = System.currentTimeMillis();
         int responseCode = Constants.STATUS_SUCCESS;
         try {
-            BlockingQueue<Bundle> asyncResult = new ArrayBlockingQueue<>(1);
+            BlockingQueue<CallbackResult> asyncResult = new ArrayBlockingQueue<>(1);
             mDataAccessService.onRequest(
                     Constants.DATA_ACCESS_OP_REMOTE_DATA_KEYSET,
                     Bundle.EMPTY,
                     new IDataAccessServiceCallback.Stub() {
                         @Override
                         public void onSuccess(@NonNull Bundle result) {
-                            if (result != null) {
-                                asyncResult.add(result);
-                            } else {
-                                asyncResult.add(Bundle.EMPTY);
-                            }
+                            asyncResult.add(new CallbackResult(result, 0));
                         }
 
                         @Override
                         public void onError(int errorCode) {
-                            asyncResult.add(Bundle.EMPTY);
+                            asyncResult.add(new CallbackResult(null, errorCode));
                         }
                     });
-            Bundle result = asyncResult.take();
-            HashSet<String> resultSet =
-                    result.getSerializable(Constants.EXTRA_RESULT, HashSet.class);
-            if (null == resultSet) {
+            CallbackResult callbackResult = asyncResult.take();
+            if (callbackResult.mErrorCode != 0) {
+                responseCode = callbackResult.mErrorCode;
                 return Collections.emptySet();
             }
-            return resultSet;
+            Bundle result = callbackResult.mResult;
+            if (result == null
+                    || result.getSerializable(Constants.EXTRA_RESULT, HashSet.class) == null) {
+                responseCode = Constants.STATUS_SUCCESS_EMPTY_RESULT;
+                return Collections.emptySet();
+            }
+            return result.getSerializable(Constants.EXTRA_RESULT, HashSet.class);
         } catch (InterruptedException | RemoteException e) {
             sLogger.e(TAG + ": Failed to retrieve keySet from remoteData", e);
+            responseCode = Constants.STATUS_INTERNAL_ERROR;
             throw new IllegalStateException(e);
         } finally {
             try {
@@ -142,4 +152,14 @@
     public int getTableId() {
         return ModelId.TABLE_ID_REMOTE_DATA;
     }
+
+    private static class CallbackResult {
+        final Bundle mResult;
+        final int mErrorCode;
+
+        CallbackResult(Bundle result, int errorCode) {
+            mResult = result;
+            mErrorCode = errorCode;
+        }
+    }
 }
diff --git a/framework/java/android/adservices/ondevicepersonalization/RenderInput.java b/framework/java/android/adservices/ondevicepersonalization/RenderInput.java
index 9549c73..aabb94f 100644
--- a/framework/java/android/adservices/ondevicepersonalization/RenderInput.java
+++ b/framework/java/android/adservices/ondevicepersonalization/RenderInput.java
@@ -21,7 +21,6 @@
 import android.annotation.Nullable;
 
 import com.android.adservices.ondevicepersonalization.flags.Flags;
-import com.android.ondevicepersonalization.internal.util.DataClass;
 
 /**
  * The input data for
@@ -29,7 +28,6 @@
  *
  */
 @FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED)
-@DataClass(genBuilder = false, genHiddenConstructor = true, genEqualsHashCode = true)
 public final class RenderInput {
     /** The width of the slot. */
     private int mWidth = 0;
@@ -48,21 +46,6 @@
         this(parcel.getWidth(), parcel.getHeight(), parcel.getRenderingConfig());
     }
 
-
-
-    // Code below generated by codegen v1.0.23.
-    //
-    // DO NOT MODIFY!
-    // CHECKSTYLE:OFF Generated code
-    //
-    // To regenerate run:
-    // $ codegen $ANDROID_BUILD_TOP/packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/RenderInput.java
-    //
-    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
-    //   Settings > Editor > Code Style > Formatter Control
-    //@formatter:off
-
-
     /**
      * Creates a new RenderInput.
      *
@@ -73,9 +56,8 @@
      * @param renderingConfig
      *   A {@link RenderingConfig} within an {@link ExecuteOutput} that was returned by
      *   {@link IsolatedWorker#onExecute(ExecuteInput, android.os.OutcomeReceiver)}.
-     * @hide
      */
-    @DataClass.Generated.Member
+    @FlaggedApi(Flags.FLAG_DATA_CLASS_MISSING_CTORS_AND_GETTERS_ENABLED)
     public RenderInput(
             int width,
             int height,
@@ -90,7 +72,6 @@
     /**
      * The width of the slot.
      */
-    @DataClass.Generated.Member
     public int getWidth() {
         return mWidth;
     }
@@ -98,7 +79,6 @@
     /**
      * The height of the slot.
      */
-    @DataClass.Generated.Member
     public int getHeight() {
         return mHeight;
     }
@@ -107,13 +87,11 @@
      * A {@link RenderingConfig} within an {@link ExecuteOutput} that was returned by
      * {@link IsolatedWorker#onExecute(ExecuteInput, android.os.OutcomeReceiver)}.
      */
-    @DataClass.Generated.Member
     public @Nullable RenderingConfig getRenderingConfig() {
         return mRenderingConfig;
     }
 
     @Override
-    @DataClass.Generated.Member
     public boolean equals(@Nullable Object o) {
         // You can override field equality logic by defining either of the methods like:
         // boolean fieldNameEquals(RenderInput other) { ... }
@@ -131,7 +109,6 @@
     }
 
     @Override
-    @DataClass.Generated.Member
     public int hashCode() {
         // You can override field hashCode logic by defining methods like:
         // int fieldNameHashCode() { ... }
@@ -142,17 +119,4 @@
         _hash = 31 * _hash + java.util.Objects.hashCode(mRenderingConfig);
         return _hash;
     }
-
-    @DataClass.Generated(
-            time = 1704831946167L,
-            codegenVersion = "1.0.23",
-            sourceFile = "packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/RenderInput.java",
-            inputSignatures = "private  int mWidth\nprivate  int mHeight\n @android.annotation.Nullable android.adservices.ondevicepersonalization.RenderingConfig mRenderingConfig\nclass RenderInput extends java.lang.Object implements []\[email protected](genBuilder=false, genHiddenConstructor=true, genEqualsHashCode=true)")
-    @Deprecated
-    private void __metadata() {}
-
-
-    //@formatter:on
-    // End of generated code
-
 }
diff --git a/framework/java/android/adservices/ondevicepersonalization/SurfacePackageToken.java b/framework/java/android/adservices/ondevicepersonalization/SurfacePackageToken.java
index d412de9..93d93e0 100644
--- a/framework/java/android/adservices/ondevicepersonalization/SurfacePackageToken.java
+++ b/framework/java/android/adservices/ondevicepersonalization/SurfacePackageToken.java
@@ -25,19 +25,20 @@
 /**
  * An opaque reference to content that can be displayed in a {@link android.view.SurfaceView}. This
  * maps to a {@link RenderingConfig} returned by an {@link IsolatedService}.
- *
  */
 @FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED)
 public class SurfacePackageToken {
     @NonNull private final String mTokenString;
 
-    SurfacePackageToken(@NonNull String tokenString) {
+    /** @hide */
+    public SurfacePackageToken(@NonNull String tokenString) {
         mTokenString = tokenString;
     }
 
     /** @hide */
     @VisibleForTesting
-    @NonNull public String getTokenString() {
+    @NonNull
+    public String getTokenString() {
         return mTokenString;
     }
 }
diff --git a/framework/java/android/adservices/ondevicepersonalization/TrainingExamplesInput.java b/framework/java/android/adservices/ondevicepersonalization/TrainingExamplesInput.java
index 447d6d0..ddd332e 100644
--- a/framework/java/android/adservices/ondevicepersonalization/TrainingExamplesInput.java
+++ b/framework/java/android/adservices/ondevicepersonalization/TrainingExamplesInput.java
@@ -21,12 +21,11 @@
 import android.annotation.Nullable;
 
 import com.android.adservices.ondevicepersonalization.flags.Flags;
-import com.android.ondevicepersonalization.internal.util.AnnotationValidations;
-import com.android.ondevicepersonalization.internal.util.DataClass;
+
+import java.util.Objects;
 
 /** The input data for {@link IsolatedWorker#onTrainingExamples}. */
 @FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED)
-@DataClass(genBuilder = false, genHiddenConstructor = true, genEqualsHashCode = true)
 public final class TrainingExamplesInput {
     /**
      * The name of the federated compute population. It should match the population name in {@link
@@ -63,19 +62,6 @@
                 parcel.getCollectionName());
     }
 
-    // Code below generated by codegen v1.0.23.
-    //
-    // DO NOT MODIFY!
-    // CHECKSTYLE:OFF Generated code
-    //
-    // To regenerate run:
-    // $ codegen
-    // $ANDROID_BUILD_TOP/packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/TrainingExamplesInput.java
-    //
-    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
-    //   Settings > Editor > Code Style > Formatter Control
-    // @formatter:off
-
     /**
      * Creates a new TrainingExamplesInput.
      *
@@ -90,29 +76,23 @@
      *     OnDevicePersonalization will store it and pass it here for generating new training
      *     examples.
      * @param collectionName The data collection name to use to create training examples.
-     * @hide
      */
-    @DataClass.Generated.Member
+    @FlaggedApi(Flags.FLAG_DATA_CLASS_MISSING_CTORS_AND_GETTERS_ENABLED)
     public TrainingExamplesInput(
             @NonNull String populationName,
             @NonNull String taskName,
             @Nullable byte[] resumptionToken,
             @Nullable String collectionName) {
-        this.mPopulationName = populationName;
-        AnnotationValidations.validate(NonNull.class, null, mPopulationName);
-        this.mTaskName = taskName;
-        AnnotationValidations.validate(NonNull.class, null, mTaskName);
+        this.mPopulationName = Objects.requireNonNull(populationName);
+        this.mTaskName = Objects.requireNonNull(taskName);
         this.mResumptionToken = resumptionToken;
         this.mCollectionName = collectionName;
-
-        // onConstructed(); // You can define this method to get a callback
     }
 
     /**
      * The name of the federated compute population. It should match the population name in {@link
      * FederatedComputeInput#getPopulationName}.
      */
-    @DataClass.Generated.Member
     public @NonNull String getPopulationName() {
         return mPopulationName;
     }
@@ -122,7 +102,6 @@
      * federated compute server. One population may have multiple tasks. The task name can be used
      * to uniquely identify the job.
      */
-    @DataClass.Generated.Member
     public @NonNull String getTaskName() {
         return mTaskName;
     }
@@ -133,25 +112,18 @@
      * {@link TrainingExampleRecord.Builder#setResumptionToken}, OnDevicePersonalization will store
      * it and pass it here for generating new training examples.
      */
-    @DataClass.Generated.Member
     public @Nullable byte[] getResumptionToken() {
         return mResumptionToken;
     }
 
     /** The data collection name to use to create training examples. */
-    @DataClass.Generated.Member
     @FlaggedApi(Flags.FLAG_FCP_MODEL_VERSION_ENABLED)
     public @Nullable String getCollectionName() {
         return mCollectionName;
     }
 
     @Override
-    @DataClass.Generated.Member
     public boolean equals(@Nullable Object o) {
-        // You can override field equality logic by defining either of the methods like:
-        // boolean fieldNameEquals(TrainingExamplesInput other) { ... }
-        // boolean fieldNameEquals(FieldType otherValue) { ... }
-
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         @SuppressWarnings("unchecked")
@@ -165,11 +137,7 @@
     }
 
     @Override
-    @DataClass.Generated.Member
     public int hashCode() {
-        // You can override field hashCode logic by defining methods like:
-        // int fieldNameHashCode() { ... }
-
         int _hash = 1;
         _hash = 31 * _hash + java.util.Objects.hashCode(mPopulationName);
         _hash = 31 * _hash + java.util.Objects.hashCode(mTaskName);
@@ -177,18 +145,4 @@
         _hash = 31 * _hash + java.util.Objects.hashCode(mCollectionName);
         return _hash;
     }
-
-    @DataClass.Generated(
-            time = 1717540629847L,
-            codegenVersion = "1.0.23",
-            sourceFile =
-                    "packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/TrainingExamplesInput.java",
-            inputSignatures =
-                    "private @android.annotation.NonNull java.lang.String mPopulationName\nprivate @android.annotation.NonNull java.lang.String mTaskName\nprivate @android.annotation.Nullable byte[] mResumptionToken\nprivate @android.annotation.Nullable java.lang.String mCollectionName\nclass TrainingExamplesInput extends java.lang.Object implements []\[email protected](genBuilder=false, genHiddenConstructor=true, genEqualsHashCode=true)")
-    @Deprecated
-    private void __metadata() {}
-
-    // @formatter:on
-    // End of generated code
-
 }
diff --git a/framework/java/android/adservices/ondevicepersonalization/WebTriggerInput.java b/framework/java/android/adservices/ondevicepersonalization/WebTriggerInput.java
index 5c57cb6..b73bedb 100644
--- a/framework/java/android/adservices/ondevicepersonalization/WebTriggerInput.java
+++ b/framework/java/android/adservices/ondevicepersonalization/WebTriggerInput.java
@@ -21,20 +21,19 @@
 import android.net.Uri;
 
 import com.android.adservices.ondevicepersonalization.flags.Flags;
-import com.android.ondevicepersonalization.internal.util.AnnotationValidations;
-import com.android.ondevicepersonalization.internal.util.DataClass;
+
+import java.util.Objects;
 
 /**
  * The input data for
  * {@link IsolatedWorker#onWebTrigger(WebTriggerInput, android.os.OutcomeReceiver)}.
  */
 @FlaggedApi(Flags.FLAG_ON_DEVICE_PERSONALIZATION_APIS_ENABLED)
-@DataClass(genBuilder = false, genHiddenConstructor = true, genEqualsHashCode = true)
 public final class WebTriggerInput {
     /** The destination URL (landing page) where the trigger event occurred. */
     @NonNull private Uri mDestinationUrl;
 
-    /** The app where the trigger event occurred */
+    /** The package name of the app where the trigger event occurred */
     @NonNull private String mAppPackageName;
 
     /**
@@ -49,64 +48,38 @@
         this(parcel.getDestinationUrl(), parcel.getAppPackageName(), parcel.getData());
     }
 
-
-
-    // Code below generated by codegen v1.0.23.
-    //
-    // DO NOT MODIFY!
-    // CHECKSTYLE:OFF Generated code
-    //
-    // To regenerate run:
-    // $ codegen $ANDROID_BUILD_TOP/packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/WebTriggerInput.java
-    //
-    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
-    //   Settings > Editor > Code Style > Formatter Control
-    //@formatter:off
-
-
     /**
      * Creates a new WebTriggerInput.
      *
      * @param destinationUrl
      *   The destination URL (landing page) where the trigger event occurred.
      * @param appPackageName
-     *   The app where the trigger event occurred
+     *   The package name of the app where the trigger event occurred
      * @param data
      *   Additional data returned by the server as part of the web trigger registration
      *   to be sent to the {@link IsolatedService}. This can be {@code null} if the server
      *   does not need to send data to the service for processing web triggers.
-     * @hide
      */
-    @DataClass.Generated.Member
+    @FlaggedApi(Flags.FLAG_DATA_CLASS_MISSING_CTORS_AND_GETTERS_ENABLED)
     public WebTriggerInput(
             @NonNull Uri destinationUrl,
             @NonNull String appPackageName,
             @NonNull byte[] data) {
-        this.mDestinationUrl = destinationUrl;
-        AnnotationValidations.validate(
-                NonNull.class, null, mDestinationUrl);
-        this.mAppPackageName = appPackageName;
-        AnnotationValidations.validate(
-                NonNull.class, null, mAppPackageName);
-        this.mData = data;
-        AnnotationValidations.validate(
-                NonNull.class, null, mData);
-
-        // onConstructed(); // You can define this method to get a callback
+        this.mDestinationUrl = Objects.requireNonNull(destinationUrl);
+        this.mAppPackageName = Objects.requireNonNull(appPackageName);
+        this.mData = Objects.requireNonNull(data);
     }
 
     /**
      * The destination URL (landing page) where the trigger event occurred.
      */
-    @DataClass.Generated.Member
     public @NonNull Uri getDestinationUrl() {
         return mDestinationUrl;
     }
 
     /**
-     * The app where the trigger event occurred
+     * The package name of the app where the trigger event occurred
      */
-    @DataClass.Generated.Member
     public @NonNull String getAppPackageName() {
         return mAppPackageName;
     }
@@ -116,18 +89,12 @@
      * to be sent to the {@link IsolatedService}. This can be {@code null} if the server
      * does not need to send data to the service for processing web triggers.
      */
-    @DataClass.Generated.Member
     public @NonNull byte[] getData() {
         return mData;
     }
 
     @Override
-    @DataClass.Generated.Member
     public boolean equals(@android.annotation.Nullable Object o) {
-        // You can override field equality logic by defining either of the methods like:
-        // boolean fieldNameEquals(WebTriggerInput other) { ... }
-        // boolean fieldNameEquals(FieldType otherValue) { ... }
-
         if (this == o) return true;
         if (o == null || getClass() != o.getClass()) return false;
         @SuppressWarnings("unchecked")
@@ -140,28 +107,11 @@
     }
 
     @Override
-    @DataClass.Generated.Member
     public int hashCode() {
-        // You can override field hashCode logic by defining methods like:
-        // int fieldNameHashCode() { ... }
-
         int _hash = 1;
         _hash = 31 * _hash + java.util.Objects.hashCode(mDestinationUrl);
         _hash = 31 * _hash + java.util.Objects.hashCode(mAppPackageName);
         _hash = 31 * _hash + java.util.Arrays.hashCode(mData);
         return _hash;
     }
-
-    @DataClass.Generated(
-            time = 1707513068642L,
-            codegenVersion = "1.0.23",
-            sourceFile = "packages/modules/OnDevicePersonalization/framework/java/android/adservices/ondevicepersonalization/WebTriggerInput.java",
-            inputSignatures = "private @android.annotation.NonNull android.net.Uri mDestinationUrl\nprivate @android.annotation.NonNull java.lang.String mAppPackageName\nprivate @android.annotation.NonNull byte[] mData\nclass WebTriggerInput extends java.lang.Object implements []\[email protected](genBuilder=false, genHiddenConstructor=true, genEqualsHashCode=true)")
-    @Deprecated
-    private void __metadata() {}
-
-
-    //@formatter:on
-    // End of generated code
-
 }
diff --git a/framework/java/android/adservices/ondevicepersonalization/aidl/IExecuteCallback.aidl b/framework/java/android/adservices/ondevicepersonalization/aidl/IExecuteCallback.aidl
index fb6a30c..a6b86bb 100644
--- a/framework/java/android/adservices/ondevicepersonalization/aidl/IExecuteCallback.aidl
+++ b/framework/java/android/adservices/ondevicepersonalization/aidl/IExecuteCallback.aidl
@@ -22,5 +22,9 @@
 /** @hide */
 oneway interface IExecuteCallback {
     void onSuccess(in Bundle result, in CalleeMetadata calleeMetadata);
-    void onError(int errorCode, int isolatedServiceErrorCode, String errorMessage, in CalleeMetadata calleeMetadata);
+    void onError(
+            in int errorCode,
+            in int isolatedServiceErrorCode,
+            in byte[] serializedExceptionInfo,
+            in CalleeMetadata calleeMetadata);
 }
diff --git a/framework/java/android/adservices/ondevicepersonalization/aidl/IIsolatedServiceCallback.aidl b/framework/java/android/adservices/ondevicepersonalization/aidl/IIsolatedServiceCallback.aidl
index eaaed5c..10fabeb 100644
--- a/framework/java/android/adservices/ondevicepersonalization/aidl/IIsolatedServiceCallback.aidl
+++ b/framework/java/android/adservices/ondevicepersonalization/aidl/IIsolatedServiceCallback.aidl
@@ -21,5 +21,8 @@
 /** @hide */
 oneway interface IIsolatedServiceCallback {
     void onSuccess(in Bundle result);
-    void onError(int errorCode, int isolatedServiceErrorCode);
+    void onError(
+            int errorCode,
+            int isolatedServiceErrorCode,
+            in byte[] serializedExceptionInfo);
 }
diff --git a/framework/java/android/adservices/ondevicepersonalization/aidl/IOnDevicePersonalizationConfigService.aidl b/framework/java/android/adservices/ondevicepersonalization/aidl/IOnDevicePersonalizationConfigService.aidl
deleted file mode 100644
index f2d04e9..0000000
--- a/framework/java/android/adservices/ondevicepersonalization/aidl/IOnDevicePersonalizationConfigService.aidl
+++ /dev/null
@@ -1,30 +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 android.adservices.ondevicepersonalization.aidl;
-
-import android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationConfigServiceCallback;
-
-/**
-  * OnDevicePersonalization service that modifies
-  * ODP's enablement status by GMS Core only.
-  * @hide
-  */
-interface IOnDevicePersonalizationConfigService {
-
-    void setPersonalizationStatus(in boolean enabled,
-            in IOnDevicePersonalizationConfigServiceCallback callback);
-}
\ No newline at end of file
diff --git a/framework/java/android/adservices/ondevicepersonalization/aidl/IOnDevicePersonalizationManagingService.aidl b/framework/java/android/adservices/ondevicepersonalization/aidl/IOnDevicePersonalizationManagingService.aidl
index 1c89a0f..eac9a76 100644
--- a/framework/java/android/adservices/ondevicepersonalization/aidl/IOnDevicePersonalizationManagingService.aidl
+++ b/framework/java/android/adservices/ondevicepersonalization/aidl/IOnDevicePersonalizationManagingService.aidl
@@ -18,6 +18,7 @@
 
 import android.content.ComponentName;
 import android.adservices.ondevicepersonalization.CallerMetadata;
+import android.adservices.ondevicepersonalization.ExecuteOptionsParcel;
 import android.adservices.ondevicepersonalization.aidl.IExecuteCallback;
 import android.adservices.ondevicepersonalization.aidl.IRegisterMeasurementEventCallback;
 import android.adservices.ondevicepersonalization.aidl.IRequestSurfacePackageCallback;
@@ -32,6 +33,7 @@
         in ComponentName handler,
         in Bundle wrappedParams,
         in CallerMetadata metadata,
+        in ExecuteOptionsParcel options,
         in IExecuteCallback callback);
 
     void requestSurfacePackage(
diff --git a/framework/java/android/adservices/ondevicepersonalization/aidl/IRequestSurfacePackageCallback.aidl b/framework/java/android/adservices/ondevicepersonalization/aidl/IRequestSurfacePackageCallback.aidl
index e8bf21b..0e51830 100644
--- a/framework/java/android/adservices/ondevicepersonalization/aidl/IRequestSurfacePackageCallback.aidl
+++ b/framework/java/android/adservices/ondevicepersonalization/aidl/IRequestSurfacePackageCallback.aidl
@@ -22,6 +22,9 @@
 /** @hide */
 oneway interface IRequestSurfacePackageCallback {
     void onSuccess(in SurfacePackage surfacePackage, in CalleeMetadata calleeMetadata);
-    void onError(int errorCode, int isolatedServiceErrorCode, String errorMessage,
-    in CalleeMetadata calleeMetadata);
+    void onError(
+            in int errorCode,
+            in int isolatedServiceErrorCode,
+            in byte[] serializedExceptionInfo,
+            in CalleeMetadata calleeMetadata);
 }
diff --git a/framework/java/com/android/federatedcompute/internal/util/AndroidServiceBinder.java b/framework/java/com/android/federatedcompute/internal/util/AndroidServiceBinder.java
index 6b2d94c..91490df 100644
--- a/framework/java/com/android/federatedcompute/internal/util/AndroidServiceBinder.java
+++ b/framework/java/com/android/federatedcompute/internal/util/AndroidServiceBinder.java
@@ -39,6 +39,8 @@
     private static final String TAG = AndroidServiceBinder.class.getSimpleName();
 
     private static final int BINDER_CONNECTION_TIMEOUT_MS = 5000;
+    private static final int MAX_GET_SERVICE_RETRIES = 2;
+    private static final long GET_SERVICE_RETRY_DELAY_MS = 100L;
     private final String mServiceIntentActionOrName;
     private final List<String> mServicePackages;
     private final Function<IBinder, T> mBinderConverter;
@@ -143,6 +145,36 @@
 
     @Override
     public T getService(@NonNull Executor executor) {
+        int retryAttempts = 0;
+        T service;
+        IllegalStateException exceptionInfo = null;
+
+        while (retryAttempts < MAX_GET_SERVICE_RETRIES) {
+            try {
+                service = getServiceWithoutRetry(executor);
+                if (service != null) {
+                    return service;
+                }
+            } catch (IllegalStateException e) {
+                LogUtil.e(TAG, e, "Failed to get service on attempt " + (retryAttempts + 1));
+                exceptionInfo = e;
+            }
+            retryAttempts++;
+            try {
+                Thread.sleep(GET_SERVICE_RETRY_DELAY_MS);
+            } catch (InterruptedException e) {
+                LogUtil.w(TAG, "Thread sleep interrupted");
+            }
+        }
+
+        throw exceptionInfo != null
+            ? exceptionInfo
+            : new IllegalStateException(
+                String.format("Failed to get non-null service %s after %d retries",
+                    mServiceIntentActionOrName, retryAttempts));
+    }
+
+    private T getServiceWithoutRetry(@NonNull Executor executor) {
         synchronized (mLock) {
             if (mService != null) {
                 return mService;
@@ -183,7 +215,8 @@
                 LogUtil.i(TAG, "bindService() %s already pending...", mServiceIntentActionOrName);
             }
         }
-        // Release the lock to let the ServiceConnection set the mService
+        // Release the lock to let the ServiceConnection set the mService. If unbind race condition
+        // happen here (e.g. onBindingDied called) client should retry
         try {
             mConnectionCountDownLatch.await(BINDER_CONNECTION_TIMEOUT_MS, MILLISECONDS);
         } catch (InterruptedException e) {
@@ -271,7 +304,11 @@
         synchronized (mLock) {
             if (mServiceConnection != null) {
                 LogUtil.d(TAG, "unbinding %s...", mServiceIntentActionOrName);
-                mContext.unbindService(mServiceConnection);
+                try {
+                    mContext.unbindService(mServiceConnection);
+                } catch (IllegalArgumentException e) {
+                    LogUtil.e(TAG, e, "unbinding failed %s", mServiceIntentActionOrName);
+                }
             }
             mServiceConnection = null;
             mService = null;
diff --git a/framework/java/com/android/ondevicepersonalization/internal/util/ExceptionInfo.java b/framework/java/com/android/ondevicepersonalization/internal/util/ExceptionInfo.java
new file mode 100644
index 0000000..3482c33
--- /dev/null
+++ b/framework/java/com/android/ondevicepersonalization/internal/util/ExceptionInfo.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 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.ondevicepersonalization.internal.util;
+
+import android.annotation.NonNull;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Objects;
+
+/**
+ * Information about an exception chain in the ODP service or an IsolatedService.
+ * @hide
+ */
+public class ExceptionInfo implements Serializable {
+    /** @hide */
+    static class ExceptionInfoElement implements Serializable {
+        String mExceptionClass;
+        String mMessage;
+        StackTraceElement[] mStackTrace;
+
+        ExceptionInfoElement(@NonNull Throwable t) {
+            Objects.requireNonNull(t);
+            mExceptionClass = t.getClass().getName();
+            mMessage = t.getMessage();
+            mStackTrace = t.getStackTrace();
+        }
+    }
+
+    private ArrayList<ExceptionInfoElement> mElements;
+
+    ExceptionInfo(Throwable t, int maxDepth) {
+        Objects.requireNonNull(t);
+        mElements = new ArrayList<>();
+        int count = 0;
+        while (t != null && count < maxDepth) {
+            mElements.add(new ExceptionInfoElement(t));
+            t = t.getCause();
+            ++count;
+        }
+    }
+
+    /** Serialize to byte array. */
+    public static byte[] toByteArray(Throwable t, int maxDepth) {
+        if (t == null) {
+            return null;
+        }
+        try {
+            ExceptionInfo info = new ExceptionInfo(t, maxDepth);
+            try (ByteArrayOutputStream bs = new ByteArrayOutputStream();
+                    ObjectOutputStream os = new ObjectOutputStream(bs)) {
+                os.writeObject(info);
+                return bs.toByteArray();
+            }
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /** Deserialize from byte array. */
+    public static Exception fromByteArray(byte[] bytes) {
+        if (bytes == null) {
+            return null;
+        }
+        try (ByteArrayInputStream bs = new ByteArrayInputStream(bytes);
+                ObjectInputStream os = new ObjectInputStream(bs)) {
+            ExceptionInfo info = (ExceptionInfo) os.readObject();
+            return info.toException();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    Exception toException() {
+        try {
+            Exception e = null;
+            for (int i = mElements.size() - 1; i >= 0; --i) {
+                ExceptionInfoElement element = mElements.get(i);
+                Exception tmp = new Exception(element.mExceptionClass + ": " + element.mMessage, e);
+                tmp.setStackTrace(element.mStackTrace);
+                e = tmp;
+            }
+            return e;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+}
diff --git a/samples/odpclient/src/main/AndroidManifest.xml b/samples/odpclient/src/main/AndroidManifest.xml
index 7b63946..a10e3d8 100644
--- a/samples/odpclient/src/main/AndroidManifest.xml
+++ b/samples/odpclient/src/main/AndroidManifest.xml
@@ -17,10 +17,10 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.example.odpclient"
-          android:versionName="1.1.2" >
+          android:versionName="1.1.3" >
           <queries>
               <package android:name="com.example.odpsamplenetwork" />
-          </queries>>
+          </queries>
     <application
         android:label="@string/title_activity_main">
 
diff --git a/samples/odpclient/src/main/java/com/example/odpclient/MainActivity.java b/samples/odpclient/src/main/java/com/example/odpclient/MainActivity.java
index 2f09dfb..258f1f1 100644
--- a/samples/odpclient/src/main/java/com/example/odpclient/MainActivity.java
+++ b/samples/odpclient/src/main/java/com/example/odpclient/MainActivity.java
@@ -30,6 +30,7 @@
 import android.os.OutcomeReceiver;
 import android.os.PersistableBundle;
 import android.os.Trace;
+import android.text.method.ScrollingMovementMethod;
 import android.util.Log;
 import android.view.SurfaceControlViewHost.SurfacePackage;
 import android.view.SurfaceHolder;
@@ -37,10 +38,13 @@
 import android.view.View;
 import android.widget.Button;
 import android.widget.EditText;
-import android.widget.Toast;
+import android.widget.TextView;
+import android.widget.ViewSwitcher;
 
 import com.google.common.util.concurrent.Futures;
 
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
@@ -52,7 +56,8 @@
     private static final String SERVICE_CLASS = "com.example.odpsamplenetwork.SampleService";
     private static final String ODP_APEX = "com.google.android.ondevicepersonalization";
     private static final String ADSERVICES_APEX = "com.google.android.adservices";
-
+    private static final int SURFACE_VIEW_INDEX = 0;
+    private static final int MESSAGE_BOX_INDEX = 1;
     private EditText mTextBox;
     private Button mGetAdButton;
     private EditText mScheduleTrainingTextBox;
@@ -62,6 +67,8 @@
     private EditText mReportConversionTextBox;
     private Button mReportConversionButton;
     private SurfaceView mRenderedView;
+    private TextView mMessageBox;
+    private ViewSwitcher mViewSwitcher;
     private Context mContext;
     private static Executor sCallbackExecutor = Executors.newSingleThreadExecutor();
 
@@ -95,6 +102,9 @@
         mScheduleTrainingTextBox = findViewById(R.id.schedule_training_text_box);
         mScheduleIntervalTextBox = findViewById(R.id.schedule_interval_text_box);
         mReportConversionTextBox = findViewById(R.id.report_conversion_text_box);
+        mMessageBox = findViewById(R.id.message_box);
+        mMessageBox.setMovementMethod(new ScrollingMovementMethod());
+        mViewSwitcher = findViewById(R.id.view_switcher);
         registerGetAdButton();
         registerScheduleTrainingButton();
         registerReportConversionButton();
@@ -114,7 +124,7 @@
         mReportConversionButton.setOnClickListener(v -> reportConversion());
     }
 
-    private OnDevicePersonalizationManager getOdpManager() {
+    private OnDevicePersonalizationManager getOdpManager() throws NoClassDefFoundError {
         return mContext.getSystemService(OnDevicePersonalizationManager.class);
     }
 
@@ -125,7 +135,7 @@
             Log.i(TAG, "Starting execute() " + getResources().getString(R.string.get_ad)
                     + " with " + mTextBox.getHint().toString() + ": "
                     + mTextBox.getText().toString());
-            AtomicReference<ExecuteResult> slotResultHandle = new AtomicReference<>();
+            AtomicReference<ExecuteResult> executeResult = new AtomicReference<>();
             PersistableBundle appParams = new PersistableBundle();
             appParams.putString("keyword", mTextBox.getText().toString());
 
@@ -142,26 +152,33 @@
                             Trace.endAsyncSection("OdpClient:makeRequest:odpManager.execute", 0);
                             Log.i(TAG, "execute() success: " + result);
                             if (result != null) {
-                                slotResultHandle.set(result);
+                                executeResult.set(result);
                             } else {
                                 Log.e(TAG, "No results!");
                             }
+                            clearText();
                             latch.countDown();
                         }
 
                         @Override
                         public void onError(Exception e) {
                             Trace.endAsyncSection("OdpClient:makeRequest:odpManager.execute", 0);
-                            makeToast("execute() error: " + e.toString());
+                            showError("OdpClient:makeRequest:odpManager.execute", e);
                             latch.countDown();
                         }
                     });
             latch.await();
             Log.d(TAG, "makeRequest:odpManager.execute wait success");
 
+            if (executeResult.get() == null
+                    || executeResult.get().getSurfacePackageToken() == null) {
+                Log.i(TAG, "No surfacePackageToken returned, skipping render.");
+                return;
+            }
+
             Trace.beginAsyncSection("OdpClient:makeRequest:odpManager.requestSurfacePackage", 0);
             odpManager.requestSurfacePackage(
-                    slotResultHandle.get().getSurfacePackageToken(),
+                    executeResult.get().getSurfacePackageToken(),
                     mRenderedView.getHostToken(),
                     getDisplay().getDisplayId(),
                     mRenderedView.getWidth(),
@@ -175,6 +192,7 @@
                             Log.i(TAG,
                                     "requestSurfacePackage() success: "
                                     + surfacePackage.toString());
+                            clearText();
                             new Handler(Looper.getMainLooper()).post(() -> {
                                 if (surfacePackage != null) {
                                     mRenderedView.setChildSurfacePackage(
@@ -182,6 +200,7 @@
                                 }
                                 mRenderedView.setZOrderOnTop(true);
                                 mRenderedView.setVisibility(View.VISIBLE);
+                                mViewSwitcher.setDisplayedChild(SURFACE_VIEW_INDEX);
                             });
                         }
 
@@ -189,11 +208,12 @@
                         public void onError(Exception e) {
                             Trace.endAsyncSection(
                                     "OdpClient:makeRequest:odpManager.requestSurfacePackage", 0);
-                            makeToast("requestSurfacePackage() error: " + e.toString());
+                            showError(
+                                    "OdpClient:makeRequest:odpManager.requestSurfacePackage", e);
                         }
                     });
-        } catch (Exception e) {
-            Log.e(TAG, "Error", e);
+        } catch (Throwable e) {
+            showError("makeRequest", e);
         }
     }
 
@@ -237,6 +257,7 @@
                             Trace.endAsyncSection(
                                     "OdpClient:scheduleTraining:odpManager.execute", 0);
                             Log.i(TAG, "execute() success: " + result);
+                            clearText();
                             latch.countDown();
                         }
 
@@ -244,14 +265,14 @@
                         public void onError(Exception e) {
                             Trace.endAsyncSection(
                                     "OdpClient:scheduleTraining:odpManager.execute", 0);
-                            makeToast("execute() error: " + e.toString());
+                            showError("OdpClient:scheduleTraining:odpManager.execute", e);
                             latch.countDown();
                         }
                     });
             latch.await();
             Log.d(TAG, "scheduleTraining:odpManager.execute wait success");
-        } catch (Exception e) {
-            Log.e(TAG, "Error", e);
+        } catch (Throwable e) {
+            showError("scheduleTraining", e);
         }
     }
 
@@ -284,6 +305,7 @@
                             Trace.endAsyncSection(
                                     "OdpClient:cancelTraining:odpManager.execute", 0);
                             Log.i(TAG, "execute() success: " + result);
+                            clearText();
                             latch.countDown();
                         }
 
@@ -291,14 +313,14 @@
                         public void onError(Exception e) {
                             Trace.endAsyncSection(
                                     "OdpClient:cancelTraining:odpManager.execute", 0);
-                            makeToast("execute() error: " + e.toString());
+                            showError("OdpClient:cancelTraining:odpManager.execute", e);
                             latch.countDown();
                         }
                     });
             latch.await();
             Log.d(TAG, "cancelTraining:odpManager.execute wait success");
-        } catch (Exception e) {
-            Log.e(TAG, "Error", e);
+        } catch (Throwable e) {
+            showError("cancelTraining", e);
         }
     }
 
@@ -325,6 +347,7 @@
                             Trace.endAsyncSection(
                                     "OdpClient:reportConversion:odpManager.execute", 0);
                             Log.i(TAG, "execute() success: " + result);
+                            clearText();
                             latch.countDown();
                         }
 
@@ -332,20 +355,36 @@
                         public void onError(Exception e) {
                             Trace.endAsyncSection(
                                     "OdpClient:reportConversion:odpManager.execute", 0);
-                            makeToast("execute() error: " + e.toString());
+                            showError("OdpClient:reportConversion:odpManager.execute", e);
                             latch.countDown();
                         }
                     });
             latch.await();
             Log.d(TAG, "reportConversion:odpManager.execute wait success");
-        } catch (Exception e) {
-            Log.e(TAG, "Error", e);
+        } catch (Throwable e) {
+            showError("reportConversion", e);
         }
     }
 
-    private void makeToast(String message) {
-        Log.i(TAG, message);
-        runOnUiThread(() -> Toast.makeText(MainActivity.this, message, Toast.LENGTH_LONG).show());
+    private void showError(String message, Throwable e) {
+        Log.i(TAG, "Error: " + message, e);
+        StringWriter out = new StringWriter();
+        PrintWriter pw = new PrintWriter(out);
+        pw.println("Error: " + message);
+        e.printStackTrace(pw);
+        pw.flush();
+        showText(out.toString());
+    }
+
+    private void showText(String s) {
+        runOnUiThread(() -> {
+            mMessageBox.setText(s);
+            mViewSwitcher.setDisplayedChild(MESSAGE_BOX_INDEX);
+        });
+    }
+
+    private void clearText() {
+        runOnUiThread(() -> mMessageBox.setText(""));
     }
 
     @Override
@@ -396,7 +435,7 @@
             String versionName = packageInfo.versionName;
             Log.i(TAG, "packageName: " + packageName + ", versionName: " + versionName);
         } catch (PackageManager.NameNotFoundException e) {
-            Log.e(TAG, "can't find package name " + packageName);
+            showError("can't find package name " + packageName, e);
         }
     }
 
@@ -409,7 +448,7 @@
                 Log.i(TAG, "apexName: " + apexName + ", longVersionCode: " + apexVersionCode);
             }
         } catch (PackageManager.NameNotFoundException e) {
-            Log.e(TAG, "apex " + apexName + " not found");
+            showError("apex " + apexName + " not found", e);
         }
     }
 
diff --git a/samples/odpclient/src/main/res/drawable/border.xml b/samples/odpclient/src/main/res/drawable/border.xml
new file mode 100644
index 0000000..b552f83
--- /dev/null
+++ b/samples/odpclient/src/main/res/drawable/border.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+        android:shape="rectangle" >
+    <stroke
+        android:width="1dp"
+        android:color="@android:color/black" />
+
+    <padding
+        android:bottom="1dp"
+        android:left="1dp"
+        android:right="1dp"
+        android:top="1dp" />
+
+</shape>
+
diff --git a/samples/odpclient/src/main/res/layout/activity_main.xml b/samples/odpclient/src/main/res/layout/activity_main.xml
index ece5ec4..7d48eff 100644
--- a/samples/odpclient/src/main/res/layout/activity_main.xml
+++ b/samples/odpclient/src/main/res/layout/activity_main.xml
@@ -16,13 +16,14 @@
   -->
 
 <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent">
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
 
-    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-                  android:orientation="vertical"
-                  android:layout_width="match_parent"
-                  android:layout_height="match_parent">
+    <LinearLayout
+        android:orientation="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:paddingTop="@dimen/scrollview_padding_top">
 
         <EditText
             android:id="@+id/text_box"
@@ -48,6 +49,7 @@
             android:layout_height="wrap_content"
             android:layout_width="match_parent"
             android:hint="Population" />
+
         <EditText
             android:id="@+id/schedule_interval_text_box"
             android:inputType="numberDecimal"
@@ -87,9 +89,23 @@
             style="?android:attr/buttonBarButtonStyle"
             android:text="@string/report_conversion" />
 
-        <SurfaceView
-            android:id="@+id/rendered_view"
-            android:layout_width="200dp"
-            android:layout_height="200dp" />
+        <ViewSwitcher
+            android:id="@+id/view_switcher"
+            android:layout_height="wrap_content"
+            android:layout_width="match_parent" >
+            <SurfaceView
+                android:id="@+id/rendered_view"
+                android:layout_width="200dp"
+                android:layout_height="200dp" />
+
+            <TextView
+                android:id="@+id/message_box"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minLines="20"
+                android:scrollHorizontally="true"
+                android:background="@drawable/border" />
+
+        </ViewSwitcher>
     </LinearLayout>
-</ScrollView>
\ No newline at end of file
+</ScrollView>
diff --git a/samples/odpclient/src/main/res/values-v35/dimens.xml b/samples/odpclient/src/main/res/values-v35/dimens.xml
new file mode 100644
index 0000000..6125d30
--- /dev/null
+++ b/samples/odpclient/src/main/res/values-v35/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 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.
+  -->
+
+<resources>
+  <dimen name="scrollview_padding_top">128dp</dimen> <!-- Padding for API level 35 and higher -->
+</resources>
\ No newline at end of file
diff --git a/samples/odpclient/src/main/res/values/dimens.xml b/samples/odpclient/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..8360c27
--- /dev/null
+++ b/samples/odpclient/src/main/res/values/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 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.
+  -->
+
+<resources>
+  <dimen name="scrollview_padding_top">0dp</dimen> <!-- No padding for lower API levels -->
+</resources>
\ No newline at end of file
diff --git a/samples/odpsamplenetwork/src/main/AndroidManifest.xml b/samples/odpsamplenetwork/src/main/AndroidManifest.xml
index 2fcd2cb..a7461a7 100644
--- a/samples/odpsamplenetwork/src/main/AndroidManifest.xml
+++ b/samples/odpsamplenetwork/src/main/AndroidManifest.xml
@@ -17,7 +17,7 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.example.odpsamplenetwork"
-          android:versionName="1.1.1" >
+          android:versionName="1.1.2" >
     <application android:label="OdpSampleNetwork"
                  android:debuggable="true">
         <property android:name="android.ondevicepersonalization.ON_DEVICE_PERSONALIZATION_CONFIG"
diff --git a/samples/odpsamplenetwork/src/main/java/com/example/odpsamplenetwork/SampleHandler.java b/samples/odpsamplenetwork/src/main/java/com/example/odpsamplenetwork/SampleHandler.java
index f57b31b..cfbd60c 100644
--- a/samples/odpsamplenetwork/src/main/java/com/example/odpsamplenetwork/SampleHandler.java
+++ b/samples/odpsamplenetwork/src/main/java/com/example/odpsamplenetwork/SampleHandler.java
@@ -111,6 +111,7 @@
             "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAA"
                     + "AAXNSR0IArs4c6QAAAAtJREFUGFdjYAACAAAFAAGq1chRAAAAAElFTkSuQmCC";
     private static final byte[] TRANSPARENT_PNG_BYTES = Base64.decode(TRANSPARENT_PNG_BASE64, 0);
+    private static final int ERROR_CODE = 10;
 
     private static final ListeningExecutorService sBackgroundExecutor =
             MoreExecutors.listeningDecorator(
@@ -178,6 +179,12 @@
             @NonNull ExecuteInput input,
             @NonNull OutcomeReceiver<ExecuteOutput, IsolatedServiceException> receiver) {
         Log.d(TAG, "onExecute() started.");
+        if (input != null
+                && input.getAppParams() != null
+                && input.getAppParams().getString("keyword") != null
+                && input.getAppParams().getString("keyword").equalsIgnoreCase("crash")) {
+            throw new RuntimeException("Client-requested crash.");
+        }
         sBackgroundExecutor.execute(() -> handleOnExecute(input, receiver));
     }
 
@@ -385,12 +392,17 @@
                 if (exampleCache.containsKey(key)) {
                     example = convertToExample(exampleCache.get(key));
                 } else {
-                    String value =
+                    try {
+                        String value =
                             new String(
-                                    mRemoteData.get(String.format("example%d", key)),
-                                    StandardCharsets.UTF_8);
-                    exampleCache.put(key, value);
-                    example = convertToExample(value);
+                                mRemoteData.get(String.format("example%d", key)),
+                                StandardCharsets.UTF_8);
+                        exampleCache.put(key, value);
+                        example = convertToExample(value);
+                    } catch (Throwable e) {
+                        Log.w(TAG, "failure getting example from remote data store", e);
+                        continue;
+                    }
                 }
                 TrainingExampleRecord record =
                         new TrainingExampleRecord.Builder()
@@ -441,6 +453,13 @@
         try {
             if (input != null
                     && input.getAppParams() != null
+                    && input.getAppParams().getString("keyword") != null
+                    && input.getAppParams().getString("keyword").equalsIgnoreCase("error")) {
+                receiver.onError(new IsolatedServiceException(ERROR_CODE));
+                return;
+            }
+            if (input != null
+                    && input.getAppParams() != null
                     && input.getAppParams().getString("schedule_training") != null) {
                 Log.d(TAG, "onExecute() performing schedule training.");
                 if (input.getAppParams().getString("schedule_training").isEmpty()) {
@@ -596,7 +615,7 @@
             String content =
                     "<img src=\""
                             + impressionUrl
-                            + "\">\n"
+                            + "\" alt=\"\">\n"
                             + "<a href=\""
                             + clickUrl
                             + "\">"
@@ -921,6 +940,12 @@
                                 public void onResult(InferenceOutput result) {
                                     completer.set(result);
                                 }
+
+                                @Override
+                                public void onError(Exception e) {
+                                    Log.e(TAG, "modelManager.run() exception", e);
+                                    completer.set(null);
+                                }
                             });
                     // Used only for debugging.
                     return "getModelInferenceResultFuture";
diff --git a/samples/odpsamplenetwork/src/main/res/raw/test_data1.json b/samples/odpsamplenetwork/src/main/res/raw/test_data1.json
index 4a4cfde..9482ea3 100644
--- a/samples/odpsamplenetwork/src/main/res/raw/test_data1.json
+++ b/samples/odpsamplenetwork/src/main/res/raw/test_data1.json
@@ -23,7 +23,7 @@
     },
     {
       "key": "template1",
-      "data": "<img src=\"$impressionUrl\">\n<a href=\"$clickUrl\">${adText}!</a>"
+      "data": "<img src=\"$impressionUrl\" alt=\"\">\n<a href=\"$clickUrl\">${adText}!</a>"
     },
     {
       "key": "example1",
diff --git a/src/com/android/ondevicepersonalization/services/Flags.java b/src/com/android/ondevicepersonalization/services/Flags.java
index 11bd51b..5e64127 100644
--- a/src/com/android/ondevicepersonalization/services/Flags.java
+++ b/src/com/android/ondevicepersonalization/services/Flags.java
@@ -16,6 +16,9 @@
 
 package com.android.ondevicepersonalization.services;
 
+import android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceRequest;
+import android.adservices.ondevicepersonalization.OnDevicePersonalizationManager;
+
 import com.android.adservices.shared.common.flags.ConfigFlag;
 import com.android.adservices.shared.common.flags.FeatureFlag;
 import com.android.adservices.shared.common.flags.ModuleSharedFlags;
@@ -177,18 +180,14 @@
         return WEB_TRIGGER_FLOW_DEADLINE_SECONDS;
     }
 
-    /**
-     * Executiton deadline for example store flow.
-     */
+    /** Execution deadline for example store flow. */
     int EXAMPLE_STORE_FLOW_DEADLINE_SECONDS = 30;
 
     default int getExampleStoreFlowDeadlineSeconds() {
         return EXAMPLE_STORE_FLOW_DEADLINE_SECONDS;
     }
 
-    /**
-     * Executiton deadline for download flow.
-     */
+    /** Execution deadline for download flow. */
     int DOWNLOAD_FLOW_DEADLINE_SECONDS = 30;
 
     default int getDownloadFlowDeadlineSeconds() {
@@ -240,14 +239,6 @@
         return DEFAULT_SPE_PILOT_JOB_ENABLED;
     }
 
-    /** Set all stable flags. */
-    default void setStableFlags() {}
-
-    /** Get a stable flag based on the flag name. */
-    default Object getStableFlag(String flagName) {
-        return null;
-    }
-
     default boolean getEnableClientErrorLogging() {
         return DEFAULT_CLIENT_ERROR_LOGGING_ENABLED;
     }
@@ -266,4 +257,103 @@
     default long getAppInstallHistoryTtlInMillis() {
         return DEFAULT_APP_INSTALL_HISTORY_TTL_MILLIS;
     }
+
+    /**
+     * The probability that we will return a random integer for {@link
+     * OnDevicePersonalizationManager#executeInIsolatedService}.
+     */
+    float DEFAULT_EXECUTE_BEST_VALUE_NOISE = 0.1f;
+
+    default float getNoiseForExecuteBestValue() {
+        return DEFAULT_EXECUTE_BEST_VALUE_NOISE;
+    }
+
+    /** Default value for flag that enables aggregated error code reporting. */
+    boolean DEFAULT_AGGREGATED_ERROR_REPORTING_ENABLED = false;
+
+    default boolean getAggregatedErrorReportingEnabled() {
+        return DEFAULT_AGGREGATED_ERROR_REPORTING_ENABLED;
+    }
+
+    int DEFAULT_AGGREGATED_ERROR_REPORT_TTL_DAYS = 30;
+
+    /**
+     * TTL for aggregate counts after which they will be deleted without waiting for a successful
+     * upload attempt.
+     */
+    default int getAggregatedErrorReportingTtlInDays() {
+        return DEFAULT_AGGREGATED_ERROR_REPORT_TTL_DAYS;
+    }
+
+    String DEFAULT_AGGREGATED_ERROR_REPORTING_URL_PATH =
+            "debugreporting/v1/exceptions:report-exceptions";
+
+    /**
+     * URL suffix that the reporting job will use to send adopters daily aggregated counts of {@link
+     * android.adservices.ondevicepersonalization.IsolatedServiceException}s.
+     */
+    default String getAggregatedErrorReportingServerPath() {
+        return DEFAULT_AGGREGATED_ERROR_REPORTING_URL_PATH;
+    }
+
+    int DEFAULT_AGGREGATED_ERROR_REPORTING_THRESHOLD = 0;
+
+    /**
+     * Minimum threshold for counts of {@link
+     * android.adservices.ondevicepersonalization.IsolatedServiceException} below which counts from
+     * device won't be reported.
+     *
+     * <p>This is applied per error code.
+     */
+    default int getAggregatedErrorMinThreshold() {
+        return DEFAULT_AGGREGATED_ERROR_REPORTING_THRESHOLD;
+    }
+
+    int DEFAULT_AGGREGATED_ERROR_REPORTING_INTERVAL_HOURS = 24;
+
+    /**
+     * Interval for the periodic runs of the {@link
+     * com.android.ondevicepersonalization.services.data.errors.AggregateErrorDataReportingService}
+     * that reports counts of {@link android.adservices.ondevicepersonalization.IsolatedService}.
+     */
+    default int getAggregatedErrorReportingIntervalInHours() {
+        return DEFAULT_AGGREGATED_ERROR_REPORTING_INTERVAL_HOURS;
+    }
+
+    /**
+     * Default value for maximum int value caller can set in {@link
+     * ExecuteInIsolatedServiceRequest.OutputSpec#buildBestValueSpec}.
+     */
+    int DEFAULT_MAX_INT_VALUES = 100;
+
+    default int getMaxIntValuesLimit() {
+        return DEFAULT_MAX_INT_VALUES;
+    }
+
+    /**
+     * Default max wait time until timeout for AdServices IPC call
+     */
+    long DEFAULT_ADSERVICES_IPC_CALL_TIMEOUT_IN_MILLIS = 5000L;
+
+    default long getAdservicesIpcCallTimeoutInMillis() {
+        return DEFAULT_ADSERVICES_IPC_CALL_TIMEOUT_IN_MILLIS;
+    }
+
+    String DEFAULT_PLATFORM_DATA_FOR_TRAINING_ALLOWLIST = "";
+
+    default String getPlatformDataForTrainingAllowlist() {
+        return DEFAULT_PLATFORM_DATA_FOR_TRAINING_ALLOWLIST;
+    }
+
+    String DEFAULT_PLATFORM_DATA_FOR_EXECUTE_ALLOWLIST = "";
+
+    default String getDefaultPlatformDataForExecuteAllowlist() {
+        return DEFAULT_PLATFORM_DATA_FOR_EXECUTE_ALLOWLIST;
+    }
+
+    String DEFAULT_LOG_ISOLATED_SERVICE_ERROR_CODE_NON_AGGREGATED_ALLOWLIST = "";
+
+    default String getLogIsolatedServiceErrorCodeNonAggregatedAllowlist() {
+        return DEFAULT_LOG_ISOLATED_SERVICE_ERROR_CODE_NON_AGGREGATED_ALLOWLIST;
+    }
 }
diff --git a/src/com/android/ondevicepersonalization/services/OdpServiceException.java b/src/com/android/ondevicepersonalization/services/OdpServiceException.java
index d46a786..6089396 100644
--- a/src/com/android/ondevicepersonalization/services/OdpServiceException.java
+++ b/src/com/android/ondevicepersonalization/services/OdpServiceException.java
@@ -27,21 +27,21 @@
     private final int mErrorCode;
 
     public OdpServiceException(int errorCode) {
-        this(errorCode, "");
+        this(errorCode, "ErrorCode: " + errorCode);
     }
 
     public OdpServiceException(int errorCode, @NonNull String errorMessage) {
-        super("Error code: " + errorCode + " message: " + errorMessage);
+        super(errorMessage);
         mErrorCode = errorCode;
     }
 
     public OdpServiceException(int errorCode, @NonNull Throwable cause) {
-        this(errorCode, "", cause);
+        this(errorCode, "ErrorCode: " + errorCode, cause);
     }
 
     public OdpServiceException(
             int errorCode, @NonNull String errorMessage, @NonNull Throwable cause) {
-        super("Error code: " + errorCode + " message: " + errorMessage, cause);
+        super(errorMessage, cause);
         mErrorCode = errorCode;
     }
 
diff --git a/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfig.java b/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfig.java
index 7d995d4..398a6be 100644
--- a/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfig.java
+++ b/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfig.java
@@ -16,10 +16,6 @@
 
 package com.android.ondevicepersonalization.services;
 
-import com.android.ondevicepersonalization.services.data.user.UserDataCollectionJobService;
-import com.android.ondevicepersonalization.services.download.OnDevicePersonalizationDownloadProcessingJobService;
-import com.android.ondevicepersonalization.services.maintenance.OnDevicePersonalizationMaintenanceJobService;
-
 import java.util.Map;
 
 /** Hard-coded configs for OnDevicePersonalization */
@@ -60,42 +56,60 @@
 
     /**
      * Job ID for Download Processing Task ({@link
-     * OnDevicePersonalizationDownloadProcessingJobService})
+     * com.android.ondevicepersonalization.services.download.OnDevicePersonalizationDownloadProcessingJobService})
      */
     public static final int DOWNLOAD_PROCESSING_TASK_JOB_ID = 1004;
+
     public static final String DOWNLOAD_PROCESSING_TASK_JOB_NAME =
             "DOWNLOAD_PROCESSING_TASK_JOB";
 
-    /** Job ID for Maintenance Task ({@link OnDevicePersonalizationMaintenanceJobService}) */
+    /**
+     * Job ID for Maintenance Task ({@link
+     * com.android.ondevicepersonalization.services.maintenance.OnDevicePersonalizationMaintenanceJobService})
+     */
     public static final int MAINTENANCE_TASK_JOB_ID = 1005;
+
     public static final String MAINTENANCE_TASK_JOB_NAME =
             "MAINTENANCE_TASK_JOB";
 
-    /** Job ID for User Data Collection Task ({@link UserDataCollectionJobService}) */
+    /**
+     * Job ID for User Data Collection Task ({@link
+     * com.android.ondevicepersonalization.services.data.user.UserDataCollectionJobService})
+     */
     public static final int USER_DATA_COLLECTION_ID = 1006;
+
     public static final String USER_DATA_COLLECTION_JOB_NAME =
             "USER_DATA_COLLECTION_JOB";
 
-    /** Job ID for Reset Task ({@link ResetDataJobService}) */
+    /**
+     * Job ID for Reset Task ({@link
+     * com.android.ondevicepersonalization.services.reset.ResetDataJobService})
+     */
     public static final int RESET_DATA_JOB_ID = 1007;
+
     public static final String RESET_DATA_JOB_NAME = "RESET_JOB";
 
-    public static final Map<Integer, String> JOB_ID_TO_NAME_MAP = Map.of(
-            MDD_MAINTENANCE_PERIODIC_TASK_JOB_ID,
-            MDD_MAINTENANCE_PERIODIC_TASK_JOB_NAME,
-            MDD_CHARGING_PERIODIC_TASK_JOB_ID,
-            MDD_CHARGING_PERIODIC_TASK_JOB_NAME,
-            MDD_CELLULAR_CHARGING_PERIODIC_TASK_JOB_ID,
-            MDD_CELLULAR_CHARGING_PERIODIC_TASK_JOB_NAME,
-            MDD_WIFI_CHARGING_PERIODIC_TASK_JOB_ID,
-            MDD_WIFI_CHARGING_PERIODIC_TASK_JOB_NAME,
-            DOWNLOAD_PROCESSING_TASK_JOB_ID,
-            DOWNLOAD_PROCESSING_TASK_JOB_NAME,
-            MAINTENANCE_TASK_JOB_ID,
-            MAINTENANCE_TASK_JOB_NAME,
-            USER_DATA_COLLECTION_ID,
-            USER_DATA_COLLECTION_JOB_NAME,
-            RESET_DATA_JOB_ID,
-            RESET_DATA_JOB_NAME
-    );
+    public static final int AGGREGATE_ERROR_DATA_REPORTING_JOB_ID = 1008;
+    public static final String AGGREGATED_ERROR_DATA_REPORTING_JOB_NAME =
+            "ERROR_DATA_REPORTING_JOB";
+    public static final Map<Integer, String> JOB_ID_TO_NAME_MAP =
+            Map.of(
+                    MDD_MAINTENANCE_PERIODIC_TASK_JOB_ID,
+                    MDD_MAINTENANCE_PERIODIC_TASK_JOB_NAME,
+                    MDD_CHARGING_PERIODIC_TASK_JOB_ID,
+                    MDD_CHARGING_PERIODIC_TASK_JOB_NAME,
+                    MDD_CELLULAR_CHARGING_PERIODIC_TASK_JOB_ID,
+                    MDD_CELLULAR_CHARGING_PERIODIC_TASK_JOB_NAME,
+                    MDD_WIFI_CHARGING_PERIODIC_TASK_JOB_ID,
+                    MDD_WIFI_CHARGING_PERIODIC_TASK_JOB_NAME,
+                    DOWNLOAD_PROCESSING_TASK_JOB_ID,
+                    DOWNLOAD_PROCESSING_TASK_JOB_NAME,
+                    MAINTENANCE_TASK_JOB_ID,
+                    MAINTENANCE_TASK_JOB_NAME,
+                    USER_DATA_COLLECTION_ID,
+                    USER_DATA_COLLECTION_JOB_NAME,
+                    RESET_DATA_JOB_ID,
+                    RESET_DATA_JOB_NAME,
+                    AGGREGATE_ERROR_DATA_REPORTING_JOB_ID,
+                    AGGREGATED_ERROR_DATA_REPORTING_JOB_NAME);
 }
diff --git a/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfigServiceDelegate.java b/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfigServiceDelegate.java
deleted file mode 100644
index 4769e24..0000000
--- a/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfigServiceDelegate.java
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.ondevicepersonalization.services;
-
-import static android.adservices.ondevicepersonalization.OnDevicePersonalizationPermissions.MODIFY_ONDEVICEPERSONALIZATION_STATE;
-
-import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__API_CALLBACK_ERROR;
-import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__API_REMOTE_EXCEPTION;
-import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ON_DEVICE_PERSONALIZATION_ERROR;
-import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__ODP;
-
-import android.adservices.ondevicepersonalization.Constants;
-import android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationConfigService;
-import android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationConfigServiceCallback;
-import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.ondevicepersonalization.IOnDevicePersonalizationSystemService;
-import android.ondevicepersonalization.IOnDevicePersonalizationSystemServiceCallback;
-import android.ondevicepersonalization.OnDevicePersonalizationSystemServiceManager;
-import android.os.Binder;
-import android.os.Bundle;
-import android.os.RemoteException;
-
-import com.android.modules.utils.build.SdkLevel;
-import com.android.ondevicepersonalization.internal.util.LoggerFactory;
-import com.android.ondevicepersonalization.services.data.user.RawUserData;
-import com.android.ondevicepersonalization.services.data.user.UserDataCollector;
-import com.android.ondevicepersonalization.services.data.user.UserPrivacyStatus;
-import com.android.ondevicepersonalization.services.statsd.errorlogging.ClientErrorLogger;
-
-import java.util.Objects;
-import java.util.concurrent.Executor;
-
-/**
- * ODP service that modifies and persists ODP enablement status
- */
-public class OnDevicePersonalizationConfigServiceDelegate
-        extends IOnDevicePersonalizationConfigService.Stub {
-    private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
-    private static final String TAG = "OnDevicePersonalizationConfigServiceDelegate";
-    private final Context mContext;
-    private static final Executor sBackgroundExecutor =
-            OnDevicePersonalizationExecutors.getBackgroundExecutor();
-    private static final int SERVICE_NOT_IMPLEMENTED = 501;
-
-    public OnDevicePersonalizationConfigServiceDelegate(Context context) {
-        mContext = context;
-    }
-
-    @Override
-    @RequiresPermission(MODIFY_ONDEVICEPERSONALIZATION_STATE)
-    public void setPersonalizationStatus(boolean enabled,
-                                     @NonNull IOnDevicePersonalizationConfigServiceCallback
-                                             callback) {
-        if (getGlobalKillSwitch()) {
-            throw new IllegalStateException("Service skipped as the API flag is turned off.");
-        }
-
-        // Verify caller's permission
-        if (mContext.checkCallingPermission(MODIFY_ONDEVICEPERSONALIZATION_STATE)
-                != PackageManager.PERMISSION_GRANTED) {
-            throw new SecurityException(
-                    "Permission denied: " + MODIFY_ONDEVICEPERSONALIZATION_STATE);
-        }
-        Objects.requireNonNull(callback);
-
-        sBackgroundExecutor.execute(
-                () -> {
-                    try {
-                        UserPrivacyStatus userPrivacyStatus = UserPrivacyStatus.getInstance();
-
-                        boolean oldStatus = userPrivacyStatus.isPersonalizationStatusEnabled();
-                        userPrivacyStatus.setPersonalizationStatusEnabled(enabled);
-                        boolean newStatus = userPrivacyStatus.isPersonalizationStatusEnabled();
-
-                        if (oldStatus == newStatus) {
-                            sendSuccess(callback);
-                            return;
-                        }
-
-                        // Rollback all user data if personalization status changes
-                        RawUserData userData = RawUserData.getInstance();
-                        UserDataCollector userDataCollector =
-                                UserDataCollector.getInstance(mContext);
-                        userDataCollector.clearUserData(userData);
-                        userDataCollector.clearMetadata();
-
-                        // TODO(b/302018665): replicate system server storage to T devices.
-                        if (!SdkLevel.isAtLeastU()) {
-                            userPrivacyStatus.setPersonalizationStatusEnabled(enabled);
-                            sendSuccess(callback);
-                            return;
-                        }
-                        // Persist in the system server for U+ devices
-                        OnDevicePersonalizationSystemServiceManager systemServiceManager =
-                                mContext.getSystemService(
-                                        OnDevicePersonalizationSystemServiceManager.class);
-                        // Cannot find system server on U+.
-                        if (systemServiceManager == null) {
-                            sendError(callback, SERVICE_NOT_IMPLEMENTED);
-                            return;
-                        }
-                        IOnDevicePersonalizationSystemService systemService =
-                                systemServiceManager.getService();
-                        // The system service is not ready.
-                        if (systemService == null) {
-                            sendError(callback, SERVICE_NOT_IMPLEMENTED);
-                            return;
-                        }
-                        try {
-                            systemService.setPersonalizationStatus(
-                                    enabled,
-                                    new IOnDevicePersonalizationSystemServiceCallback.Stub() {
-                                        @Override
-                                        public void onResult(Bundle bundle) throws RemoteException {
-                                            userPrivacyStatus.setPersonalizationStatusEnabled(
-                                                    enabled);
-                                            callback.onSuccess();
-                                        }
-
-                                        @Override
-                                        public void onError(int errorCode) throws RemoteException {
-                                            callback.onFailure(errorCode);
-                                        }
-                                    });
-                        } catch (RemoteException re) {
-                            sLogger.e(TAG + ": Unable to send result to the callback.", re);
-                            ClientErrorLogger.getInstance()
-                                    .logErrorWithExceptionInfo(
-                                            re,
-                                            AD_SERVICES_ERROR_REPORTED__ERROR_CODE__API_REMOTE_EXCEPTION,
-                                            AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__ODP);
-                        }
-                    } catch (Exception e) {
-                        sLogger.e(TAG + ": Failed to set personalization status.", e);
-                        ClientErrorLogger.getInstance()
-                                .logErrorWithExceptionInfo(
-                                        e,
-                                        AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ON_DEVICE_PERSONALIZATION_ERROR,
-                                        AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__ODP);
-                        sendError(callback, Constants.STATUS_INTERNAL_ERROR);
-                    }
-                });
-    }
-
-    private void sendSuccess(
-            @NonNull IOnDevicePersonalizationConfigServiceCallback callback) {
-        try {
-            callback.onSuccess();
-        } catch (RemoteException e) {
-            sLogger.e(TAG + ": Callback error", e);
-            ClientErrorLogger.getInstance()
-                    .logErrorWithExceptionInfo(
-                            e,
-                            AD_SERVICES_ERROR_REPORTED__ERROR_CODE__API_CALLBACK_ERROR,
-                            AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__ODP);
-        }
-    }
-
-    private void sendError(
-            @NonNull IOnDevicePersonalizationConfigServiceCallback callback, int errorCode) {
-        try {
-            callback.onFailure(errorCode);
-        } catch (RemoteException e) {
-            sLogger.e(TAG + ": Callback error", e);
-            ClientErrorLogger.getInstance()
-                    .logErrorWithExceptionInfo(
-                            e,
-                            AD_SERVICES_ERROR_REPORTED__ERROR_CODE__API_CALLBACK_ERROR,
-                            AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__ODP);
-        }
-    }
-
-    private boolean getGlobalKillSwitch() {
-        long origId = Binder.clearCallingIdentity();
-        boolean globalKillSwitch = FlagsFactory.getFlags().getGlobalKillSwitch();
-        Binder.restoreCallingIdentity(origId);
-        return globalKillSwitch;
-    }
-}
diff --git a/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfigServiceImpl.java b/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfigServiceImpl.java
deleted file mode 100644
index 53b50e5..0000000
--- a/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfigServiceImpl.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.ondevicepersonalization.services;
-
-import android.app.Service;
-import android.content.Intent;
-import android.os.IBinder;
-
-/**
- * ODP service that modifies and persists user's privacy status.
- */
-public class OnDevicePersonalizationConfigServiceImpl extends Service {
-
-    /** Binder interface. */
-    private OnDevicePersonalizationConfigServiceDelegate mBinder;
-
-    @Override
-    public void onCreate() {
-        mBinder = new OnDevicePersonalizationConfigServiceDelegate(this);
-    }
-
-    @Override
-    public IBinder onBind(Intent intent) {
-        return mBinder;
-    }
-}
diff --git a/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationExecutors.java b/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationExecutors.java
index 8849a0e..5eb3819 100644
--- a/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationExecutors.java
+++ b/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationExecutors.java
@@ -39,7 +39,7 @@
     private static final ListeningExecutorService sHighPriorityBackgroundExecutor =
             MoreExecutors.listeningDecorator(
                     Executors.newFixedThreadPool(
-                            /* nThreads */ 2,
+                            /* nThreads */ 4,
                             createThreadFactory(
                                     "HPBG Thread",
                                     Process.THREAD_PRIORITY_BACKGROUND
@@ -49,7 +49,7 @@
     private static final ListeningExecutorService sLowPriorityBackgroundExecutor =
             MoreExecutors.listeningDecorator(
                     Executors.newFixedThreadPool(
-                            /* nThreads */ 2,
+                            /* nThreads */ 4,
                             createThreadFactory(
                                     "LPBG Thread",
                                     Process.THREAD_PRIORITY_BACKGROUND
diff --git a/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationManagingServiceDelegate.java b/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationManagingServiceDelegate.java
index b52a773..4d5d7ac 100644
--- a/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationManagingServiceDelegate.java
+++ b/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationManagingServiceDelegate.java
@@ -20,6 +20,8 @@
 
 import android.adservices.ondevicepersonalization.CallerMetadata;
 import android.adservices.ondevicepersonalization.Constants;
+import android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceRequest;
+import android.adservices.ondevicepersonalization.ExecuteOptionsParcel;
 import android.adservices.ondevicepersonalization.aidl.IExecuteCallback;
 import android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationManagingService;
 import android.adservices.ondevicepersonalization.aidl.IRegisterMeasurementEventCallback;
@@ -34,6 +36,7 @@
 import android.os.SystemClock;
 import android.os.Trace;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.odp.module.common.DeviceUtils;
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 import com.android.ondevicepersonalization.services.enrollment.PartnerEnrollmentChecker;
@@ -51,9 +54,23 @@
     private static final String TAG = "OnDevicePersonalizationManagingServiceDelegate";
     private static final ServiceFlowOrchestrator sSfo = ServiceFlowOrchestrator.getInstance();
     @NonNull private final Context mContext;
+    private final Injector mInjector;
 
     public OnDevicePersonalizationManagingServiceDelegate(@NonNull Context context) {
+        this(context, new Injector());
+    }
+
+    @VisibleForTesting
+    public OnDevicePersonalizationManagingServiceDelegate(
+            @NonNull Context context, Injector injector) {
         mContext = Objects.requireNonNull(context);
+        mInjector = injector;
+    }
+
+    static class Injector {
+        Flags getFlags() {
+            return FlagsFactory.getFlags();
+        }
     }
 
     @Override
@@ -67,6 +84,7 @@
             @NonNull ComponentName handler,
             @NonNull Bundle wrappedParams,
             @NonNull CallerMetadata metadata,
+            @NonNull ExecuteOptionsParcel options,
             @NonNull IExecuteCallback callback) {
         if (getGlobalKillSwitch()) {
             throw new IllegalStateException("Service skipped as the global kill switch is on.");
@@ -95,13 +113,22 @@
             throw new IllegalArgumentException("missing service class name");
         }
 
+        checkExecutionsOptions(options);
+
         final int uid = Binder.getCallingUid();
         enforceCallingPackageBelongsToUid(callingPackageName, uid);
         enforceEnrollment(callingPackageName, handler);
 
-        sSfo.schedule(ServiceFlowType.APP_REQUEST_FLOW,
-                callingPackageName, handler, wrappedParams,
-                callback, mContext, metadata.getStartTimeMillis(), serviceEntryTimeMillis);
+        sSfo.schedule(
+                ServiceFlowType.APP_REQUEST_FLOW,
+                callingPackageName,
+                handler,
+                wrappedParams,
+                callback,
+                mContext,
+                metadata.getStartTimeMillis(),
+                serviceEntryTimeMillis,
+                options);
         Trace.endSection();
     }
 
@@ -217,8 +244,7 @@
 
     private boolean getGlobalKillSwitch() {
         long origId = Binder.clearCallingIdentity();
-        boolean globalKillSwitch = FlagsFactory.getFlags().getGlobalKillSwitch();
-        FlagsFactory.getFlags().setStableFlags();
+        boolean globalKillSwitch = mInjector.getFlags().getGlobalKillSwitch();
         Binder.restoreCallingIdentity(origId);
         return globalKillSwitch;
     }
@@ -257,4 +283,19 @@
             Binder.restoreCallingIdentity(origId);
         }
     }
+
+    private void checkExecutionsOptions(@NonNull ExecuteOptionsParcel options) {
+        long origId = Binder.clearCallingIdentity();
+        try {
+            if (options.getOutputType()
+                    == ExecuteInIsolatedServiceRequest.OutputSpec.OUTPUT_TYPE_BEST_VALUE
+                    && options.getMaxIntValue() > mInjector.getFlags().getMaxIntValuesLimit()) {
+                throw new IllegalArgumentException(
+                        "The maxIntValue in OutputSpec can not exceed limit "
+                                + mInjector.getFlags().getMaxIntValuesLimit());
+            }
+        } finally {
+            Binder.restoreCallingIdentity(origId);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/com/android/ondevicepersonalization/services/PhFlags.java b/src/com/android/ondevicepersonalization/services/PhFlags.java
index a71f6ec..00d89a2 100644
--- a/src/com/android/ondevicepersonalization/services/PhFlags.java
+++ b/src/com/android/ondevicepersonalization/services/PhFlags.java
@@ -18,7 +18,9 @@
 
 import android.annotation.NonNull;
 import android.provider.DeviceConfig;
+
 import com.android.modules.utils.build.SdkLevel;
+
 import java.util.HashMap;
 import java.util.Map;
 
@@ -68,9 +70,6 @@
     public static final String KEY_ODP_ENABLE_CLIENT_ERROR_LOGGING =
             "odp_enable_client_error_logging";
 
-    public static final String KEY_ODP_BACKGROUND_JOBS_LOGGING_ENABLED =
-            "odp_background_jobs_logging_enabled";
-
     public static final String KEY_ODP_BACKGROUND_JOB_SAMPLING_LOGGING_RATE =
             "odp_background_job_sampling_logging_rate";
 
@@ -95,16 +94,41 @@
     public static final String KEY_RESET_DATA_DEADLINE_SECONDS = "reset_data_deadline_seconds";
 
     public static final String APP_INSTALL_HISTORY_TTL = "app_install_history_ttl";
+    public static final String EXECUTE_BEST_VALUE_NOISE = "noise_for_execute_best_value";
+
+    public static final String KEY_ENABLE_AGGREGATED_ERROR_REPORTING =
+            "enable_aggregated_error_reporting";
+
+    public static final String KEY_AGGREGATED_ERROR_REPORT_TTL_DAYS =
+            "aggregated_error_report_ttl_days";
+
+    public static final String KEY_AGGREGATED_ERROR_REPORTING_PATH =
+            "aggregated_error_reporting_path";
+
+    public static final String KEY_AGGREGATED_ERROR_REPORTING_THRESHOLD =
+            "aggregated_error_reporting_threshold";
+
+    public static final String KEY_AGGREGATED_ERROR_REPORTING_INTERVAL_HOURS =
+            "aggregated_error_reporting_interval_hours";
+
+    public static final String MAX_INT_VALUES_LIMIT = "max_int_values_limit";
+
+    public static final String KEY_ADSERVICES_IPC_CALL_TIMEOUT_IN_MILLIS =
+            "adservices_ipc_call_timeout_in_millis";
+    public static final String KEY_PLATFORM_DATA_FOR_TRAINING_ALLOWLIST =
+            "platform_data_for_training_allowlist";
+    public static final String KEY_PLATFORM_DATA_FOR_EXECUTE_ALLOWLIST =
+            "platform_data_for_execute_allowlist";
+
+    public static final String KEY_LOG_ISOLATED_SERVICE_ERROR_CODE_NON_AGGREGATED_ALLOWLIST =
+            "log_isolated_service_error_code_non_aggregated_allowlist";
 
     // OnDevicePersonalization Namespace String from DeviceConfig class
     public static final String NAMESPACE_ON_DEVICE_PERSONALIZATION = "on_device_personalization";
 
     private final Map<String, Object> mStableFlags = new HashMap<>();
 
-    PhFlags() {
-        // This is only called onece so stable flags require process restart to be reset.
-        setStableFlags();
-    }
+    PhFlags() {}
 
     /** Returns the singleton instance of the PhFlags. */
     @NonNull
@@ -116,42 +140,6 @@
         private static final PhFlags sSingleton = new PhFlags();
     }
 
-    /** Sets the stable flag map. */
-    public void setStableFlags() {
-        mStableFlags.put(KEY_APP_REQUEST_FLOW_DEADLINE_SECONDS,
-                getAppRequestFlowDeadlineSeconds());
-        mStableFlags.put(KEY_RENDER_FLOW_DEADLINE_SECONDS,
-                getRenderFlowDeadlineSeconds());
-        mStableFlags.put(KEY_WEB_TRIGGER_FLOW_DEADLINE_SECONDS,
-                getWebTriggerFlowDeadlineSeconds());
-        mStableFlags.put(KEY_WEB_VIEW_FLOW_DEADLINE_SECONDS,
-                getWebViewFlowDeadlineSeconds());
-        mStableFlags.put(KEY_EXAMPLE_STORE_FLOW_DEADLINE_SECONDS,
-                getExampleStoreFlowDeadlineSeconds());
-        mStableFlags.put(KEY_DOWNLOAD_FLOW_DEADLINE_SECONDS,
-                getDownloadFlowDeadlineSeconds());
-        mStableFlags.put(KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED,
-                isSharedIsolatedProcessFeatureEnabled());
-        mStableFlags.put(KEY_TRUSTED_PARTNER_APPS_LIST,
-                getTrustedPartnerAppsList());
-        mStableFlags.put(KEY_IS_ART_IMAGE_LOADING_OPTIMIZATION_ENABLED,
-                isArtImageLoadingOptimizationEnabled());
-        mStableFlags.put(KEY_ENABLE_PERSONALIZATION_STATUS_OVERRIDE,
-                isPersonalizationStatusOverrideEnabled());
-        mStableFlags.put(KEY_PERSONALIZATION_STATUS_OVERRIDE_VALUE,
-                getPersonalizationStatusOverrideValue());
-        mStableFlags.put(KEY_USER_CONTROL_CACHE_IN_MILLIS,
-                getUserControlCacheInMillis());
-    }
-
-    /** Gets a stable flag value based on flag name. */
-    public Object getStableFlag(String flagName) {
-        if (!mStableFlags.containsKey(flagName)) {
-            throw new IllegalArgumentException("Flag " + flagName + " is not stable.");
-        }
-        return mStableFlags.get(flagName);
-    }
-
     // Group of All Killswitches
     @Override
     public boolean getGlobalKillSwitch() {
@@ -313,18 +301,17 @@
                 /* defaultValue= */ DEFAULT_CLIENT_ERROR_LOGGING_ENABLED);
     }
 
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>This method always return {@code true} because the underlying flag is fully launched on
+     * {@code OnDevicePersonalization} but the method cannot be removed (as it's defined on {@code
+     * ModuleSharedFlags}).
+     */
     @Override
     public boolean getBackgroundJobsLoggingEnabled() {
-        // needs stable: execution stats may be less accurate if flag changed during job execution
-        return (boolean)
-                mStableFlags.computeIfAbsent(
-                        KEY_ODP_BACKGROUND_JOBS_LOGGING_ENABLED,
-                        key -> {
-                            return DeviceConfig.getBoolean(
-                                    /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
-                                    /* name= */ KEY_ODP_BACKGROUND_JOBS_LOGGING_ENABLED,
-                                    /* defaultValue= */ BACKGROUND_JOB_LOGGING_ENABLED);
-                        });
+        return true;
     }
 
     @Override
@@ -398,4 +385,93 @@
                 /* name= */ APP_INSTALL_HISTORY_TTL,
                 /* defaultValue= */ DEFAULT_APP_INSTALL_HISTORY_TTL_MILLIS);
     }
+
+    @Override
+    public float getNoiseForExecuteBestValue() {
+        return DeviceConfig.getFloat(
+                /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                /* name= */ EXECUTE_BEST_VALUE_NOISE,
+                /* defaultValue= */ DEFAULT_EXECUTE_BEST_VALUE_NOISE);
+    }
+
+    @Override
+    public boolean getAggregatedErrorReportingEnabled() {
+        return DeviceConfig.getBoolean(
+                /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                /* name= */ KEY_ENABLE_AGGREGATED_ERROR_REPORTING,
+                /* defaultValue= */ DEFAULT_AGGREGATED_ERROR_REPORTING_ENABLED);
+    }
+
+    @Override
+    public int getAggregatedErrorReportingTtlInDays() {
+        return DeviceConfig.getInt(
+                /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                /* name= */ KEY_AGGREGATED_ERROR_REPORT_TTL_DAYS,
+                /* defaultValue= */ DEFAULT_AGGREGATED_ERROR_REPORT_TTL_DAYS);
+    }
+
+    @Override
+    public String getAggregatedErrorReportingServerPath() {
+        return DeviceConfig.getString(
+                /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                /* name= */ KEY_AGGREGATED_ERROR_REPORTING_PATH,
+                /* defaultValue= */ DEFAULT_AGGREGATED_ERROR_REPORTING_URL_PATH);
+    }
+
+    @Override
+    public int getAggregatedErrorMinThreshold() {
+        return DeviceConfig.getInt(
+                /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                /* name= */ KEY_AGGREGATED_ERROR_REPORTING_THRESHOLD,
+                /* defaultValue= */ DEFAULT_AGGREGATED_ERROR_REPORTING_THRESHOLD);
+    }
+
+    @Override
+    public int getAggregatedErrorReportingIntervalInHours() {
+        return DeviceConfig.getInt(
+                /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                /* name= */ KEY_AGGREGATED_ERROR_REPORTING_INTERVAL_HOURS,
+                /* defaultValue= */ DEFAULT_AGGREGATED_ERROR_REPORTING_INTERVAL_HOURS);
+    }
+
+    @Override
+    public int getMaxIntValuesLimit() {
+        return DeviceConfig.getInt(
+                /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                /* name= */ MAX_INT_VALUES_LIMIT,
+                /* defaultValue= */ DEFAULT_MAX_INT_VALUES);
+    }
+
+    @Override
+    public long getAdservicesIpcCallTimeoutInMillis() {
+        return DeviceConfig.getLong(
+                /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                /* name= */ KEY_ADSERVICES_IPC_CALL_TIMEOUT_IN_MILLIS,
+                /* defaultValue= */ DEFAULT_ADSERVICES_IPC_CALL_TIMEOUT_IN_MILLIS);
+    }
+
+    @Override
+    public String getPlatformDataForTrainingAllowlist() {
+        return DeviceConfig.getString(
+                /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                /* name= */ KEY_PLATFORM_DATA_FOR_TRAINING_ALLOWLIST,
+                /* defaultValue= */ DEFAULT_PLATFORM_DATA_FOR_TRAINING_ALLOWLIST);
+    }
+
+    @Override
+    public String getDefaultPlatformDataForExecuteAllowlist() {
+        return DeviceConfig.getString(
+                /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                /* name= */ KEY_PLATFORM_DATA_FOR_EXECUTE_ALLOWLIST,
+                /* defaultValue= */ DEFAULT_PLATFORM_DATA_FOR_EXECUTE_ALLOWLIST);
+    }
+
+    @Override
+    public String getLogIsolatedServiceErrorCodeNonAggregatedAllowlist() {
+        return DeviceConfig.getString(
+                /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                /* name= */ KEY_LOG_ISOLATED_SERVICE_ERROR_CODE_NON_AGGREGATED_ALLOWLIST,
+                /* defaultValue= */
+                DEFAULT_LOG_ISOLATED_SERVICE_ERROR_CODE_NON_AGGREGATED_ALLOWLIST);
+    }
 }
diff --git a/src/com/android/ondevicepersonalization/services/StableFlags.java b/src/com/android/ondevicepersonalization/services/StableFlags.java
new file mode 100644
index 0000000..3a7141e
--- /dev/null
+++ b/src/com/android/ondevicepersonalization/services/StableFlags.java
@@ -0,0 +1,90 @@
+/*
+ * 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.ondevicepersonalization.services;
+
+import android.os.Binder;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Container of process-stable flags.
+ */
+public class StableFlags {
+    private static final Object sLock = new Object();
+    private static volatile StableFlags sStableFlags = null;
+
+    private final Map<String, Object> mStableFlagsMap = new HashMap<>();
+
+    /** Returns the value of the named stable flag. */
+    public static Object get(String flagName) {
+        return getInstance().getStableFlag(flagName);
+
+    }
+
+    /** Returns the singleton instance of StableFlags. */
+    @VisibleForTesting
+    public static StableFlags getInstance() {
+        if (sStableFlags == null) {
+            synchronized (sLock) {
+                if (sStableFlags == null) {
+                    long origId = Binder.clearCallingIdentity();
+                    sStableFlags = new StableFlags(FlagsFactory.getFlags());
+                    Binder.restoreCallingIdentity(origId);
+                }
+            }
+        }
+        return sStableFlags;
+    }
+
+    @VisibleForTesting
+    StableFlags(Flags flags) {
+        mStableFlagsMap.put(PhFlags.KEY_APP_REQUEST_FLOW_DEADLINE_SECONDS,
+                flags.getAppRequestFlowDeadlineSeconds());
+        mStableFlagsMap.put(PhFlags.KEY_RENDER_FLOW_DEADLINE_SECONDS,
+                flags.getRenderFlowDeadlineSeconds());
+        mStableFlagsMap.put(PhFlags.KEY_WEB_TRIGGER_FLOW_DEADLINE_SECONDS,
+                flags.getWebTriggerFlowDeadlineSeconds());
+        mStableFlagsMap.put(PhFlags.KEY_WEB_VIEW_FLOW_DEADLINE_SECONDS,
+                flags.getWebViewFlowDeadlineSeconds());
+        mStableFlagsMap.put(PhFlags.KEY_EXAMPLE_STORE_FLOW_DEADLINE_SECONDS,
+                flags.getExampleStoreFlowDeadlineSeconds());
+        mStableFlagsMap.put(PhFlags.KEY_DOWNLOAD_FLOW_DEADLINE_SECONDS,
+                flags.getDownloadFlowDeadlineSeconds());
+        mStableFlagsMap.put(PhFlags.KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED,
+                flags.isSharedIsolatedProcessFeatureEnabled());
+        mStableFlagsMap.put(PhFlags.KEY_TRUSTED_PARTNER_APPS_LIST,
+                flags.getTrustedPartnerAppsList());
+        mStableFlagsMap.put(PhFlags.KEY_IS_ART_IMAGE_LOADING_OPTIMIZATION_ENABLED,
+                flags.isArtImageLoadingOptimizationEnabled());
+        mStableFlagsMap.put(PhFlags.KEY_ENABLE_PERSONALIZATION_STATUS_OVERRIDE,
+                flags.isPersonalizationStatusOverrideEnabled());
+        mStableFlagsMap.put(PhFlags.KEY_PERSONALIZATION_STATUS_OVERRIDE_VALUE,
+                flags.getPersonalizationStatusOverrideValue());
+        mStableFlagsMap.put(PhFlags.KEY_USER_CONTROL_CACHE_IN_MILLIS,
+                flags.getUserControlCacheInMillis());
+    }
+
+    private Object getStableFlag(String flagName) {
+        if (!mStableFlagsMap.containsKey(flagName)) {
+            throw new IllegalArgumentException("Flag " + flagName + " is not stable.");
+        }
+        return mStableFlagsMap.get(flagName);
+    }
+}
diff --git a/src/com/android/ondevicepersonalization/services/data/errors/AggregateErrorDataReportingService.java b/src/com/android/ondevicepersonalization/services/data/errors/AggregateErrorDataReportingService.java
new file mode 100644
index 0000000..075290d
--- /dev/null
+++ b/src/com/android/ondevicepersonalization/services/data/errors/AggregateErrorDataReportingService.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2024 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.ondevicepersonalization.services.data.errors;
+
+import static android.app.job.JobScheduler.RESULT_FAILURE;
+
+import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_JOB_NOT_CONFIGURED;
+import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON;
+import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.AGGREGATE_ERROR_DATA_REPORTING_JOB_ID;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.ondevicepersonalization.internal.util.LoggerFactory;
+import com.android.ondevicepersonalization.services.Flags;
+import com.android.ondevicepersonalization.services.FlagsFactory;
+import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
+import com.android.ondevicepersonalization.services.statsd.joblogging.OdpJobServiceLogger;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+
+/** {@link JobService} to perform daily reporting of aggregated error codes. */
+public class AggregateErrorDataReportingService extends JobService {
+    private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
+    private static final String TAG = AggregateErrorDataReportingService.class.getSimpleName();
+
+    private ListenableFuture<Void> mFuture;
+
+    private final Injector mInjector;
+
+    public AggregateErrorDataReportingService() {
+        this(new Injector());
+    }
+
+    @VisibleForTesting
+    AggregateErrorDataReportingService(Injector injector) {
+        mInjector = injector;
+    }
+
+    static class Injector {
+        ListeningExecutorService getExecutor() {
+            return OnDevicePersonalizationExecutors.getBackgroundExecutor();
+        }
+
+        Flags getFlags() {
+            return FlagsFactory.getFlags();
+        }
+    }
+
+    /** Schedules a unique instance of the {@link AggregateErrorDataReportingService} to be run. */
+    public static int scheduleIfNeeded(Context context) {
+        return scheduleIfNeeded(context, FlagsFactory.getFlags());
+    }
+
+    @VisibleForTesting
+    static int scheduleIfNeeded(Context context, Flags flags) {
+        if (!flags.getAggregatedErrorReportingEnabled()) {
+            sLogger.d(TAG + ": Aggregate error reporting is disabled.");
+            return RESULT_FAILURE;
+        }
+
+        JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+        if (jobScheduler.getPendingJob(AGGREGATE_ERROR_DATA_REPORTING_JOB_ID) != null) {
+            sLogger.d(TAG + ": Job is already scheduled. Doing nothing.");
+            return RESULT_FAILURE;
+        }
+
+        ComponentName serviceComponent =
+                new ComponentName(context, AggregateErrorDataReportingService.class);
+        JobInfo.Builder builder =
+                new JobInfo.Builder(AGGREGATE_ERROR_DATA_REPORTING_JOB_ID, serviceComponent);
+
+        // Constraints
+        builder.setRequiresDeviceIdle(true);
+        builder.setRequiresBatteryNotLow(true);
+        builder.setRequiresStorageNotLow(true);
+        builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);
+        builder.setPeriodic(
+                1000L
+                        * FlagsFactory.getFlags().getAggregatedErrorReportingIntervalInHours()
+                        * 3600L); // JobScheduler uses Milliseconds.
+        // persist this job across boots
+        builder.setPersisted(true);
+
+        return jobScheduler.schedule(builder.build());
+    }
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        sLogger.d(TAG + ": onStartJob()");
+        OdpJobServiceLogger.getInstance(this)
+                .recordOnStartJob(AGGREGATE_ERROR_DATA_REPORTING_JOB_ID);
+        if (mInjector.getFlags().getGlobalKillSwitch()) {
+            sLogger.d(TAG + ": GlobalKillSwitch enabled, finishing job.");
+            return cancelAndFinishJob(
+                    params,
+                    AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON);
+        }
+
+        if (!mInjector.getFlags().getAggregatedErrorReportingEnabled()) {
+            sLogger.d(TAG + ": aggregate error reporting disabled, finishing job.");
+            return cancelAndFinishJob(
+                    params,
+                    AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_JOB_NOT_CONFIGURED);
+        }
+
+        mFuture =
+                Futures.submit(
+                        new Runnable() {
+                            @Override
+                            public void run() {
+                                // TODO(b/329921267): Add logic for reporting new data from DAO.
+                                sLogger.d(
+                                        TAG + ": Running the aggregate error data collection job");
+                            }
+                        },
+                        mInjector.getExecutor());
+
+        Futures.addCallback(
+                mFuture,
+                new FutureCallback<Void>() {
+                    @Override
+                    public void onSuccess(Void result) {
+                        sLogger.d(TAG + ": Aggregate error reporting job completed successfully.");
+                        boolean wantsReschedule = false;
+                        OdpJobServiceLogger.getInstance(AggregateErrorDataReportingService.this)
+                                .recordJobFinished(
+                                        AGGREGATE_ERROR_DATA_REPORTING_JOB_ID,
+                                        /* isSuccessful= */ true,
+                                        wantsReschedule);
+                        jobFinished(params, wantsReschedule);
+                    }
+
+                    @Override
+                    public void onFailure(Throwable t) {
+                        sLogger.e(TAG + ": Failed to handle JobService: " + params.getJobId(), t);
+                        boolean wantsReschedule = false;
+                        OdpJobServiceLogger.getInstance(AggregateErrorDataReportingService.this)
+                                .recordJobFinished(
+                                        AGGREGATE_ERROR_DATA_REPORTING_JOB_ID,
+                                        /* isSuccessful= */ false,
+                                        wantsReschedule);
+                        //  When failure, also tell the JobScheduler that the job has completed and
+                        // does not need to be rescheduled.
+                        jobFinished(params, wantsReschedule);
+                    }
+                },
+                mInjector.getExecutor());
+
+        return true;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        if (mFuture != null) {
+            mFuture.cancel(true);
+            mFuture = null;
+        }
+
+        // Reschedule the job since it ended before finishing
+        boolean wantsReschedule = true;
+        OdpJobServiceLogger.getInstance(this)
+                .recordOnStopJob(params, AGGREGATE_ERROR_DATA_REPORTING_JOB_ID, wantsReschedule);
+        return wantsReschedule;
+    }
+
+    private boolean cancelAndFinishJob(final JobParameters params, int skipReason) {
+        JobScheduler jobScheduler = this.getSystemService(JobScheduler.class);
+        if (jobScheduler != null) {
+            jobScheduler.cancel(AGGREGATE_ERROR_DATA_REPORTING_JOB_ID);
+        }
+        OdpJobServiceLogger.getInstance(this)
+                .recordJobSkipped(AGGREGATE_ERROR_DATA_REPORTING_JOB_ID, skipReason);
+        jobFinished(params, /* wantsReschedule= */ false);
+        return true;
+    }
+}
diff --git a/src/com/android/ondevicepersonalization/services/data/errors/AggregatedErrorCodesContract.java b/src/com/android/ondevicepersonalization/services/data/errors/AggregatedErrorCodesContract.java
new file mode 100644
index 0000000..c9632a1
--- /dev/null
+++ b/src/com/android/ondevicepersonalization/services/data/errors/AggregatedErrorCodesContract.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 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.ondevicepersonalization.services.data.errors;
+
+import android.provider.BaseColumns;
+
+/** Contract for the per vendor aggregated error code tables. Defines the table. */
+final class AggregatedErrorCodesContract {
+    private AggregatedErrorCodesContract() {}
+
+    /**
+     * Table containing aggregated error data associated with a particular vendor/adopter.
+     *
+     * <p>Each table is associated with a particular vendor.
+     */
+    public static class ErrorDataEntry implements BaseColumns {
+
+        /** The {@code isolatedServiceErrorCode} returned from the {@code IsolatedWorker}. */
+        public static final String EXCEPTION_ERROR_CODE = "exception_error_code";
+
+        /** The date that error was thrown. */
+        public static final String EXCEPTION_DATE = "exception_date";
+
+        /** The total count of the errors thrown by the vendor code on the given date. */
+        public static final String EXCEPTION_COUNT = "exception_count";
+
+        /**
+         * The version of the package of the {@code IsolatedService} when the error was reported.
+         */
+        public static final String SERVICE_PACKAGE_VERSION = "service_package_version";
+
+        private ErrorDataEntry() {}
+
+        /** Returns the statement for table creation for the given table name. */
+        public static String getCreateTableIfNotExistsStatement(final String tableName) {
+            return "CREATE TABLE IF NOT EXISTS "
+                    + tableName
+                    + " ("
+                    + EXCEPTION_ERROR_CODE
+                    + " INTEGER DEFAULT 0,"
+                    + EXCEPTION_DATE
+                    + " INTEGER DEFAULT 0,"
+                    + EXCEPTION_COUNT
+                    + " INTEGER DEFAULT 0,"
+                    + SERVICE_PACKAGE_VERSION
+                    + " INTEGER DEFAULT 0,"
+                    + "PRIMARY KEY("
+                    + EXCEPTION_ERROR_CODE
+                    + ","
+                    + EXCEPTION_DATE
+                    + "))";
+        }
+    }
+}
diff --git a/src/com/android/ondevicepersonalization/services/data/errors/AggregatedErrorCodesLogger.java b/src/com/android/ondevicepersonalization/services/data/errors/AggregatedErrorCodesLogger.java
new file mode 100644
index 0000000..a8b47ac
--- /dev/null
+++ b/src/com/android/ondevicepersonalization/services/data/errors/AggregatedErrorCodesLogger.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2024 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.ondevicepersonalization.services.data.errors;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import com.android.odp.module.common.PackageUtils;
+import com.android.ondevicepersonalization.internal.util.LoggerFactory;
+import com.android.ondevicepersonalization.services.FlagsFactory;
+import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
+import com.android.ondevicepersonalization.services.manifest.AppManifestConfigHelper;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+public final class AggregatedErrorCodesLogger {
+    private static final String TAG = AggregatedErrorCodesLogger.class.getSimpleName();
+    private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
+
+    /**
+     * Adds the given isolatedServiceError code into the package specific DB via the {@link
+     * OnDevicePersonalizationAggregatedErrorDataDao}.
+     *
+     * <p>No-op if the aggregate error reporting flag is disabled.
+     *
+     * @param isolatedServiceErrorCode the error code returned from the isolated service.
+     * @param componentName the name of the component hosting the isolated service.
+     * @param context calling service context.
+     * @return {@link ListenableFuture} that resolves successfully when the error code is
+     *     successfully logged via the Dao.
+     */
+    public static ListenableFuture<Void> logIsolatedServiceErrorCode(
+            int isolatedServiceErrorCode, ComponentName componentName, Context context) {
+        if (!FlagsFactory.getFlags().getAggregatedErrorReportingEnabled()) {
+            sLogger.e(TAG + ": Skipping logging, aggregated error code logging disabled");
+            return Futures.immediateVoidFuture();
+        }
+
+        return (ListenableFuture<Void>)
+                OnDevicePersonalizationExecutors.getBackgroundExecutor()
+                        .submit(() -> logError(isolatedServiceErrorCode, componentName, context));
+    }
+
+    private static void logError(
+            int isolatedServiceErrorCode, ComponentName componentName, Context context) {
+        String certDigest = "";
+        try {
+            certDigest = PackageUtils.getCertDigest(context, componentName.getPackageName());
+        } catch (PackageManager.NameNotFoundException nne) {
+            sLogger.e(TAG + ": failed to get cert digest.", nne);
+            return;
+        }
+
+        OnDevicePersonalizationAggregatedErrorDataDao dao =
+                OnDevicePersonalizationAggregatedErrorDataDao.getInstance(
+                        context, componentName, certDigest);
+        dao.addExceptionCount(isolatedServiceErrorCode, /* exceptionCount= */ 1);
+    }
+
+    /**
+     * Deletes any aggregate error data tables on device except for those ODP services that are
+     * still installed and enrolled.
+     *
+     * <p>No-op if the aggregate error reporting flag is disabled.
+     *
+     * <p>Can use the {@link #cleanupErrorData(Context)} if already calling on an {@code executor}.
+     *
+     * @param context calling service context.
+     * @return {@link ListenableFuture} that resolves successfully when the deletion is successful.
+     */
+    public static ListenableFuture<Void> cleanupAggregatedErrorData(Context context) {
+        if (!FlagsFactory.getFlags().getAggregatedErrorReportingEnabled()) {
+            sLogger.e(TAG + ": Skipping cleanup, aggregated error code logging disabled");
+            return Futures.immediateVoidFuture();
+        }
+
+        return (ListenableFuture<Void>)
+                OnDevicePersonalizationExecutors.getBackgroundExecutor()
+                        .submit(() -> cleanupErrorData(context));
+    }
+
+    /**
+     * Deletes any aggregate error data tables on device except for those ODP services that are
+     * still installed and enrolled.
+     *
+     * <p>No-op if the aggregate error reporting flag is disabled.
+     *
+     * <p>Should be called on an appropriate {@link OnDevicePersonalizationExecutors}.
+     *
+     * @param context calling service context.
+     */
+    public static void cleanupErrorData(Context context) {
+        // Delete all error data for any services that are no longer installed
+        ImmutableList<ComponentName> odpServices =
+                AppManifestConfigHelper.getOdpServices(context, /* enrolledOnly= */ true);
+        OnDevicePersonalizationAggregatedErrorDataDao.cleanupErrorData(context, odpServices);
+    }
+
+    /**
+     * Test only method that returns count of error data tables on device.
+     *
+     * @param context calling service context.
+     * @return the number of error data tables on device.
+     */
+    @VisibleForTesting
+    public static int getErrorDataTableCount(Context context) {
+        return OnDevicePersonalizationAggregatedErrorDataDao.getErrorDataTableNames(context).size();
+    }
+
+    private AggregatedErrorCodesLogger() {}
+}
diff --git a/src/com/android/ondevicepersonalization/services/data/errors/DateTimeUtils.java b/src/com/android/ondevicepersonalization/services/data/errors/DateTimeUtils.java
new file mode 100644
index 0000000..91c8a7a
--- /dev/null
+++ b/src/com/android/ondevicepersonalization/services/data/errors/DateTimeUtils.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 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.ondevicepersonalization.services.data.errors;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.odp.module.common.Clock;
+import com.android.odp.module.common.MonotonicClock;
+import com.android.ondevicepersonalization.internal.util.LoggerFactory;
+
+import java.time.DateTimeException;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+
+/** Utilities for date/time transformations. */
+final class DateTimeUtils {
+    private static final String TAG = DateTimeUtils.class.getSimpleName();
+    private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
+
+    /**
+     * Get the day index in the UTC timezone.
+     *
+     * <p>Returns {@code -1} if unsuccessful.
+     */
+    public static int dayIndexUtc() {
+        return dayIndexUtc(MonotonicClock.getInstance());
+    }
+
+    @VisibleForTesting
+    static int dayIndexUtc(Clock clock) {
+        // Package-private method for easier testing, allows injecting a clock in tests.
+        Instant currentInstant = getCurrentInstant(clock);
+        try {
+            return (int) currentInstant.atZone(ZoneOffset.UTC).toLocalDate().toEpochDay();
+        } catch (DateTimeException e) {
+            sLogger.e(TAG + " : failed to get day index.", e);
+            return -1;
+        }
+    }
+
+    /**
+     * Get the day index in the local device's timezone.
+     *
+     * <p>Returns {@code -1} if unsuccessful.
+     */
+    public static int dayIndexLocal() {
+        return dayIndexLocal(MonotonicClock.getInstance());
+    }
+
+    @VisibleForTesting
+    static int dayIndexLocal(Clock clock) {
+        // Package-private method for easier testing, allows injecting a clock in tests.
+        Instant currentInstant = getCurrentInstant(clock);
+        try {
+            return (int) currentInstant.atZone(ZoneId.systemDefault()).toLocalDate().toEpochDay();
+        } catch (DateTimeException e) {
+            sLogger.e(TAG + " : failed to get day index.", e);
+            return -1;
+        }
+    }
+
+    private static Instant getCurrentInstant(Clock clock) {
+        long currentSystemTime = clock.currentTimeMillis();
+        sLogger.i(TAG + ": current system time = " + currentSystemTime);
+        return Instant.ofEpochMilli(currentSystemTime);
+    }
+
+    private DateTimeUtils() {}
+}
diff --git a/src/com/android/ondevicepersonalization/services/data/errors/ErrorData.java b/src/com/android/ondevicepersonalization/services/data/errors/ErrorData.java
new file mode 100644
index 0000000..4ced422
--- /dev/null
+++ b/src/com/android/ondevicepersonalization/services/data/errors/ErrorData.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2024 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.ondevicepersonalization.services.data.errors;
+
+import android.annotation.NonNull;
+
+import com.android.ondevicepersonalization.internal.util.AnnotationValidations;
+import com.android.ondevicepersonalization.internal.util.DataClass;
+
+@DataClass(genBuilder = true, genEqualsHashCode = true)
+public class ErrorData {
+
+    /** The error code returned by the {@code IsolatedService}. */
+    @NonNull private final int mErrorCode;
+
+    /** The aggregated count of {@link #mErrorCode} on the given {@link #mEpochDay}. */
+    @NonNull private final int mErrorCount;
+
+    /** The date associated with this record of aggregated errors. */
+    @NonNull private final int mEpochDay;
+
+    /** The version of the package of the {@code IsolatedService}. */
+    @NonNull private final long mServicePackageVersion;
+
+    // Code below generated by codegen v1.0.23.
+    //
+    // DO NOT MODIFY!
+    // CHECKSTYLE:OFF Generated code
+    //
+    // To regenerate run:
+    // $ codegen
+    // $ANDROID_BUILD_TOP/packages/modules/OnDevicePersonalization/src/com/android/ondevicepersonalization/services/data/errors/ErrorData.java
+    //
+    // To exclude the generated code from IntelliJ auto-formatting enable (one-time):
+    //   Settings > Editor > Code Style > Formatter Control
+    // @formatter:off
+
+    @DataClass.Generated.Member
+    /* package-private */ ErrorData(
+            @NonNull int errorCode,
+            @NonNull int errorCount,
+            @NonNull int epochDay,
+            @NonNull long servicePackageVersion) {
+        this.mErrorCode = errorCode;
+        AnnotationValidations.validate(NonNull.class, null, mErrorCode);
+        this.mErrorCount = errorCount;
+        AnnotationValidations.validate(NonNull.class, null, mErrorCount);
+        this.mEpochDay = epochDay;
+        AnnotationValidations.validate(NonNull.class, null, mEpochDay);
+        this.mServicePackageVersion = servicePackageVersion;
+        AnnotationValidations.validate(NonNull.class, null, mServicePackageVersion);
+
+        // onConstructed(); // You can define this method to get a callback
+    }
+
+    @DataClass.Generated.Member
+    public @NonNull int getErrorCode() {
+        return mErrorCode;
+    }
+
+    @DataClass.Generated.Member
+    public @NonNull int getErrorCount() {
+        return mErrorCount;
+    }
+
+    @DataClass.Generated.Member
+    public @NonNull int getEpochDay() {
+        return mEpochDay;
+    }
+
+    @DataClass.Generated.Member
+    public @NonNull long getServicePackageVersion() {
+        return mServicePackageVersion;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public boolean equals(@android.annotation.Nullable Object o) {
+        // You can override field equality logic by defining either of the methods like:
+        // boolean fieldNameEquals(ErrorData other) { ... }
+        // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        @SuppressWarnings("unchecked")
+        ErrorData that = (ErrorData) o;
+        //noinspection PointlessBooleanExpression
+        return true
+                && mErrorCode == that.mErrorCode
+                && mErrorCount == that.mErrorCount
+                && mEpochDay == that.mEpochDay
+                && mServicePackageVersion == that.mServicePackageVersion;
+    }
+
+    @Override
+    @DataClass.Generated.Member
+    public int hashCode() {
+        // You can override field hashCode logic by defining methods like:
+        // int fieldNameHashCode() { ... }
+
+        int _hash = 1;
+        _hash = 31 * _hash + mErrorCode;
+        _hash = 31 * _hash + mErrorCount;
+        _hash = 31 * _hash + mEpochDay;
+        _hash = 31 * _hash + Long.hashCode(mServicePackageVersion);
+        return _hash;
+    }
+
+    /** A builder for {@link ErrorData} */
+    @SuppressWarnings("WeakerAccess")
+    @DataClass.Generated.Member
+    public static class Builder {
+
+        private @NonNull int mErrorCode;
+        private @NonNull int mErrorCount;
+        private @NonNull int mEpochDay;
+        private @NonNull long mServicePackageVersion;
+
+        private long mBuilderFieldsSet = 0L;
+
+        public Builder(
+                @NonNull int errorCode,
+                @NonNull int errorCount,
+                @NonNull int epochDay,
+                @NonNull long servicePackageVersion) {
+            mErrorCode = errorCode;
+            AnnotationValidations.validate(NonNull.class, null, mErrorCode);
+            mErrorCount = errorCount;
+            AnnotationValidations.validate(NonNull.class, null, mErrorCount);
+            mEpochDay = epochDay;
+            AnnotationValidations.validate(NonNull.class, null, mEpochDay);
+            mServicePackageVersion = servicePackageVersion;
+            AnnotationValidations.validate(NonNull.class, null, mServicePackageVersion);
+        }
+
+        @DataClass.Generated.Member
+        public @NonNull Builder setErrorCode(@NonNull int value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x1;
+            mErrorCode = value;
+            return this;
+        }
+
+        @DataClass.Generated.Member
+        public @NonNull Builder setErrorCount(@NonNull int value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x2;
+            mErrorCount = value;
+            return this;
+        }
+
+        @DataClass.Generated.Member
+        public @NonNull Builder setEpochDay(@NonNull int value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x4;
+            mEpochDay = value;
+            return this;
+        }
+
+        @DataClass.Generated.Member
+        public @NonNull Builder setServicePackageVersion(@NonNull long value) {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x8;
+            mServicePackageVersion = value;
+            return this;
+        }
+
+        /** Builds the instance. This builder should not be touched after calling this! */
+        public @NonNull ErrorData build() {
+            checkNotUsed();
+            mBuilderFieldsSet |= 0x10; // Mark builder used
+
+            ErrorData o = new ErrorData(mErrorCode, mErrorCount, mEpochDay, mServicePackageVersion);
+            return o;
+        }
+
+        private void checkNotUsed() {
+            if ((mBuilderFieldsSet & 0x10) != 0) {
+                throw new IllegalStateException(
+                        "This Builder should not be reused. Use a new Builder instance instead");
+            }
+        }
+    }
+
+    @DataClass.Generated(
+            time = 1724390597119L,
+            codegenVersion = "1.0.23",
+            sourceFile =
+                    "packages/modules/OnDevicePersonalization/src/com/android/ondevicepersonalization/services/data/errors/ErrorData.java",
+            inputSignatures =
+                    "private final @android.annotation.NonNull int mErrorCode\n"
+                        + "private final @android.annotation.NonNull int mErrorCount\n"
+                        + "private final @android.annotation.NonNull int mEpochDay\n"
+                        + "private final @android.annotation.NonNull long mServicePackageVersion\n"
+                        + "class ErrorData extends java.lang.Object implements []\n"
+                        + "@com.android.ondevicepersonalization.internal.util.DataClass(genBuilder=true,"
+                        + " genEqualsHashCode=true)")
+    @Deprecated
+    private void __metadata() {}
+
+    // @formatter:on
+    // End of generated code
+
+}
diff --git a/src/com/android/ondevicepersonalization/services/data/errors/OnDevicePersonalizationAggregatedErrorDataDao.java b/src/com/android/ondevicepersonalization/services/data/errors/OnDevicePersonalizationAggregatedErrorDataDao.java
new file mode 100644
index 0000000..e7a996d
--- /dev/null
+++ b/src/com/android/ondevicepersonalization/services/data/errors/OnDevicePersonalizationAggregatedErrorDataDao.java
@@ -0,0 +1,458 @@
+/*
+ * Copyright (C) 2024 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.ondevicepersonalization.services.data.errors;
+
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+
+import com.android.odp.module.common.PackageUtils;
+import com.android.ondevicepersonalization.internal.util.LoggerFactory;
+import com.android.ondevicepersonalization.services.data.DbUtils;
+import com.android.ondevicepersonalization.services.data.OnDevicePersonalizationDbHelper;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+
+import java.io.File;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Dao used to manage access to per vendor aggregated error codes that are returned by {@link
+ * android.adservices.ondevicepersonalization.IsolatedService} implementations.
+ *
+ * <p>The Dao should all be called on appropriate {@code executor}.
+ */
+class OnDevicePersonalizationAggregatedErrorDataDao {
+    private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
+    private static final String TAG =
+            OnDevicePersonalizationAggregatedErrorDataDao.class.getSimpleName();
+    private static final String ERROR_DATA_TABLE_NAME_PREFIX = "errordata";
+
+    @VisibleForTesting static final int MAX_ALLOWED_ERROR_CODE = 32;
+
+    private static final Map<String, OnDevicePersonalizationAggregatedErrorDataDao>
+            sVendorDataDaos = new ConcurrentHashMap<>();
+    private final OnDevicePersonalizationDbHelper mDbHelper;
+    private final ComponentName mOwner;
+    private final String mCertDigest;
+    private final String mTableName;
+    private final long mPackageVersion;
+
+    private OnDevicePersonalizationAggregatedErrorDataDao(
+            OnDevicePersonalizationDbHelper dbHelper,
+            ComponentName owner,
+            String certDigest,
+            long packageVersion) {
+        this.mDbHelper = dbHelper;
+        this.mOwner = owner;
+        this.mCertDigest = certDigest;
+        this.mTableName = getTableName(owner, certDigest);
+        this.mPackageVersion = packageVersion;
+    }
+
+    /**
+     * Clears all the aggregated error data tables except for the provided excluded services.
+     *
+     * @param context The context of the application
+     * @param excludedServices the services whose tables/data that should not be cleaned up.
+     *     <p>Synchronized to avoid any concurrent modifications to the underlying {@link
+     *     #sVendorDataDaos}.
+     */
+    static synchronized void cleanupErrorData(
+            Context context, ImmutableList<ComponentName> excludedServices) {
+        ImmutableList<String> existingTables = getErrorDataTableNames(context);
+        if (existingTables.isEmpty()) {
+            sLogger.d(TAG + ": no tables found to delete");
+            return;
+        }
+
+        Set<String> excludedTableNames = new HashSet<>();
+        for (ComponentName service : excludedServices) {
+            String certDigest = getCertDigest(context, service.getPackageName());
+            if (certDigest.isEmpty()) {
+                sLogger.d(
+                        TAG
+                                + ": unable to get cert digest skipping deletion for service "
+                                + service);
+                continue;
+            }
+
+            excludedTableNames.add(getTableName(service, certDigest));
+        }
+
+        OnDevicePersonalizationDbHelper dbHelper =
+                OnDevicePersonalizationDbHelper.getInstance(context);
+        SQLiteDatabase db = dbHelper == null ? null : dbHelper.safeGetWritableDatabase();
+        if (db == null) {
+            sLogger.e(TAG + ": failed to get the db while deleting exception data.");
+            return;
+        }
+
+        db.beginTransactionNonExclusive();
+        try {
+            for (String tableName : existingTables) {
+                if (excludedTableNames.contains(tableName)) {
+                    sLogger.d(TAG + ": skipping deletion for " + tableName);
+                    continue;
+                }
+                db.execSQL("DROP TABLE IF EXISTS " + tableName);
+                sVendorDataDaos.remove(tableName);
+            }
+            db.setTransactionSuccessful();
+        } catch (Exception e) {
+            sLogger.e(TAG + ": Failed to delete exception data.", e);
+        } finally {
+            db.endTransaction();
+        }
+    }
+
+    /**
+     * Helper method that returns an empty cert-digest if the underlying {@code PackageManager} call
+     * fails.
+     */
+    private static String getCertDigest(Context context, String packageName) {
+        try {
+            return PackageUtils.getCertDigest(context, packageName);
+        } catch (PackageManager.NameNotFoundException nne) {
+            sLogger.e(TAG + ": failed to get cert digest for " + packageName);
+        }
+        return "";
+    }
+
+    /**
+     * Returns an instance of the {@link OnDevicePersonalizationAggregatedErrorDataDao} for a given
+     * component and associated cert digest.
+     *
+     * @param context The context of the application
+     * @param owner ComponentName of the package whose errors will be aggregated in the table
+     * @param certDigest Hash of the certificate used to sign the package
+     * @return Instance of {@link OnDevicePersonalizationAggregatedErrorDataDao} for accessing the
+     *     requested components aggregated error table.
+     */
+    public static OnDevicePersonalizationAggregatedErrorDataDao getInstance(
+            Context context, ComponentName owner, String certDigest) {
+        String tableName = getTableName(owner, certDigest);
+        OnDevicePersonalizationAggregatedErrorDataDao instance = sVendorDataDaos.get(tableName);
+        if (instance == null) {
+            synchronized (sVendorDataDaos) {
+                instance = sVendorDataDaos.get(tableName);
+                if (instance == null) {
+                    OnDevicePersonalizationDbHelper dbHelper =
+                            OnDevicePersonalizationDbHelper.getInstance(context);
+                    instance =
+                            new OnDevicePersonalizationAggregatedErrorDataDao(
+                                    dbHelper, owner, certDigest, getPackageVersion(owner, context));
+                    sVendorDataDaos.put(tableName, instance);
+                }
+            }
+        }
+        return instance;
+    }
+
+    private static long getPackageVersion(ComponentName owner, Context context) {
+        long packageVersion = 0;
+        try {
+            String packageName = owner.getPackageName();
+            PackageInfo packageInfo =
+                    context.getPackageManager().getPackageInfo(packageName, /* flags= */ 0);
+            packageVersion = packageInfo.getLongVersionCode();
+        } catch (PackageManager.NameNotFoundException nne) {
+            sLogger.e(TAG + ": Unable to find package " + owner.getPackageName(), nne);
+        }
+        return packageVersion;
+    }
+
+    /** Delete the existing aggregate exception data for this package. */
+    public boolean deleteExceptionData() {
+        SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
+        if (db == null) {
+            sLogger.e(TAG + ": failed to get the db while deleting exception data.");
+            return false;
+        }
+
+        try {
+            db.beginTransactionNonExclusive();
+            if (db.delete(mTableName, /* whereClause= */ "1", /* whereArgs= */ null) <= 0) {
+                sLogger.d(TAG + ": zero records deleted for " + mOwner);
+                return false;
+            }
+
+            db.setTransactionSuccessful();
+        } catch (SQLException exception) {
+            sLogger.e(TAG + ": failed to delete exception data for " + mOwner, exception);
+        } finally {
+            db.endTransaction();
+        }
+        return true;
+    }
+
+    /** Get the existing aggregate exception data for this package. */
+    public ImmutableList<ErrorData> getExceptionData() {
+        ImmutableList.Builder listBuilder = ImmutableList.builder();
+        try {
+            SQLiteDatabase db = mDbHelper.getReadableDatabase();
+            try (Cursor cursor =
+                    db.query(
+                            mTableName,
+                            /* columns= */ null,
+                            /* selection= */ null,
+                            /* selectionArgs= */ null,
+                            /* groupBy= */ null,
+                            /* having= */ null,
+                            /* orderBy= */ null)) {
+                while (cursor.moveToNext()) {
+                    int errorCount =
+                            cursor.getInt(
+                                    cursor.getColumnIndexOrThrow(
+                                            AggregatedErrorCodesContract.ErrorDataEntry
+                                                    .EXCEPTION_COUNT));
+                    int errorCode =
+                            cursor.getInt(
+                                    cursor.getColumnIndexOrThrow(
+                                            AggregatedErrorCodesContract.ErrorDataEntry
+                                                    .EXCEPTION_ERROR_CODE));
+                    int epochDay =
+                            cursor.getInt(
+                                    cursor.getColumnIndexOrThrow(
+                                            AggregatedErrorCodesContract.ErrorDataEntry
+                                                    .EXCEPTION_DATE));
+                    long packageVersion =
+                            cursor.getLong(
+                                    cursor.getColumnIndexOrThrow(
+                                            AggregatedErrorCodesContract.ErrorDataEntry
+                                                    .SERVICE_PACKAGE_VERSION));
+                    listBuilder.add(
+                            new ErrorData.Builder(errorCode, errorCount, epochDay, packageVersion)
+                                    .build());
+                }
+                cursor.close();
+                return listBuilder.build();
+            }
+        } catch (SQLiteException e) {
+            sLogger.e(TAG + ": Failed to read aggregate exception data for " + mOwner, e);
+        }
+        return ImmutableList.of();
+    }
+
+    /**
+     * Add or update the record of exception count for the provided error code.
+     *
+     * <p>Uses the current date as the date they exception was thrown.
+     *
+     * @return whether the exception was successfully recorded in the database.
+     */
+    public boolean addExceptionCount(int isolatedServiceErrorCode, int exceptionCount) {
+        if (isolatedServiceErrorCode > MAX_ALLOWED_ERROR_CODE) {
+            sLogger.e(
+                    TAG
+                            + ": failed to record exception "
+                            + isolatedServiceErrorCode
+                            + " for package "
+                            + mOwner.getPackageName());
+            return false;
+        }
+
+        int epochDay = DateTimeUtils.dayIndexUtc();
+        if (epochDay == -1) {
+            sLogger.e(
+                    TAG
+                            + ": failed to get the epoch day, unable to add exception for package "
+                            + mOwner.getPackageName());
+            return false;
+        }
+
+        int existingExceptionCount = getExceptionCount(isolatedServiceErrorCode, epochDay);
+        if (!createTableIfNotExists(mTableName)) {
+            sLogger.e(TAG + ": failed to create table " + mTableName);
+            return false;
+        }
+
+        SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
+        if (db == null) {
+            sLogger.e(TAG + " : failed to get the DB while inserting into DB.");
+            return false;
+        }
+
+        try {
+            db.beginTransactionNonExclusive();
+            if (!insertErrorData(
+                    new ErrorData.Builder(
+                                    isolatedServiceErrorCode,
+                                    existingExceptionCount + exceptionCount,
+                                    epochDay,
+                                    mPackageVersion)
+                            .build())) {
+                sLogger.e(TAG + ": failed to insert error data " + mTableName);
+                return false;
+            }
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+
+        return true;
+    }
+
+    /**
+     * Updates the given vendor data row, adds it if it doesn't already exist.
+     *
+     * @return true if the update/insert succeeded, false otherwise
+     */
+    private boolean insertErrorData(ErrorData errorData) {
+        try {
+            SQLiteDatabase db = mDbHelper.getWritableDatabase();
+
+            ContentValues values = new ContentValues();
+            values.put(
+                    AggregatedErrorCodesContract.ErrorDataEntry.EXCEPTION_ERROR_CODE,
+                    errorData.getErrorCode());
+            values.put(
+                    AggregatedErrorCodesContract.ErrorDataEntry.EXCEPTION_DATE,
+                    errorData.getEpochDay());
+            values.put(
+                    AggregatedErrorCodesContract.ErrorDataEntry.SERVICE_PACKAGE_VERSION,
+                    errorData.getServicePackageVersion());
+            values.put(
+                    AggregatedErrorCodesContract.ErrorDataEntry.EXCEPTION_COUNT,
+                    errorData.getErrorCount());
+            return db.insertWithOnConflict(
+                            mTableName, null, values, SQLiteDatabase.CONFLICT_REPLACE)
+                    != -1;
+        } catch (SQLiteException e) {
+            sLogger.e(TAG + ": Failed to update or insert error data. ", e);
+        }
+        return false;
+    }
+
+    @VisibleForTesting
+    /** Returns the existing count associated with the given error code on the given day. */
+    int getExceptionCount(int isolatedServiceErrorCode, int epochDay) {
+        SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
+        if (db == null) {
+            sLogger.e(TAG + ": failed to get the DB while getting exception count.");
+            return 0;
+        }
+
+        String selection =
+                AggregatedErrorCodesContract.ErrorDataEntry.EXCEPTION_ERROR_CODE
+                        + " = ? AND "
+                        + AggregatedErrorCodesContract.ErrorDataEntry.EXCEPTION_DATE
+                        + " = ?";
+        String[] selectionArgs = {
+            String.valueOf(isolatedServiceErrorCode), String.valueOf(epochDay)
+        };
+        String[] columns = {AggregatedErrorCodesContract.ErrorDataEntry.EXCEPTION_COUNT};
+        try (Cursor cursor =
+                db.query(
+                        mTableName,
+                        columns,
+                        selection,
+                        selectionArgs,
+                        /* groupBy= */ null,
+                        /* having= */ null,
+                        /* orderBy= */ null)) {
+            if (cursor.moveToFirst()) {
+                return cursor.getInt(
+                        cursor.getColumnIndexOrThrow(
+                                AggregatedErrorCodesContract.ErrorDataEntry.EXCEPTION_COUNT));
+            }
+        } catch (SQLiteException e) {
+            sLogger.e(
+                    TAG
+                            + ": Failed to query existing error counts associated with error-code: "
+                            + isolatedServiceErrorCode
+                            + " on day: "
+                            + epochDay,
+                    e);
+        }
+        // No existing records or encountered exception
+        return 0;
+    }
+
+    /** Creates table name based on owner and certDigest */
+    public static String getTableName(ComponentName owner, String certDigest) {
+        return DbUtils.getTableName(ERROR_DATA_TABLE_NAME_PREFIX, owner, certDigest);
+    }
+
+    /** Creates file directory name based on table name and base directory */
+    public static String getFileDir(String tableName, File baseDir) {
+        return baseDir + "/VendorData/" + tableName;
+    }
+
+    private boolean createTableIfNotExists(String tableName) {
+        try {
+            SQLiteDatabase db = mDbHelper.getWritableDatabase();
+            db.execSQL(
+                    AggregatedErrorCodesContract.ErrorDataEntry.getCreateTableIfNotExistsStatement(
+                            tableName));
+        } catch (SQLException e) {
+            sLogger.e(TAG + ": Failed to create table: " + tableName, e);
+            return false;
+        }
+        sLogger.d(TAG + ": Successfully created table: " + tableName);
+        return true;
+    }
+
+    @VisibleForTesting
+    /** Get existing error data tables in the DB. */
+    static ImmutableList<String> getErrorDataTableNames(Context context) {
+        try {
+            OnDevicePersonalizationDbHelper db =
+                    OnDevicePersonalizationDbHelper.getInstance(context);
+            return getMatchingTableNames(
+                    db.safeGetReadableDatabase(), ERROR_DATA_TABLE_NAME_PREFIX);
+        } catch (SQLException e) {
+            sLogger.e(TAG + ": Failed to get matching tables ", e);
+            return ImmutableList.of();
+        }
+    }
+
+    private static ImmutableList<String> getMatchingTableNames(
+            SQLiteDatabase db, String tablePrefix) {
+        try (Cursor cursor =
+                db.rawQuery(
+                        "SELECT name,sql FROM sqlite_master WHERE type='table' AND name LIKE '%"
+                                + tablePrefix
+                                + "%'",
+                        /* selectionArgs= */ null)) {
+            if (!cursor.moveToFirst()) {
+                sLogger.d(TAG + ": no tables found.");
+                return ImmutableList.of();
+            }
+
+            ImmutableList.Builder<String> listBuilder = new ImmutableList.Builder<>();
+            do {
+                String name = cursor.getString(/* columnIndex= */ 0);
+                if (name != null) {
+                    listBuilder.add(name);
+                }
+            } while (cursor.moveToNext());
+
+            return listBuilder.build();
+        }
+    }
+}
diff --git a/src/com/android/ondevicepersonalization/services/data/events/EventsDao.java b/src/com/android/ondevicepersonalization/services/data/events/EventsDao.java
index 3e6dc9b..cbc43f4 100644
--- a/src/com/android/ondevicepersonalization/services/data/events/EventsDao.java
+++ b/src/com/android/ondevicepersonalization/services/data/events/EventsDao.java
@@ -110,7 +110,11 @@
      * @return true if all inserts succeeded, false otherwise.
      */
     public boolean insertEvents(@NonNull List<Event> events) {
-        SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
+        if (db == null) {
+            return false;
+        }
+
         try {
             db.beginTransactionNonExclusive();
             for (Event event : events) {
@@ -180,7 +184,11 @@
      * @return true if the all the update/inserts succeeded, false otherwise
      */
     public boolean updateOrInsertEventStatesTransaction(List<EventState> eventStates) {
-        SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
+        if (db == null) {
+            return false;
+        }
+
         try {
             db.beginTransactionNonExclusive();
             for (EventState eventState : eventStates) {
@@ -205,7 +213,11 @@
      * @return eventState if found, null otherwise
      */
     public EventState getEventState(String taskIdentifier, ComponentName service) {
-        SQLiteDatabase db = mDbHelper.getReadableDatabase();
+        SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
+        if (db == null) {
+            return null;
+        }
+
         String selection = EventStateContract.EventStateEntry.TASK_IDENTIFIER + " = ? AND "
                 + EventStateContract.EventStateEntry.SERVICE_NAME + " = ?";
         String[] selectionArgs = {taskIdentifier, DbUtils.toTableValue(service)};
@@ -302,7 +314,11 @@
 
     private List<Query> readQueryRows(String selection, String[] selectionArgs) {
         List<Query> queries = new ArrayList<>();
-        SQLiteDatabase db = mDbHelper.getReadableDatabase();
+        SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
+        if (db == null) {
+            return queries;
+        }
+
         String orderBy = QueriesContract.QueriesEntry.QUERY_ID;
         try (Cursor cursor = db.query(
                 QueriesContract.QueriesEntry.TABLE_NAME,
@@ -342,8 +358,11 @@
 
     private List<JoinedEvent> readJoinedTableRows(String selection, String[] selectionArgs) {
         List<JoinedEvent> joinedEventList = new ArrayList<>();
+        SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
+        if (db == null) {
+            return List.of();
+        }
 
-        SQLiteDatabase db = mDbHelper.getReadableDatabase();
         String select = "SELECT "
                 + EventsContract.EventsEntry.EVENT_ID + ","
                 + EventsContract.EventsEntry.ROW_INDEX + ","
@@ -414,7 +433,11 @@
      * @return true if the delete executed successfully, false otherwise.
      */
     public boolean deleteEventState(ComponentName service) {
-        SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
+        if (db == null) {
+            return false;
+        }
+
         try {
             String selection = EventStateContract.EventStateEntry.SERVICE_NAME + " = ?";
             String[] selectionArgs = {DbUtils.toTableValue(service)};
@@ -433,7 +456,11 @@
      * @return true if the delete executed successfully, false otherwise.
      */
     public boolean deleteEventsAndQueries(long timestamp) {
-        SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
+        if (db == null) {
+            return false;
+        }
+
         try {
             db.beginTransactionNonExclusive();
             // Delete from events table first to satisfy FK requirements.
@@ -521,7 +548,6 @@
      */
     public boolean hasEvent(long queryId, int type, int rowIndex, ComponentName service) {
         try {
-            int count = 0;
             SQLiteDatabase db = mDbHelper.getReadableDatabase();
             String[] projection = {EventsContract.EventsEntry.EVENT_ID};
             String selection = EventsContract.EventsEntry.QUERY_ID + " = ?"
diff --git a/src/com/android/ondevicepersonalization/services/data/user/AdServicesCommonStatesWrapper.java b/src/com/android/ondevicepersonalization/services/data/user/AdServicesCommonStatesWrapper.java
new file mode 100644
index 0000000..afd7031
--- /dev/null
+++ b/src/com/android/ondevicepersonalization/services/data/user/AdServicesCommonStatesWrapper.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 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.ondevicepersonalization.services.data.user;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * A wrapper for the AdServicesCommonStates API.
+ */
+public interface AdServicesCommonStatesWrapper {
+    /** Wrapped result from AdServicesCommonStates API */
+    class CommonStatesResult {
+        private final int mPaState;
+        private final int mMeasurementState;
+
+        /** Creates a Result */
+        public CommonStatesResult(int paState, int measurementState) {
+            mPaState = paState;
+            mMeasurementState = measurementState;
+        }
+
+        /** Returns the ProtectedAudience allowed state. */
+        public int getPaState() {
+            return mPaState;
+        }
+
+        /** Returns the Measurement allowed state. */
+        public int getMeasurementState() {
+            return mMeasurementState;
+        }
+    }
+
+    /** Returns the wrapped CommonStatesResult */
+    ListenableFuture<CommonStatesResult> getCommonStates();
+}
diff --git a/src/com/android/ondevicepersonalization/services/data/user/AdServicesCommonStatesWrapperImpl.java b/src/com/android/ondevicepersonalization/services/data/user/AdServicesCommonStatesWrapperImpl.java
new file mode 100644
index 0000000..d08c8ab
--- /dev/null
+++ b/src/com/android/ondevicepersonalization/services/data/user/AdServicesCommonStatesWrapperImpl.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 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.ondevicepersonalization.services.data.user;
+
+import android.adservices.common.AdServicesCommonManager;
+import android.adservices.common.AdServicesCommonStates;
+import android.adservices.common.AdServicesCommonStatesResponse;
+import android.adservices.common.AdServicesOutcomeReceiver;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Binder;
+
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+
+import com.android.ondevicepersonalization.internal.util.LoggerFactory;
+import com.android.ondevicepersonalization.services.FlagsFactory;
+import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
+
+import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A wrapper for the AdServicesCommonStates API. Used by UserPrivacyStatus to
+ * fetch common states from AdServices.
+ */
+class AdServicesCommonStatesWrapperImpl implements AdServicesCommonStatesWrapper {
+    private static final String TAG = AdServicesCommonStatesWrapperImpl.class.getSimpleName();
+    private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
+    private final Context mContext;
+
+    AdServicesCommonStatesWrapperImpl(Context context) {
+        mContext = Objects.requireNonNull(context);
+    }
+
+    @Override public ListenableFuture<CommonStatesResult> getCommonStates() {
+        try {
+            AdServicesCommonManager manager =
+                    Objects.requireNonNull(getAdServicesCommonManager());
+            sLogger.d(TAG + ": IPC getAdServicesCommonStates() started");
+            long origId = Binder.clearCallingIdentity();
+            long timeoutInMillis = FlagsFactory.getFlags().getAdservicesIpcCallTimeoutInMillis();
+            Binder.restoreCallingIdentity(origId);
+            ListenableFuture<AdServicesCommonStatesResponse> futureWithTimeout =
+                    Futures.withTimeout(
+                            getAdServicesResponse(manager),
+                            timeoutInMillis,
+                            TimeUnit.MILLISECONDS,
+                            OnDevicePersonalizationExecutors.getScheduledExecutor());
+
+            return FluentFuture.from(futureWithTimeout)
+                    .transform(
+                            v -> getResultFromResponse(v),
+                            MoreExecutors.newDirectExecutorService());
+        } catch (Exception e) {
+            return Futures.immediateFailedFuture(e);
+        }
+    }
+
+    private AdServicesCommonManager getAdServicesCommonManager() {
+        try {
+            return mContext.getSystemService(AdServicesCommonManager.class);
+        } catch (NoClassDefFoundError e) {
+            throw new IllegalStateException("Cannot find AdServicesCommonManager.", e);
+        }
+    }
+
+    private static CommonStatesResult getResultFromResponse(
+            AdServicesCommonStatesResponse response) {
+        AdServicesCommonStates commonStates = response.getAdServicesCommonStates();
+        return new CommonStatesResult(
+                commonStates.getPaState(), commonStates.getMeasurementState());
+    }
+
+    private ListenableFuture<AdServicesCommonStatesResponse> getAdServicesResponse(
+                    @NonNull AdServicesCommonManager adServicesCommonManager) {
+        return CallbackToFutureAdapter.getFuture(
+                completer -> {
+                    adServicesCommonManager.getAdservicesCommonStates(
+                            OnDevicePersonalizationExecutors.getBackgroundExecutor(),
+                            new AdServicesOutcomeReceiver<AdServicesCommonStatesResponse,
+                                    Exception>() {
+                                @Override
+                                public void onResult(AdServicesCommonStatesResponse result) {
+                                    sLogger.d(
+                                            TAG + ": IPC getAdServicesCommonStates() success");
+                                    completer.set(result);
+                                }
+
+                                @Override
+                                public void onError(Exception error) {
+                                    sLogger.e(error,
+                                            TAG + ": IPC getAdServicesCommonStates() error");
+                                    completer.setException(error);
+                                }
+                            });
+                    // For debugging purpose only.
+                    return "getAdServicesCommonStates";
+                }
+        );
+    }
+}
diff --git a/src/com/android/ondevicepersonalization/services/data/user/UserDataCollectionJobService.java b/src/com/android/ondevicepersonalization/services/data/user/UserDataCollectionJobService.java
index 68881fa..46356d7 100644
--- a/src/com/android/ondevicepersonalization/services/data/user/UserDataCollectionJobService.java
+++ b/src/com/android/ondevicepersonalization/services/data/user/UserDataCollectionJobService.java
@@ -29,7 +29,9 @@
 import android.content.ComponentName;
 import android.content.Context;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
+import com.android.ondevicepersonalization.services.Flags;
 import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
 import com.android.ondevicepersonalization.services.statsd.joblogging.OdpJobServiceLogger;
@@ -37,10 +39,9 @@
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
 
-/**
- * JobService to collect user data in the background thread.
- */
+/** JobService to collect user data in the background thread. */
 public class UserDataCollectionJobService extends JobService {
     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
     private static final String TAG = "UserDataCollectionJobService";
@@ -50,20 +51,37 @@
     private UserDataCollector mUserDataCollector;
     private RawUserData mUserData;
 
-    /**
-     * Schedules a unique instance of UserDataCollectionJobService to be run.
-     */
+    private final Injector mInjector;
+
+    public UserDataCollectionJobService() {
+        mInjector = new Injector();
+    }
+
+    @VisibleForTesting
+    public UserDataCollectionJobService(Injector injector) {
+        mInjector = injector;
+    }
+
+    static class Injector {
+        ListeningExecutorService getExecutor() {
+            return OnDevicePersonalizationExecutors.getBackgroundExecutor();
+        }
+
+        Flags getFlags() {
+            return FlagsFactory.getFlags();
+        }
+    }
+
+    /** Schedules a unique instance of UserDataCollectionJobService to be run. */
     public static int schedule(Context context) {
         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
-        if (jobScheduler.getPendingJob(
-                USER_DATA_COLLECTION_ID) != null) {
+        if (jobScheduler.getPendingJob(USER_DATA_COLLECTION_ID) != null) {
             sLogger.d(TAG + ": Job is already scheduled. Doing nothing,");
             return RESULT_FAILURE;
         }
-        ComponentName serviceComponent = new ComponentName(context,
-                UserDataCollectionJobService.class);
-        JobInfo.Builder builder = new JobInfo.Builder(
-                USER_DATA_COLLECTION_ID, serviceComponent);
+        ComponentName serviceComponent =
+                new ComponentName(context, UserDataCollectionJobService.class);
+        JobInfo.Builder builder = new JobInfo.Builder(USER_DATA_COLLECTION_ID, serviceComponent);
 
         // Constraints
         builder.setRequiresDeviceIdle(true);
@@ -80,27 +98,47 @@
     @Override
     public boolean onStartJob(JobParameters params) {
         sLogger.d(TAG + ": onStartJob()");
-        OdpJobServiceLogger.getInstance(this)
-                .recordOnStartJob(USER_DATA_COLLECTION_ID);
-        if (FlagsFactory.getFlags().getGlobalKillSwitch()) {
+        OdpJobServiceLogger.getInstance(this).recordOnStartJob(USER_DATA_COLLECTION_ID);
+        if (mInjector.getFlags().getGlobalKillSwitch()) {
             sLogger.d(TAG + ": GlobalKillSwitch enabled, finishing job.");
-            return cancelAndFinishJob(params,
+            return cancelAndFinishJob(
+                    params,
                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON);
         }
-        if (!UserPrivacyStatus.getInstance().isProtectedAudienceEnabled()
-                        && !UserPrivacyStatus.getInstance().isMeasurementEnabled()) {
-            sLogger.d(TAG + ": user control is revoked, "
-                            + "deleting existing user data and finishing job.");
-            mUserDataCollector = UserDataCollector.getInstance(this);
-            mUserData = RawUserData.getInstance();
-            mUserDataCollector.clearUserData(mUserData);
-            mUserDataCollector.clearMetadata();
-            OdpJobServiceLogger.getInstance(this).recordJobSkipped(
-                    USER_DATA_COLLECTION_ID,
-                    AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_PERSONALIZATION_NOT_ENABLED);
-            jobFinished(params, /* wantsReschedule = */ false);
-            return true;
-        }
+        runPrivacyStatusChecksInBackground(params);
+        return true;
+    }
+
+    private void runPrivacyStatusChecksInBackground(final JobParameters params) {
+        OnDevicePersonalizationExecutors.getHighPriorityBackgroundExecutor().execute(() -> {
+            boolean isProtectedAudienceAndMeasurementBothDisabled =
+                    UserPrivacyStatus.getInstance()
+                            .isProtectedAudienceAndMeasurementBothDisabled();
+            sLogger.d(TAG + ": is ProtectedAudience and Measurement both disabled: %s",
+                    isProtectedAudienceAndMeasurementBothDisabled);
+            if (isProtectedAudienceAndMeasurementBothDisabled) {
+                handlePrivacyControlsRevoked(params);
+            } else {
+                startUserDataCollectionJob(params);
+            }
+        });
+    }
+
+    private void handlePrivacyControlsRevoked(JobParameters params) {
+        sLogger.d(TAG
+                + ": user control is revoked, deleting existing user data and finishing job.");
+        mUserDataCollector = UserDataCollector.getInstance(this);
+        mUserData = RawUserData.getInstance();
+        mUserDataCollector.clearUserData(mUserData);
+        mUserDataCollector.clearMetadata();
+        OdpJobServiceLogger.getInstance(this)
+                .recordJobSkipped(
+                        USER_DATA_COLLECTION_ID,
+                        AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_PERSONALIZATION_NOT_ENABLED);
+        jobFinished(params, /* wantsReschedule= */ false);
+    }
+
+    private void startUserDataCollectionJob(final JobParameters params) {
         mUserDataCollector = UserDataCollector.getInstance(this);
         mUserData = RawUserData.getInstance();
         mFuture = Futures.submit(new Runnable() {
@@ -108,13 +146,12 @@
             public void run() {
                 sLogger.d(TAG + ": Running user data collection job");
                 try {
-                    // TODO(b/262749958): add multi-threading support if necessary.
                     mUserDataCollector.updateUserData(mUserData);
                 } catch (Exception e) {
                     sLogger.e(TAG + ": Failed to collect user data", e);
                 }
             }
-        }, OnDevicePersonalizationExecutors.getBackgroundExecutor());
+        }, mInjector.getExecutor());
 
         Futures.addCallback(
                 mFuture,
@@ -122,32 +159,27 @@
                     @Override
                     public void onSuccess(Void result) {
                         sLogger.d(TAG + ": User data collection job completed.");
-                        boolean wantsReschedule = false;
-                        OdpJobServiceLogger.getInstance(UserDataCollectionJobService.this)
-                                .recordJobFinished(
-                                        USER_DATA_COLLECTION_ID,
-                                        /* isSuccessful= */ true,
-                                        wantsReschedule);
-                        jobFinished(params, wantsReschedule);
+                        handleJobCompletion(params, /* isSuccessful= */ true);
                     }
 
                     @Override
                     public void onFailure(Throwable t) {
-                        sLogger.e(TAG + ": Failed to handle JobService: " + params.getJobId(), t);
-                        boolean wantsReschedule = false;
-                        OdpJobServiceLogger.getInstance(UserDataCollectionJobService.this)
-                                .recordJobFinished(
-                                        USER_DATA_COLLECTION_ID,
-                                        /* isSuccessful= */ false,
-                                        wantsReschedule);
-                        //  When failure, also tell the JobScheduler that the job has completed and
-                        // does not need to be rescheduled.
-                        jobFinished(params, wantsReschedule);
+                        sLogger.e(t, TAG + ": Failed to handle JobService: " + params.getJobId());
+                        handleJobCompletion(params, /* isSuccessful= */ false);
                     }
                 },
-                OnDevicePersonalizationExecutors.getBackgroundExecutor());
+                mInjector.getExecutor()
+        );
+    }
 
-        return true;
+    private void handleJobCompletion(JobParameters params, boolean isSuccessful) {
+        boolean wantsReschedule = false;
+        OdpJobServiceLogger.getInstance(UserDataCollectionJobService.this)
+                .recordJobFinished(
+                        USER_DATA_COLLECTION_ID,
+                        isSuccessful,
+                        wantsReschedule);
+        jobFinished(params, wantsReschedule);
     }
 
     @Override
@@ -158,10 +190,7 @@
         // Reschedule the job since it ended before finishing
         boolean wantsReschedule = true;
         OdpJobServiceLogger.getInstance(this)
-                .recordOnStopJob(
-                        params,
-                        USER_DATA_COLLECTION_ID,
-                        wantsReschedule);
+                .recordOnStopJob(params, USER_DATA_COLLECTION_ID, wantsReschedule);
         return wantsReschedule;
     }
 
@@ -170,10 +199,8 @@
         if (jobScheduler != null) {
             jobScheduler.cancel(USER_DATA_COLLECTION_ID);
         }
-        OdpJobServiceLogger.getInstance(this).recordJobSkipped(
-                USER_DATA_COLLECTION_ID,
-                skipReason);
-        jobFinished(params, /* wantsReschedule = */ false);
+        OdpJobServiceLogger.getInstance(this).recordJobSkipped(USER_DATA_COLLECTION_ID, skipReason);
+        jobFinished(params, /* wantsReschedule= */ false);
         return true;
     }
 }
diff --git a/src/com/android/ondevicepersonalization/services/data/user/UserDataCollector.java b/src/com/android/ondevicepersonalization/services/data/user/UserDataCollector.java
index 013d0ed..de51898 100644
--- a/src/com/android/ondevicepersonalization/services/data/user/UserDataCollector.java
+++ b/src/com/android/ondevicepersonalization/services/data/user/UserDataCollector.java
@@ -47,10 +47,12 @@
 
 /**
  * A collector for getting user data signals. This class only exposes two public operations:
- * periodic update, and real-time update. Periodic update operation will be run every 4 hours in the
- * background, given several on-device resource constraints are satisfied. Real-time update
- * operation will be run before any ads serving request and update a few time-sensitive signals in
- * UserData to the latest version.
+ * periodic update, and real-time update.
+ *
+ * <p>Periodic update operation will be run every 4 hours in the background, given several on-device
+ * resource constraints are satisfied. Real-time update operation will be run before any ads serving
+ * request and update a few time-sensitive signals in {@link
+ * android.adservices.ondevicepersonalization.UserData} to the latest version.
  */
 public class UserDataCollector {
     private static final int MILLISECONDS_IN_MINUTE = 60000;
@@ -60,7 +62,7 @@
     private static final String TAG = UserDataCollector.class.getSimpleName();
 
     @VisibleForTesting
-    public static final Set<Integer> ALLOWED_NETWORK_TYPE =
+    static final Set<Integer> ALLOWED_NETWORK_TYPE =
             Set.of(
                     TelephonyManager.NETWORK_TYPE_UNKNOWN,
                     TelephonyManager.NETWORK_TYPE_GPRS,
@@ -117,7 +119,7 @@
      * testing purpose.
      */
     @VisibleForTesting
-    public static UserDataCollector getInstanceForTest(Context context, UserDataDao userDataDao) {
+    static UserDataCollector getInstanceForTest(Context context, UserDataDao userDataDao) {
         return new UserDataCollector(context, userDataDao);
     }
 
@@ -179,8 +181,7 @@
     }
 
     /** Collects current device's time zone in +/- offset of minutes from UTC. */
-    @VisibleForTesting
-    public void getUtcOffset(RawUserData userData) {
+    private static void getUtcOffset(RawUserData userData) {
         try {
             userData.utcOffset =
                     TimeZone.getDefault().getOffset(System.currentTimeMillis())
@@ -191,8 +192,7 @@
     }
 
     /** Collects the current device orientation. */
-    @VisibleForTesting
-    public void getOrientation(RawUserData userData) {
+    private void getOrientation(RawUserData userData) {
         try {
             userData.orientation = mContext.getResources().getConfiguration().orientation;
         } catch (Exception e) {
@@ -201,8 +201,7 @@
     }
 
     /** Collects available bytes and converts to MB. */
-    @VisibleForTesting
-    public void getAvailableStorageBytes(RawUserData userData) {
+    private static void getAvailableStorageBytes(RawUserData userData) {
         try {
             StatFs statFs = new StatFs(Environment.getDataDirectory().getPath());
             userData.availableStorageBytes = statFs.getAvailableBytes();
@@ -212,8 +211,7 @@
     }
 
     /** Collects the battery percentage of the device. */
-    @VisibleForTesting
-    public void getBatteryPercentage(RawUserData userData) {
+    private void getBatteryPercentage(RawUserData userData) {
         try {
             IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
             Intent batteryStatus = mContext.registerReceiver(null, ifilter);
@@ -230,7 +228,7 @@
 
     /** Collects carrier info. */
     @VisibleForTesting
-    public void getCarrier(RawUserData userData) {
+    private void getCarrier(RawUserData userData) {
         // TODO (b/307158231): handle i18n later if the carrier's name is in non-English script.
         try {
             switch (mTelephonyManager.getSimOperatorName().toUpperCase(Locale.US)) {
@@ -270,20 +268,24 @@
     }
 
     /** Collects network capabilities. */
-    @VisibleForTesting
-    public void getNetworkCapabilities(RawUserData userData) {
+    private void getNetworkCapabilities(RawUserData userData) {
         try {
             NetworkCapabilities networkCapabilities =
                     mConnectivityManager.getNetworkCapabilities(
                             mConnectivityManager.getActiveNetwork());
+            // Returns null if network is unknown.
+            if (networkCapabilities == null) {
+                sLogger.w(TAG + ": networkCapabilities is null");
+                return;
+            }
+            sLogger.d("Successfully collected network capabilities.");
             userData.networkCapabilities = getFilteredNetworkCapabilities(networkCapabilities);
         } catch (Exception e) {
             sLogger.w(TAG + ": Failed to collect networkCapabilities.", e);
         }
     }
 
-    @VisibleForTesting
-    public void getDataNetworkType(RawUserData userData) {
+    private void getDataNetworkType(RawUserData userData) {
         try {
             int dataNetworkType = mTelephonyManager.getDataNetworkType();
             if (!ALLOWED_NETWORK_TYPE.contains(dataNetworkType)) {
@@ -296,8 +298,8 @@
         }
     }
 
-    /** Util to reset all fields in [UserData] to default for testing purpose */
-    public void clearUserData(@NonNull RawUserData userData) {
+    /** Util to reset all fields in passed in {@link RawUserData} to default. */
+    public static void clearUserData(@NonNull RawUserData userData) {
         userData.utcOffset = 0;
         userData.orientation = Configuration.ORIENTATION_PORTRAIT;
         userData.availableStorageBytes = 0;
@@ -307,7 +309,7 @@
         userData.installedApps.clear();
     }
 
-    /** Util to reset all in-memory metadata for testing purpose. */
+    /** Util to reset all in-memory metadata. */
     public void clearMetadata() {
         mInitialized = false;
     }
@@ -332,7 +334,7 @@
         return builder.build();
     }
 
-    /** Initials the installed app list by reading from database. */
+    /** Initialize the installed app list by reading from database. */
     public void initialInstalledApp(RawUserData userData) {
         Map<String, Long> existingInstallApps = mUserDataDao.getAppInstallMap();
         userData.installedApps = existingInstallApps.keySet();
@@ -340,7 +342,7 @@
 
     /** Updates app installed list if necessary. */
     @VisibleForTesting
-    public void updateInstalledApps(RawUserData userData) {
+    void updateInstalledApps(RawUserData userData) {
         try {
             Map<String, Long> existingInstallApps = mUserDataDao.getAppInstallMap();
             PackageManager packageManager = mContext.getPackageManager();
@@ -370,7 +372,7 @@
             currentAppInstallMap.put(packageName, currentTime);
         }
 
-        // Iterator the new app install list and remove expired apps over 30 days (ttl).
+        // Iterate the new app install list and remove expired apps over 30 days (ttl).
         long ttl = FlagsFactory.getFlags().getAppInstallHistoryTtlInMillis();
         for (Map.Entry<String, Long> entry : existingInstallApps.entrySet()) {
             String packageName = entry.getKey();
diff --git a/src/com/android/ondevicepersonalization/services/data/user/UserPrivacyStatus.java b/src/com/android/ondevicepersonalization/services/data/user/UserPrivacyStatus.java
index 4243abb..a27a0aa 100644
--- a/src/com/android/ondevicepersonalization/services/data/user/UserPrivacyStatus.java
+++ b/src/com/android/ondevicepersonalization/services/data/user/UserPrivacyStatus.java
@@ -16,38 +16,31 @@
 
 package com.android.ondevicepersonalization.services.data.user;
 
+import static android.adservices.ondevicepersonalization.Constants.API_NAME_ADSERVICES_GET_COMMON_STATES;
+import static android.adservices.ondevicepersonalization.Constants.STATUS_CALLER_NOT_ALLOWED;
+import static android.adservices.ondevicepersonalization.Constants.STATUS_INTERNAL_ERROR;
+import static android.adservices.ondevicepersonalization.Constants.STATUS_METHOD_NOT_FOUND;
+import static android.adservices.ondevicepersonalization.Constants.STATUS_REMOTE_EXCEPTION;
+import static android.adservices.ondevicepersonalization.Constants.STATUS_SUCCESS;
+import static android.adservices.ondevicepersonalization.Constants.STATUS_TIMEOUT;
+
 import static com.android.ondevicepersonalization.services.PhFlags.KEY_ENABLE_PERSONALIZATION_STATUS_OVERRIDE;
 import static com.android.ondevicepersonalization.services.PhFlags.KEY_PERSONALIZATION_STATUS_OVERRIDE_VALUE;
 import static com.android.ondevicepersonalization.services.PhFlags.KEY_USER_CONTROL_CACHE_IN_MILLIS;
 
-import android.adservices.common.AdServicesCommonManager;
-import android.adservices.common.AdServicesCommonStates;
-import android.adservices.common.AdServicesCommonStatesResponse;
-import android.adservices.common.AdServicesOutcomeReceiver;
-import android.adservices.ondevicepersonalization.Constants;
-import android.annotation.NonNull;
-import android.content.Context;
-import android.ondevicepersonalization.IOnDevicePersonalizationSystemService;
-import android.ondevicepersonalization.IOnDevicePersonalizationSystemServiceCallback;
-import android.ondevicepersonalization.OnDevicePersonalizationSystemServiceManager;
-import android.os.Bundle;
-
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.modules.utils.build.SdkLevel;
 import com.android.odp.module.common.Clock;
 import com.android.odp.module.common.MonotonicClock;
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
-import com.android.ondevicepersonalization.services.Flags;
-import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationApplication;
-import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
+import com.android.ondevicepersonalization.services.StableFlags;
 import com.android.ondevicepersonalization.services.reset.ResetDataJobService;
 import com.android.ondevicepersonalization.services.util.DebugUtils;
+import com.android.ondevicepersonalization.services.util.StatsUtils;
 
-
-import com.google.common.util.concurrent.ListenableFuture;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
 
 /**
  * A singleton class that stores all user privacy statuses in memory.
@@ -55,53 +48,44 @@
 public final class UserPrivacyStatus {
     private static final String TAG = "UserPrivacyStatus";
     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
-    private static final Clock sClock = MonotonicClock.getInstance();
     private static final String PERSONALIZATION_STATUS_KEY = "PERSONALIZATION_STATUS";
     @VisibleForTesting
     static final int CONTROL_GIVEN_STATUS_CODE = 3;
     @VisibleForTesting
     static final int CONTROL_REVOKED_STATUS_CODE = 2;
-    static volatile UserPrivacyStatus sUserPrivacyStatus = null;
-    private boolean mPersonalizationStatusEnabled;
+    private static final Object sLock = new Object();
+    private static volatile UserPrivacyStatus sUserPrivacyStatus = null;
     private boolean mProtectedAudienceEnabled;
     private boolean mMeasurementEnabled;
     private boolean mProtectedAudienceReset;
     private boolean mMeasurementReset;
     private long mLastUserControlCacheUpdate;
+    private final Clock mClock;
+    private final AdServicesCommonStatesWrapper mAdServicesCommonStatesWrapper;
 
-    private UserPrivacyStatus() {
+    @VisibleForTesting
+    UserPrivacyStatus(
+            AdServicesCommonStatesWrapper wrapper,
+            Clock clock) {
         // Assume the more privacy-safe option until updated.
-        mPersonalizationStatusEnabled = false;
         mProtectedAudienceEnabled = false;
         mMeasurementEnabled = false;
         mProtectedAudienceReset = false;
         mMeasurementReset = false;
         mLastUserControlCacheUpdate = -1L;
+        mAdServicesCommonStatesWrapper = Objects.requireNonNull(wrapper);
+        mClock = Objects.requireNonNull(clock);
     }
 
     /** Returns an instance of UserPrivacyStatus. */
     public static UserPrivacyStatus getInstance() {
         if (sUserPrivacyStatus == null) {
-            synchronized (UserPrivacyStatus.class) {
+            synchronized (sLock) {
                 if (sUserPrivacyStatus == null) {
-                    sUserPrivacyStatus = new UserPrivacyStatus();
-                    // Restore personalization status from the system server on U+ devices.
-                    if (SdkLevel.isAtLeastU()) {
-                        sUserPrivacyStatus.restorePersonalizationStatus();
-                    }
-                }
-            }
-        }
-        return sUserPrivacyStatus;
-    }
-
-    /** Returns an instance of UserPrivacyStatus. */
-    @VisibleForTesting
-    public static UserPrivacyStatus getInstanceForTest() {
-        if (sUserPrivacyStatus == null) {
-            synchronized (UserPrivacyStatus.class) {
-                if (sUserPrivacyStatus == null) {
-                    sUserPrivacyStatus = new UserPrivacyStatus();
+                    sUserPrivacyStatus = new UserPrivacyStatus(
+                            new AdServicesCommonStatesWrapperImpl(
+                                    OnDevicePersonalizationApplication.getAppContext()),
+                            MonotonicClock.getInstance());
                 }
             }
         }
@@ -109,34 +93,34 @@
     }
 
     private static boolean isOverrideEnabled() {
-        Flags flags = FlagsFactory.getFlags();
         return DebugUtils.isDeveloperModeEnabled(
                 OnDevicePersonalizationApplication.getAppContext())
-                && (boolean) flags.getStableFlag(KEY_ENABLE_PERSONALIZATION_STATUS_OVERRIDE);
+                && (boolean) StableFlags.get(KEY_ENABLE_PERSONALIZATION_STATUS_OVERRIDE);
     }
 
-    public void setPersonalizationStatusEnabled(boolean personalizationStatusEnabled) {
-        Flags flags = FlagsFactory.getFlags();
-        if (!isOverrideEnabled()) {
-            mPersonalizationStatusEnabled = personalizationStatusEnabled;
-        }
-    }
-
-    public boolean isPersonalizationStatusEnabled() {
-        Flags flags = FlagsFactory.getFlags();
+    /**
+     * Return if both Protected Audience (PA) and Measurement consent status are disabled
+     */
+    public boolean isProtectedAudienceAndMeasurementBothDisabled() {
         if (isOverrideEnabled()) {
-            return (boolean) flags.getStableFlag(KEY_PERSONALIZATION_STATUS_OVERRIDE_VALUE);
+            boolean overrideToBothEnabled =
+                    (boolean) StableFlags.get(KEY_PERSONALIZATION_STATUS_OVERRIDE_VALUE);
+            return !overrideToBothEnabled;
         }
-        return mPersonalizationStatusEnabled;
+        if (isUserControlCacheValid()) {
+            return !mProtectedAudienceEnabled && !mMeasurementEnabled;
+        }
+        // make request to AdServices#getCommonStates API once
+        fetchStateFromAdServices();
+        return !mProtectedAudienceEnabled && !mMeasurementEnabled;
     }
 
     /**
      * Returns the user control status of Protected Audience (PA).
      */
     public boolean isProtectedAudienceEnabled() {
-        Flags flags = FlagsFactory.getFlags();
         if (isOverrideEnabled()) {
-            return (boolean) flags.getStableFlag(KEY_PERSONALIZATION_STATUS_OVERRIDE_VALUE);
+            return (boolean) StableFlags.get(KEY_PERSONALIZATION_STATUS_OVERRIDE_VALUE);
         }
         if (isUserControlCacheValid()) {
             return mProtectedAudienceEnabled;
@@ -150,9 +134,8 @@
      * Returns the user control status of Measurement.
      */
     public boolean isMeasurementEnabled() {
-        Flags flags = FlagsFactory.getFlags();
         if (isOverrideEnabled()) {
-            return (boolean) flags.getStableFlag(KEY_PERSONALIZATION_STATUS_OVERRIDE_VALUE);
+            return (boolean) StableFlags.get(KEY_PERSONALIZATION_STATUS_OVERRIDE_VALUE);
         }
         if (isUserControlCacheValid()) {
             return mMeasurementEnabled;
@@ -186,7 +169,7 @@
         mMeasurementEnabled = (measurementState != CONTROL_REVOKED_STATUS_CODE);
         mProtectedAudienceReset = (protectedAudienceState != CONTROL_GIVEN_STATUS_CODE);
         mMeasurementReset = (measurementState != CONTROL_GIVEN_STATUS_CODE);
-        mLastUserControlCacheUpdate = sClock.currentTimeMillis();
+        mLastUserControlCacheUpdate = mClock.currentTimeMillis();
         handleResetIfNeeded();
     }
 
@@ -198,10 +181,9 @@
         if (mLastUserControlCacheUpdate == -1L) {
             return false;
         }
-        long cacheDuration = sClock.currentTimeMillis() - mLastUserControlCacheUpdate;
+        long cacheDuration = mClock.currentTimeMillis() - mLastUserControlCacheUpdate;
         return cacheDuration >= 0
-                        && cacheDuration < (long) FlagsFactory.getFlags().getStableFlag(
-                                        KEY_USER_CONTROL_CACHE_IN_MILLIS);
+                && cacheDuration < (long) StableFlags.get(KEY_USER_CONTROL_CACHE_IN_MILLIS);
     }
 
     /**
@@ -221,24 +203,38 @@
      */
     @VisibleForTesting
     void invalidateUserControlCacheForTesting() {
-        mLastUserControlCacheUpdate = sClock.currentTimeMillis()
-                        - 2 * (long) FlagsFactory.getFlags().getStableFlag(
-                                        KEY_USER_CONTROL_CACHE_IN_MILLIS);
+        mLastUserControlCacheUpdate = mClock.currentTimeMillis()
+                        - 2 * (long) StableFlags.get(KEY_USER_CONTROL_CACHE_IN_MILLIS);
     }
 
     private void fetchStateFromAdServices() {
+        long startTime = mClock.elapsedRealtime();
+        String packageName = OnDevicePersonalizationApplication.getAppContext().getPackageName();
         try {
             // IPC.
-            AdServicesCommonManager adServicesCommonManager = getAdServicesCommonManager();
-            AdServicesCommonStates commonStates =
-                            getAdServicesCommonStates(adServicesCommonManager);
-
+            AdServicesCommonStatesWrapper.CommonStatesResult commonStates =
+                    mAdServicesCommonStatesWrapper.getCommonStates().get();
+            StatsUtils.writeServiceRequestMetrics(
+                    API_NAME_ADSERVICES_GET_COMMON_STATES,
+                    packageName,
+                    null,
+                    mClock,
+                    STATUS_SUCCESS,
+                    startTime);
             // update cache.
             int updatedProtectedAudienceState = commonStates.getPaState();
             int updatedMeasurementState = commonStates.getMeasurementState();
             updateUserControlCache(updatedProtectedAudienceState, updatedMeasurementState);
         } catch (Exception e) {
-            sLogger.e(TAG + ": fetchStateFromAdServices error", e);
+            int statusCode = getExceptionStatus(e);
+            sLogger.e(e, TAG + ": fetchStateFromAdServices error, status code %d", statusCode);
+            StatsUtils.writeServiceRequestMetrics(
+                    API_NAME_ADSERVICES_GET_COMMON_STATES,
+                    packageName,
+                    null,
+                    mClock,
+                    statusCode,
+                    startTime);
         }
     }
 
@@ -248,100 +244,20 @@
         }
     }
 
-    /**
-     * Get AdServices common manager from ODP.
-     */
-    private static AdServicesCommonManager getAdServicesCommonManager() {
-        Context odpContext = OnDevicePersonalizationApplication.getAppContext();
-        try {
-            return odpContext.getSystemService(AdServicesCommonManager.class);
-        } catch (NoClassDefFoundError e) {
-            throw new IllegalStateException("Cannot find AdServicesCommonManager.", e);
+    @VisibleForTesting
+    int getExceptionStatus(Exception e) {
+        if (e instanceof ExecutionException && e.getCause() instanceof TimeoutException) {
+            return STATUS_TIMEOUT;
         }
-    }
-
-    /**
-     * Get common states from AdServices, such as user control.
-     */
-    private AdServicesCommonStates getAdServicesCommonStates(
-                    @NonNull AdServicesCommonManager adServicesCommonManager) {
-        ListenableFuture<AdServicesCommonStatesResponse> response =
-                        getAdServicesResponse(adServicesCommonManager);
-        try {
-            return response.get().getAdServicesCommonStates();
-        } catch (Exception e) {
-            throw new IllegalStateException("Failed when calling "
-                    + "AdServicesCommonManager#getAdServicesCommonStates().", e);
+        if (e instanceof NoSuchMethodException) {
+            return STATUS_METHOD_NOT_FOUND;
         }
-    }
-
-    /**
-     * IPC to AdServices API.
-     */
-    private ListenableFuture<AdServicesCommonStatesResponse> getAdServicesResponse(
-                    @NonNull AdServicesCommonManager adServicesCommonManager) {
-        return CallbackToFutureAdapter.getFuture(
-                completer -> {
-                    adServicesCommonManager.getAdservicesCommonStates(
-                            OnDevicePersonalizationExecutors.getBackgroundExecutor(),
-                            new AdServicesOutcomeReceiver<AdServicesCommonStatesResponse,
-                                    Exception>() {
-                                @Override
-                                public void onResult(AdServicesCommonStatesResponse result) {
-                                    completer.set(result);
-                                }
-
-                                @Override
-                                public void onError(Exception error) {
-                                    completer.setException(error);
-                                }
-                            });
-                    // For debugging purpose only.
-                    return "getAdServicesCommonStates";
-                }
-        );
-    }
-
-    // TODO (b/331684191): remove SecurityException after mocking all UserPrivacyStatus
-    private void restorePersonalizationStatus() {
-        if (isOverrideEnabled()) {
-            return;
+        if (e instanceof SecurityException) {
+            return STATUS_CALLER_NOT_ALLOWED;
         }
-        Context odpContext = OnDevicePersonalizationApplication.getAppContext();
-        OnDevicePersonalizationSystemServiceManager systemServiceManager =
-                odpContext.getSystemService(OnDevicePersonalizationSystemServiceManager.class);
-        if (systemServiceManager != null) {
-            IOnDevicePersonalizationSystemService systemService =
-                    systemServiceManager.getService();
-            if (systemService != null) {
-                try {
-                    systemService.readPersonalizationStatus(
-                            new IOnDevicePersonalizationSystemServiceCallback.Stub() {
-                                @Override
-                                public void onResult(Bundle bundle) {
-                                    boolean personalizationStatus =
-                                            bundle.getBoolean(PERSONALIZATION_STATUS_KEY);
-                                    setPersonalizationStatusEnabled(personalizationStatus);
-                                }
-
-                                @Override
-                                public void onError(int errorCode) {
-                                    if (errorCode == Constants.STATUS_KEY_NOT_FOUND) {
-                                        sLogger.d(
-                                                TAG
-                                                        + ": Personalization status "
-                                                        + "not found in the system server");
-                                    }
-                                }
-                            });
-                } catch (Exception e) {
-                    sLogger.e(TAG + ": Error when reading personalization status.", e);
-                }
-            } else {
-                sLogger.w(TAG + ": System service is not ready.");
-            }
-        } else {
-            sLogger.w(TAG + ": Cannot find system server on U+ devices.");
+        if (e instanceof IllegalArgumentException) {
+            return STATUS_INTERNAL_ERROR;
         }
+        return STATUS_REMOTE_EXCEPTION;
     }
 }
diff --git a/src/com/android/ondevicepersonalization/services/data/vendor/OnDevicePersonalizationLocalDataDao.java b/src/com/android/ondevicepersonalization/services/data/vendor/OnDevicePersonalizationLocalDataDao.java
index ad80adc..6ff2061 100644
--- a/src/com/android/ondevicepersonalization/services/data/vendor/OnDevicePersonalizationLocalDataDao.java
+++ b/src/com/android/ondevicepersonalization/services/data/vendor/OnDevicePersonalizationLocalDataDao.java
@@ -25,7 +25,6 @@
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteException;
 
-
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 import com.android.ondevicepersonalization.services.data.DbUtils;
@@ -151,7 +150,11 @@
      * Creates local data tables and adds corresponding vendor_settings metadata
      */
     public boolean createTable() {
-        SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
+        if (db == null) {
+            return false;
+        }
+
         try {
             db.beginTransactionNonExclusive();
             if (!createTableIfNotExists()) {
@@ -310,7 +313,12 @@
     public static void deleteTable(Context context, ComponentName owner, String certDigest) {
         OnDevicePersonalizationDbHelper dbHelper =
                 OnDevicePersonalizationDbHelper.getInstance(context);
-        SQLiteDatabase db = dbHelper.getWritableDatabase();
+        SQLiteDatabase db = dbHelper.safeGetWritableDatabase();
+        if (db == null) {
+            sLogger.e(TAG + ": Failed to get database.");
+            return;
+        }
+
         db.execSQL("DROP TABLE IF EXISTS " + getTableName(owner, certDigest));
     }
 }
diff --git a/src/com/android/ondevicepersonalization/services/data/vendor/OnDevicePersonalizationVendorDataDao.java b/src/com/android/ondevicepersonalization/services/data/vendor/OnDevicePersonalizationVendorDataDao.java
index 4b77876..2b8d57d 100644
--- a/src/com/android/ondevicepersonalization/services/data/vendor/OnDevicePersonalizationVendorDataDao.java
+++ b/src/com/android/ondevicepersonalization/services/data/vendor/OnDevicePersonalizationVendorDataDao.java
@@ -152,7 +152,12 @@
     public static List<Map.Entry<String, String>> getVendors(Context context) {
         OnDevicePersonalizationDbHelper dbHelper =
                 OnDevicePersonalizationDbHelper.getInstance(context);
-        SQLiteDatabase db = dbHelper.getReadableDatabase();
+        List<Map.Entry<String, String>> result = new ArrayList<>();
+        SQLiteDatabase db = dbHelper.safeGetReadableDatabase();
+        if (db == null) {
+            return result;
+        }
+
         String[] projection = {VendorSettingsContract.VendorSettingsEntry.OWNER,
                 VendorSettingsContract.VendorSettingsEntry.CERT_DIGEST};
         Cursor cursor = db.query(
@@ -167,7 +172,7 @@
                 /* limit= */ null
         );
 
-        List<Map.Entry<String, String>> result = new ArrayList<>();
+
         try {
             while (cursor.moveToNext()) {
                 String owner = cursor.getString(cursor.getColumnIndexOrThrow(
@@ -191,7 +196,11 @@
             Context context, ComponentName owner, String certDigest) {
         OnDevicePersonalizationDbHelper dbHelper =
                 OnDevicePersonalizationDbHelper.getInstance(context);
-        SQLiteDatabase db = dbHelper.getWritableDatabase();
+        SQLiteDatabase db = dbHelper.safeGetWritableDatabase();
+        if (db == null) {
+            return false;
+        }
+
         String vendorDataTableName = getTableName(owner, certDigest);
         try {
             db.beginTransactionNonExclusive();
@@ -345,7 +354,11 @@
      */
     public boolean batchUpdateOrInsertVendorDataTransaction(List<VendorData> vendorDataList,
             List<String> retainedKeys, long syncToken) {
-        SQLiteDatabase db = mDbHelper.getWritableDatabase();
+        SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
+        if (db == null) {
+            return false;
+        }
+
         try {
             db.beginTransactionNonExclusive();
             if (!createTableIfNotExists(mTableName)) {
@@ -566,7 +579,11 @@
      * @return syncToken if found, -1 otherwise
      */
     public long getSyncToken() {
-        SQLiteDatabase db = mDbHelper.getReadableDatabase();
+        SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
+        if (db == null) {
+            return -1;
+        }
+
         String selection = VendorSettingsContract.VendorSettingsEntry.OWNER + " = ? AND "
                 + VendorSettingsContract.VendorSettingsEntry.CERT_DIGEST + " = ?";
         String[] selectionArgs = {DbUtils.toTableValue(mOwner), mCertDigest};
diff --git a/src/com/android/ondevicepersonalization/services/display/WebViewFlow.java b/src/com/android/ondevicepersonalization/services/display/WebViewFlow.java
index 1731520..c5892fe 100644
--- a/src/com/android/ondevicepersonalization/services/display/WebViewFlow.java
+++ b/src/com/android/ondevicepersonalization/services/display/WebViewFlow.java
@@ -159,28 +159,33 @@
 
     @Override
     public void uploadServiceFlowMetrics(ListenableFuture<Bundle> runServiceFuture) {
-        var unused = FluentFuture.from(runServiceFuture)
-                .transform(
-                        result -> {
-                            StatsUtils.writeServiceRequestMetrics(
-                                    Constants.API_NAME_SERVICE_ON_EVENT,
-                                    result, mInjector.getClock(),
-                                    Constants.STATUS_SUCCESS,
-                                    mStartServiceTimeMillis);
-                            return null;
-                        },
-                        mInjector.getExecutor())
-                .catchingAsync(
-                        Exception.class,
-                        e -> {
-                            StatsUtils.writeServiceRequestMetrics(
-                                    Constants.API_NAME_SERVICE_ON_EVENT,
-                                    /* result= */ null, mInjector.getClock(),
-                                    Constants.STATUS_INTERNAL_ERROR,
-                                    mStartServiceTimeMillis);
-                            return Futures.immediateFailedFuture(e);
-                        },
-                        mInjector.getExecutor());
+        var unused =
+                FluentFuture.from(runServiceFuture)
+                        .transform(
+                                result -> {
+                                    StatsUtils.writeServiceRequestMetrics(
+                                            Constants.API_NAME_SERVICE_ON_EVENT,
+                                            mService.getPackageName(),
+                                            result,
+                                            mInjector.getClock(),
+                                            Constants.STATUS_SUCCESS,
+                                            mStartServiceTimeMillis);
+                                    return null;
+                                },
+                                mInjector.getExecutor())
+                        .catchingAsync(
+                                Exception.class,
+                                e -> {
+                                    StatsUtils.writeServiceRequestMetrics(
+                                            Constants.API_NAME_SERVICE_ON_EVENT,
+                                            mService.getPackageName(),
+                                            /* result= */ null,
+                                            mInjector.getClock(),
+                                            Constants.STATUS_INTERNAL_ERROR,
+                                            mStartServiceTimeMillis);
+                                    return Futures.immediateFailedFuture(e);
+                                },
+                                mInjector.getExecutor());
     }
 
     @Override
diff --git a/src/com/android/ondevicepersonalization/services/download/DownloadFlow.java b/src/com/android/ondevicepersonalization/services/download/DownloadFlow.java
index f8746a0..118e66a 100644
--- a/src/com/android/ondevicepersonalization/services/download/DownloadFlow.java
+++ b/src/com/android/ondevicepersonalization/services/download/DownloadFlow.java
@@ -153,10 +153,10 @@
             long existingSyncToken = mDao.getSyncToken();
 
             // If existingToken is greaterThan or equal to the new token, skip as there is
-            // no new data.
+            // no new data. Mark success to upstream caller for reporting purpose
             if (existingSyncToken >= syncToken) {
                 sLogger.d(TAG + ": syncToken is not newer than existing token.");
-                mCallback.onFailure(new IllegalArgumentException("SyncToken is stale."));
+                mCallback.onSuccess(null);
                 return false;
             }
 
@@ -218,27 +218,33 @@
 
     @Override
     public void uploadServiceFlowMetrics(ListenableFuture<Bundle> runServiceFuture) {
-        var unused = FluentFuture.from(runServiceFuture)
-                .transform(
-                        val -> {
-                            StatsUtils.writeServiceRequestMetrics(
-                                    Constants.API_NAME_SERVICE_ON_DOWNLOAD_COMPLETED,
-                                    val, mInjector.getClock(), Constants.STATUS_SUCCESS,
-                                    mStartServiceTimeMillis);
-                            return val;
-                        },
-                        mInjector.getExecutor())
-                .catchingAsync(
-                        Exception.class,
-                        e -> {
-                            StatsUtils.writeServiceRequestMetrics(
-                                    Constants.API_NAME_SERVICE_ON_DOWNLOAD_COMPLETED,
-                                    /* result= */ null, mInjector.getClock(),
-                                    Constants.STATUS_INTERNAL_ERROR,
-                                    mStartServiceTimeMillis);
-                            return Futures.immediateFailedFuture(e);
-                        },
-                        mInjector.getExecutor());
+        var unused =
+                FluentFuture.from(runServiceFuture)
+                        .transform(
+                                val -> {
+                                    StatsUtils.writeServiceRequestMetrics(
+                                            Constants.API_NAME_SERVICE_ON_DOWNLOAD_COMPLETED,
+                                            mService.getPackageName(),
+                                            val,
+                                            mInjector.getClock(),
+                                            Constants.STATUS_SUCCESS,
+                                            mStartServiceTimeMillis);
+                                    return val;
+                                },
+                                mInjector.getExecutor())
+                        .catchingAsync(
+                                Exception.class,
+                                e -> {
+                                    StatsUtils.writeServiceRequestMetrics(
+                                            Constants.API_NAME_SERVICE_ON_DOWNLOAD_COMPLETED,
+                                            mService.getPackageName(),
+                                            /* result= */ null,
+                                            mInjector.getClock(),
+                                            Constants.STATUS_INTERNAL_ERROR,
+                                            mStartServiceTimeMillis);
+                                    return Futures.immediateFailedFuture(e);
+                                },
+                                mInjector.getExecutor());
     }
 
     @Override
@@ -388,7 +394,8 @@
 
         if (cfg == null || cfg.getStatus() != ClientConfigProto.ClientFileGroup.Status.DOWNLOADED) {
             sLogger.d(TAG + mPackageName + " has no completed downloads.");
-            mCallback.onFailure(new IllegalArgumentException("No completed downloads."));
+            // No completed downloads is a valid case. Mark as success and return null.
+            mCallback.onSuccess(null);
             return null;
         }
 
diff --git a/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDataProcessingAsyncCallable.java b/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDataProcessingAsyncCallable.java
index 9774129..008f6cf 100644
--- a/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDataProcessingAsyncCallable.java
+++ b/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDataProcessingAsyncCallable.java
@@ -30,10 +30,8 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.SettableFuture;
 
-/**
- * AsyncCallable to handle the processing of the downloaded vendor data
- */
-public class OnDevicePersonalizationDataProcessingAsyncCallable implements AsyncCallable {
+/** AsyncCallable to handle the processing of the downloaded vendor data */
+class OnDevicePersonalizationDataProcessingAsyncCallable implements AsyncCallable {
     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
 
     private static final ServiceFlowOrchestrator sSfo = ServiceFlowOrchestrator.getInstance();
@@ -43,7 +41,7 @@
     private final Injector mInjector;
 
     @VisibleForTesting
-    public static class Injector {
+    static class Injector {
         FutureCallback<DownloadCompletedOutputParcel> getFutureCallback(
                 SettableFuture<Boolean> downloadFlowFuture) {
             return new FutureCallback<>() {
@@ -60,23 +58,23 @@
         }
     }
 
-    public OnDevicePersonalizationDataProcessingAsyncCallable(String packageName,
-            Context context) {
+    OnDevicePersonalizationDataProcessingAsyncCallable(String packageName, Context context) {
         this(packageName, context, new Injector());
     }
 
     @VisibleForTesting
-    public OnDevicePersonalizationDataProcessingAsyncCallable(String packageName,
-            Context context, Injector injector) {
+    OnDevicePersonalizationDataProcessingAsyncCallable(
+            String packageName, Context context, Injector injector) {
         mPackageName = packageName;
         mContext = context;
         mInjector = injector;
     }
 
     /**
-     * Processes the downloaded files for the given package and stores the data into sqlite
-     * vendor tables.
+     * Processes the downloaded files for the given package and stores the data into sqlite vendor
+     * tables.
      */
+    @Override
     public ListenableFuture<Boolean> call() {
         SettableFuture<Boolean> downloadFlowFuture = SettableFuture.create();
         FutureCallback<DownloadCompletedOutputParcel> callback =
diff --git a/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDownloadProcessingJobService.java b/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDownloadProcessingJobService.java
index 8c5dbfc..36a3669 100644
--- a/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDownloadProcessingJobService.java
+++ b/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDownloadProcessingJobService.java
@@ -17,7 +17,6 @@
 package com.android.ondevicepersonalization.services.download;
 
 import static android.app.job.JobScheduler.RESULT_FAILURE;
-import static android.content.pm.PackageManager.GET_META_DATA;
 
 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON;
 import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.DOWNLOAD_PROCESSING_TASK_JOB_ID;
@@ -28,13 +27,10 @@
 import android.app.job.JobService;
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
 
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
-import com.android.ondevicepersonalization.services.enrollment.PartnerEnrollmentChecker;
 import com.android.ondevicepersonalization.services.manifest.AppManifestConfigHelper;
 import com.android.ondevicepersonalization.services.statsd.joblogging.OdpJobServiceLogger;
 
@@ -90,44 +86,68 @@
             return true;
         }
 
-        mFutures = new ArrayList<>();
-        for (PackageInfo packageInfo : this.getPackageManager().getInstalledPackages(
-                PackageManager.PackageInfoFlags.of(GET_META_DATA))) {
-            String packageName = packageInfo.packageName;
-            if (AppManifestConfigHelper.manifestContainsOdpSettings(
-                    this, packageName)) {
-                if (!PartnerEnrollmentChecker.isIsolatedServiceEnrolled(packageName)) {
-                    sLogger.d(TAG + ": service %s has ODP manifest, but not enrolled",
-                            packageName);
-                    continue;
-                }
-                sLogger.d(TAG + ": service %s has ODP manifest and is enrolled", packageName);
-                mFutures.add(Futures.submitAsync(
-                        new OnDevicePersonalizationDataProcessingAsyncCallable(packageName,
-                                this),
-                        OnDevicePersonalizationExecutors.getBackgroundExecutor()));
-            }
-        }
-        var unused = Futures.whenAllComplete(mFutures).call(() -> {
-            boolean wantsReschedule = false;
-            boolean allSuccess = true;
-            for (ListenableFuture<Void> future : mFutures) {
-                try {
-                    future.get();
-                } catch (Exception e) {
-                    allSuccess = false;
-                    break;
-                }
-            }
-            OdpJobServiceLogger.getInstance(
-                    OnDevicePersonalizationDownloadProcessingJobService.this)
-                    .recordJobFinished(
-                            DOWNLOAD_PROCESSING_TASK_JOB_ID,
-                            /* isSuccessful= */ allSuccess,
-                            wantsReschedule);
-            jobFinished(params, wantsReschedule);
-            return null;
-        }, OnDevicePersonalizationExecutors.getLightweightExecutor());
+        OnDevicePersonalizationExecutors.getHighPriorityBackgroundExecutor()
+                .execute(
+                        () -> {
+                            mFutures = new ArrayList<>();
+                            // Processing installed packages
+                            for (String packageName :
+                                    AppManifestConfigHelper.getOdpPackages(
+                                            /* context= */ this, /* enrolledOnly= */ true)) {
+                                mFutures.add(
+                                        Futures.submitAsync(
+                                                new OnDevicePersonalizationDataProcessingAsyncCallable(
+                                                        packageName, /* context= */ this),
+                                                OnDevicePersonalizationExecutors
+                                                        .getBackgroundExecutor()));
+                            }
+
+                            // Handling task completion asynchronously
+                            var unused =
+                                    Futures.whenAllComplete(mFutures)
+                                            .call(
+                                                    () -> {
+                                                        boolean wantsReschedule = false;
+                                                        boolean allSuccess = true;
+                                                        int successTaskCount = 0;
+                                                        int failureTaskCount = 0;
+                                                        for (ListenableFuture<Void> future :
+                                                                mFutures) {
+                                                            try {
+                                                                future.get();
+                                                                successTaskCount++;
+                                                            } catch (Exception e) {
+                                                                sLogger.e(
+                                                                        e,
+                                                                        TAG
+                                                                                + ": Error"
+                                                                                + " processing"
+                                                                                + " future");
+                                                                failureTaskCount++;
+                                                                allSuccess = false;
+                                                            }
+                                                        }
+                                                        sLogger.d(
+                                                                TAG
+                                                                        + ": all download"
+                                                                        + " processing tasks"
+                                                                        + " finished, %d succeeded,"
+                                                                        + " %d failed",
+                                                                successTaskCount,
+                                                                failureTaskCount);
+                                                        OdpJobServiceLogger.getInstance(
+                                                                        OnDevicePersonalizationDownloadProcessingJobService
+                                                                                .this)
+                                                                .recordJobFinished(
+                                                                        DOWNLOAD_PROCESSING_TASK_JOB_ID,
+                                                                        /* isSuccessful= */ allSuccess,
+                                                                        wantsReschedule);
+                                                        jobFinished(params, wantsReschedule);
+                                                        return null;
+                                                    },
+                                                    OnDevicePersonalizationExecutors
+                                                            .getLightweightExecutor());
+                        });
 
         return true;
     }
diff --git a/src/com/android/ondevicepersonalization/services/download/mdd/MddJobService.java b/src/com/android/ondevicepersonalization/services/download/mdd/MddJobService.java
index 4e7c686..fd16918 100644
--- a/src/com/android/ondevicepersonalization/services/download/mdd/MddJobService.java
+++ b/src/com/android/ondevicepersonalization/services/download/mdd/MddJobService.java
@@ -25,10 +25,11 @@
 import android.app.job.JobParameters;
 import android.app.job.JobScheduler;
 import android.app.job.JobService;
-import android.content.Context;
 import android.os.PersistableBundle;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
+import com.android.ondevicepersonalization.services.Flags;
 import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
 import com.android.ondevicepersonalization.services.data.user.UserPrivacyStatus;
@@ -39,6 +40,7 @@
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
 
 /**
  * MDD JobService. This will download MDD files in background tasks.
@@ -49,72 +51,102 @@
 
     private String mMddTaskTag;
 
+    private final Injector mInjector;
+
+    public MddJobService() {
+        mInjector = new Injector();
+    }
+
+    @VisibleForTesting
+    public MddJobService(Injector injector) {
+        mInjector = injector;
+    }
+
+    static class Injector {
+        ListeningExecutorService getBackgroundExecutor() {
+            return OnDevicePersonalizationExecutors.getBackgroundExecutor();
+        }
+
+        Flags getFlags() {
+            return FlagsFactory.getFlags();
+        }
+    }
+
     @Override
     public boolean onStartJob(JobParameters params) {
-        int jobId = getMddTaskJobId(params);
         sLogger.d(TAG + ": onStartJob()");
-        OdpJobServiceLogger.getInstance(this).recordOnStartJob(jobId);
-        if (FlagsFactory.getFlags().getGlobalKillSwitch()) {
+        OdpJobServiceLogger.getInstance(this).recordOnStartJob(getMddTaskJobId(params));
+
+        if (mInjector.getFlags().getGlobalKillSwitch()) {
             sLogger.d(TAG + ": GlobalKillSwitch enabled, finishing job.");
             return cancelAndFinishJob(params,
                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON);
         }
 
-        if (!UserPrivacyStatus.getInstance().isMeasurementEnabled()
-                && !UserPrivacyStatus.getInstance().isProtectedAudienceEnabled()) {
-            sLogger.d(TAG + ": User control is not given for all ODP services.");
-            OdpJobServiceLogger.getInstance(this).recordJobSkipped(jobId,
-                    AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_PERSONALIZATION_NOT_ENABLED);
-            jobFinished(params, false);
-            return true;
-        }
-
-        mMddTaskTag = getMddTaskTag(params);
-
-        ListenableFuture<Void> handleTaskFuture =
-                PropagatedFutures.submitAsync(
-                        () -> MobileDataDownloadFactory.getMdd(this).handleTask(mMddTaskTag),
-                        OnDevicePersonalizationExecutors.getBackgroundExecutor());
-
-        Context context = this;
-        Futures.addCallback(
-                handleTaskFuture,
-                new FutureCallback<Void>() {
-                    @Override
-                    public void onSuccess(Void result) {
-                        sLogger.d(TAG + ": MddJobService.MddHandleTask succeeded!");
-                        // Attempt to process any data downloaded
-                        if (WIFI_CHARGING_PERIODIC_TASK.equals(mMddTaskTag)) {
-                            OnDevicePersonalizationDownloadProcessingJobService.schedule(context);
-                        }
-                        boolean wantsReschedule = false;
-                        OdpJobServiceLogger.getInstance(MddJobService.this)
-                                .recordJobFinished(jobId,
-                                        /* isSuccessful= */ true,
-                                        wantsReschedule);
-                        // Tell the JobScheduler that the job has completed and does not needs to be
-                        // rescheduled.
-                        jobFinished(params, wantsReschedule);
-                    }
-
-                    @Override
-                    public void onFailure(Throwable t) {
-                        sLogger.e(TAG + ": Failed to handle JobService: " + jobId, t);
-                        boolean wantsReschedule = false;
-                        OdpJobServiceLogger.getInstance(MddJobService.this)
-                                .recordJobFinished(jobId,
-                                        /* isSuccessful= */ false,
-                                        wantsReschedule);
-                        //  When failure, also tell the JobScheduler that the job has completed and
-                        // does not need to be rescheduled.
-                        jobFinished(params, wantsReschedule);
-                    }
-                },
-                OnDevicePersonalizationExecutors.getBackgroundExecutor());
-
+        // Run privacy status checks in the background
+        runPrivacyStatusChecksInBackgroundAndExecute(params);
         return true;
     }
 
+    private void runPrivacyStatusChecksInBackgroundAndExecute(final JobParameters params) {
+        int jobId = getMddTaskJobId(params);
+        OnDevicePersonalizationExecutors.getHighPriorityBackgroundExecutor().execute(() -> {
+            if (UserPrivacyStatus.getInstance().isProtectedAudienceAndMeasurementBothDisabled()) {
+                // User control is revoked; handle this case
+                sLogger.d(TAG + ": User control is not given for all ODP services.");
+                OdpJobServiceLogger.getInstance(MddJobService.this)
+                        .recordJobSkipped(jobId,
+                                AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_PERSONALIZATION_NOT_ENABLED);
+                jobFinished(params, false);
+            } else {
+                // User control is given; handle the MDD task
+                mMddTaskTag = getMddTaskTag(params);
+
+                ListenableFuture<Void> handleTaskFuture =
+                        PropagatedFutures.submitAsync(
+                                () -> MobileDataDownloadFactory.getMdd(this)
+                                        .handleTask(mMddTaskTag),
+                                mInjector.getBackgroundExecutor());
+
+                Futures.addCallback(
+                        handleTaskFuture,
+                        new FutureCallback<Void>() {
+                            @Override
+                            public void onSuccess(Void result) {
+                                handleSuccess(jobId, params);
+                            }
+
+                            @Override
+                            public void onFailure(Throwable t) {
+                                handleFailure(jobId, params, t);
+                            }
+                        },
+                        mInjector.getBackgroundExecutor());
+            }
+        });
+    }
+
+    private void handleSuccess(int jobId, JobParameters params) {
+        sLogger.d(TAG + ": MddJobService.MddHandleTask succeeded!");
+        if (WIFI_CHARGING_PERIODIC_TASK.equals(mMddTaskTag)) {
+            OnDevicePersonalizationDownloadProcessingJobService.schedule(this);
+        }
+        recordJobFinished(jobId, true);
+        jobFinished(params, false);
+    }
+
+    private void handleFailure(int jobId, JobParameters params, Throwable throwable) {
+        sLogger.e(TAG + ": Failed to handle JobService: " + jobId, throwable);
+        recordJobFinished(jobId, false);
+        jobFinished(params, false);
+    }
+
+    private void recordJobFinished(int jobId, boolean isSuccessful) {
+        boolean wantsReschedule = false;
+        OdpJobServiceLogger.getInstance(this)
+                .recordJobFinished(jobId, isSuccessful, wantsReschedule);
+    }
+
     @Override
     public boolean onStopJob(JobParameters params) {
         // Attempt to process any data downloaded before the worker was stopped.
diff --git a/src/com/android/ondevicepersonalization/services/download/mdd/MddTaskScheduler.java b/src/com/android/ondevicepersonalization/services/download/mdd/MddTaskScheduler.java
index dd5edd4..a3b8cd3 100644
--- a/src/com/android/ondevicepersonalization/services/download/mdd/MddTaskScheduler.java
+++ b/src/com/android/ondevicepersonalization/services/download/mdd/MddTaskScheduler.java
@@ -28,12 +28,16 @@
 import android.content.SharedPreferences;
 import android.os.PersistableBundle;
 
+import com.android.ondevicepersonalization.internal.util.LoggerFactory;
+
 import com.google.android.libraries.mobiledatadownload.TaskScheduler;
 
 /**
  * MddTaskScheduler that uses JobScheduler to schedule MDD background tasks
  */
 public class MddTaskScheduler implements TaskScheduler {
+    private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
+    private static final String TAG = MddTaskScheduler.class.getSimpleName();
     static final String MDD_TASK_TAG_KEY = "MDD_TASK_TAG_KEY";
     private static final String MDD_TASK_SHARED_PREFS = "mdd_worker_task_periods";
     private final Context mContext;
@@ -78,21 +82,28 @@
 
         // When the period change, we will need to update the existing works.
         boolean updateCurrent = false;
-        if (prefs.getLong(mddTaskTag, 0) != periodSeconds) {
+        if (getCurrentPeriodValue(prefs, mddTaskTag) != periodSeconds) {
             SharedPreferences.Editor editor = prefs.edit();
             editor.putLong(mddTaskTag, periodSeconds);
             editor.apply();
             updateCurrent = true;
         }
 
-        if (updateCurrent) {
-            schedulePeriodicTaskWithUpdate(mddTaskTag, periodSeconds, networkState);
+        JobScheduler jobScheduler = mContext.getSystemService(JobScheduler.class);
+        if (jobScheduler.getPendingJob(getMddTaskJobId(mddTaskTag)) == null) {
+            sLogger.d(TAG + ": MddJob %s is not scheduled, scheduling now", mddTaskTag);
+            schedulePeriodicTaskWithUpdate(jobScheduler, mddTaskTag, periodSeconds, networkState);
+        } else if (updateCurrent) {
+            sLogger.d(TAG + ": scheduling MddJob %s with frequency update", mddTaskTag);
+            schedulePeriodicTaskWithUpdate(jobScheduler, mddTaskTag, periodSeconds, networkState);
+        } else {
+            sLogger.d(TAG + ": MddJob %s already scheduled and frequency unchanged,"
+                    + " not scheduling", mddTaskTag);
         }
     }
 
-    private void schedulePeriodicTaskWithUpdate(String mddTaskTag, long periodSeconds,
-            NetworkState networkState) {
-        final JobScheduler jobScheduler = mContext.getSystemService(JobScheduler.class);
+    private void schedulePeriodicTaskWithUpdate(JobScheduler jobScheduler, String mddTaskTag,
+            long periodSeconds, NetworkState networkState) {
 
         // We use Extra to pass the MDD Task Tag. This will be used in the MddJobService.
         PersistableBundle extras = new PersistableBundle();
@@ -113,4 +124,14 @@
                         .build();
         jobScheduler.schedule(job);
     }
+
+    private long getCurrentPeriodValue(SharedPreferences prefs, String mddTaskTag) {
+        try {
+            return prefs.getLong(mddTaskTag, 0);
+        } catch (ClassCastException e) {
+            sLogger.w(e, TAG + ": ClassCastException retrieving long value from prefs for tag: %s",
+                    mddTaskTag);
+            return 0;
+        }
+    }
 }
diff --git a/src/com/android/ondevicepersonalization/services/download/mdd/OnDevicePersonalizationFileGroupPopulator.java b/src/com/android/ondevicepersonalization/services/download/mdd/OnDevicePersonalizationFileGroupPopulator.java
index 482de97..476eec1 100644
--- a/src/com/android/ondevicepersonalization/services/download/mdd/OnDevicePersonalizationFileGroupPopulator.java
+++ b/src/com/android/ondevicepersonalization/services/download/mdd/OnDevicePersonalizationFileGroupPopulator.java
@@ -16,11 +16,8 @@
 
 package com.android.ondevicepersonalization.services.download.mdd;
 
-import static android.content.pm.PackageManager.GET_META_DATA;
-
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.SystemProperties;
@@ -30,7 +27,6 @@
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
 import com.android.ondevicepersonalization.services.data.vendor.OnDevicePersonalizationVendorDataDao;
-import com.android.ondevicepersonalization.services.enrollment.PartnerEnrollmentChecker;
 import com.android.ondevicepersonalization.services.manifest.AppManifestConfigHelper;
 
 import com.google.android.libraries.mobiledatadownload.AddFileGroupRequest;
@@ -77,10 +73,9 @@
         this.mContext = context;
     }
 
-    /**
-     * A helper function to create a DataFilegroup.
-     */
-    public static DataFileGroup createDataFileGroup(
+    /** A helper function to create a DataFilegroup. */
+    @VisibleForTesting
+    static DataFileGroup createDataFileGroup(
             String groupName,
             String ownerPackage,
             String[] fileId,
@@ -138,12 +133,12 @@
      * Creates the MDD download URL for the given package
      *
      * @param packageName PackageName of the package owning the fileGroup
-     * @param context     Context of the calling service/application
+     * @param context Context of the calling service/application
      * @return The created MDD URL for the package.
      */
     @VisibleForTesting
-    public static String createDownloadUrl(String packageName, Context context) throws
-            PackageManager.NameNotFoundException {
+    static String createDownloadUrl(String packageName, Context context)
+            throws PackageManager.NameNotFoundException {
         String baseURL = AppManifestConfigHelper.getDownloadUrlFromOdpSettings(
                 context, packageName);
 
@@ -202,76 +197,75 @@
         GetFileGroupsByFilterRequest request =
                 GetFileGroupsByFilterRequest.newBuilder().setIncludeAllGroups(true).build();
         return FluentFuture.from(mobileDataDownload.getFileGroupsByFilter(request))
-                .transformAsync(fileGroupList -> {
-                    Set<String> fileGroupsToRemove = new HashSet<>();
-                    for (ClientConfigProto.ClientFileGroup fileGroup : fileGroupList) {
-                        fileGroupsToRemove.add(fileGroup.getGroupName());
-                    }
-                    List<ListenableFuture<Boolean>> mFutures = new ArrayList<>();
-                    for (PackageInfo packageInfo : mContext.getPackageManager()
-                            .getInstalledPackages(
-                                    PackageManager.PackageInfoFlags.of(GET_META_DATA))) {
-                        String packageName = packageInfo.packageName;
-                        if (AppManifestConfigHelper.manifestContainsOdpSettings(
-                                mContext, packageName)) {
-                            if (!PartnerEnrollmentChecker.isIsolatedServiceEnrolled(packageName)) {
-                                sLogger.d(TAG + ": service %s has ODP manifest, "
-                                        + "but not enrolled", packageName);
-                                continue;
+                .transformAsync(
+                        fileGroupList -> {
+                            Set<String> fileGroupsToRemove = new HashSet<>();
+                            for (ClientConfigProto.ClientFileGroup fileGroup : fileGroupList) {
+                                fileGroupsToRemove.add(fileGroup.getGroupName());
                             }
-                            sLogger.d(TAG + ": service %s has ODP manifest and is enrolled",
-                                    packageName);
-                            try {
-                                String groupName = createPackageFileGroupName(
-                                        packageName,
-                                        mContext);
-                                fileGroupsToRemove.remove(groupName);
-                                String ownerPackage = mContext.getPackageName();
-                                String fileId = groupName;
-                                int byteSize = 0;
-                                String checksum = "";
-                                ChecksumType checksumType = ChecksumType.NONE;
-                                String downloadUrl = createDownloadUrl(packageName,
-                                        mContext);
-                                DeviceNetworkPolicy deviceNetworkPolicy =
-                                        DeviceNetworkPolicy.DOWNLOAD_ONLY_ON_WIFI;
-                                DataFileGroup dataFileGroup = createDataFileGroup(
-                                        groupName,
-                                        ownerPackage,
-                                        new String[]{fileId},
-                                        new int[]{byteSize},
-                                        new String[]{checksum},
-                                        new ChecksumType[]{checksumType},
-                                        new String[]{downloadUrl},
-                                        deviceNetworkPolicy);
-                                mFutures.add(mobileDataDownload.addFileGroup(
-                                        AddFileGroupRequest.newBuilder().setDataFileGroup(
-                                                dataFileGroup).build()));
-                            } catch (Exception e) {
-                                sLogger.e(TAG + ": Failed to create file group for "
-                                        + packageName, e);
-                            }
-                        }
-                    }
-
-                    for (String group : fileGroupsToRemove) {
-                        sLogger.d(TAG + ": Removing file group: " + group);
-                        mFutures.add(mobileDataDownload.removeFileGroup(
-                                RemoveFileGroupRequest.newBuilder().setGroupName(group).build()));
-                    }
-
-                    return PropagatedFutures.transform(
-                            Futures.successfulAsList(mFutures),
-                            result -> {
-                                if (result.contains(null)) {
-                                    sLogger.d(TAG + ": Failed to add or remove a file group");
-                                } else {
-                                    sLogger.d(TAG + ": Successfully updated all file groups");
+                            List<ListenableFuture<Boolean>> mFutures = new ArrayList<>();
+                            for (String packageName :
+                                    AppManifestConfigHelper.getOdpPackages(
+                                            mContext, /* enrolledOnly= */ true)) {
+                                try {
+                                    String groupName =
+                                            createPackageFileGroupName(packageName, mContext);
+                                    fileGroupsToRemove.remove(groupName);
+                                    String ownerPackage = mContext.getPackageName();
+                                    String fileId = groupName;
+                                    int byteSize = 0;
+                                    String checksum = "";
+                                    ChecksumType checksumType = ChecksumType.NONE;
+                                    String downloadUrl = createDownloadUrl(packageName, mContext);
+                                    DeviceNetworkPolicy deviceNetworkPolicy =
+                                            DeviceNetworkPolicy.DOWNLOAD_ONLY_ON_WIFI;
+                                    DataFileGroup dataFileGroup =
+                                            createDataFileGroup(
+                                                    groupName,
+                                                    ownerPackage,
+                                                    new String[] {fileId},
+                                                    new int[] {byteSize},
+                                                    new String[] {checksum},
+                                                    new ChecksumType[] {checksumType},
+                                                    new String[] {downloadUrl},
+                                                    deviceNetworkPolicy);
+                                    mFutures.add(
+                                            mobileDataDownload.addFileGroup(
+                                                    AddFileGroupRequest.newBuilder()
+                                                            .setDataFileGroup(dataFileGroup)
+                                                            .build()));
+                                } catch (Exception e) {
+                                    sLogger.e(
+                                            TAG
+                                                    + ": Failed to create file group for "
+                                                    + packageName,
+                                            e);
                                 }
-                                return null;
-                            },
-                            OnDevicePersonalizationExecutors.getBackgroundExecutor()
-                    );
-                }, OnDevicePersonalizationExecutors.getBackgroundExecutor());
+                            }
+
+                            for (String group : fileGroupsToRemove) {
+                                sLogger.d(TAG + ": Removing file group: " + group);
+                                mFutures.add(
+                                        mobileDataDownload.removeFileGroup(
+                                                RemoveFileGroupRequest.newBuilder()
+                                                        .setGroupName(group)
+                                                        .build()));
+                            }
+
+                            return PropagatedFutures.transform(
+                                    Futures.successfulAsList(mFutures),
+                                    result -> {
+                                        if (result.contains(null)) {
+                                            sLogger.d(
+                                                    TAG + ": Failed to add or remove a file group");
+                                        } else {
+                                            sLogger.d(
+                                                    TAG + ": Successfully updated all file groups");
+                                        }
+                                        return null;
+                                    },
+                                    OnDevicePersonalizationExecutors.getBackgroundExecutor());
+                        },
+                        OnDevicePersonalizationExecutors.getBackgroundExecutor());
     }
 }
diff --git a/src/com/android/ondevicepersonalization/services/federatedcompute/FederatedComputeServiceImpl.java b/src/com/android/ondevicepersonalization/services/federatedcompute/FederatedComputeServiceImpl.java
index ef6657d..5a395ba 100644
--- a/src/com/android/ondevicepersonalization/services/federatedcompute/FederatedComputeServiceImpl.java
+++ b/src/com/android/ondevicepersonalization/services/federatedcompute/FederatedComputeServiceImpl.java
@@ -16,29 +16,27 @@
 
 package com.android.ondevicepersonalization.services.federatedcompute;
 
+import android.adservices.ondevicepersonalization.Constants;
 import android.adservices.ondevicepersonalization.aidl.IFederatedComputeCallback;
 import android.adservices.ondevicepersonalization.aidl.IFederatedComputeService;
 import android.annotation.NonNull;
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.PackageManager;
 import android.federatedcompute.FederatedComputeManager;
 import android.federatedcompute.common.ClientConstants;
 import android.federatedcompute.common.ScheduleFederatedComputeRequest;
 import android.federatedcompute.common.TrainingOptions;
 import android.os.OutcomeReceiver;
 import android.os.RemoteException;
-import android.os.SystemProperties;
-import android.provider.DeviceConfig;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.odp.module.common.PackageUtils;
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
 import com.android.ondevicepersonalization.services.data.events.EventState;
 import com.android.ondevicepersonalization.services.data.events.EventsDao;
 import com.android.ondevicepersonalization.services.data.user.UserPrivacyStatus;
 import com.android.ondevicepersonalization.services.manifest.AppManifestConfigHelper;
+import com.android.ondevicepersonalization.services.util.DebugUtils;
 
 import com.google.common.util.concurrent.ListeningExecutorService;
 
@@ -53,13 +51,8 @@
     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
     private static final String TAG = "FederatedComputeServiceImpl";
 
-    private static final String OVERRIDE_FC_SERVER_URL_PACKAGE =
-            "debug.ondevicepersonalization.override_fc_server_url_package";
-    private static final String OVERRIDE_FC_SERVER_URL =
-            "debug.ondevicepersonalization.override_fc_server_url";
-
     @NonNull private final Context mApplicationContext;
-    @NonNull private ComponentName mCallingService;
+    @NonNull private final ComponentName mCallingService;
     @NonNull private final Injector mInjector;
 
     @NonNull private final FederatedComputeManager mFederatedComputeManager;
@@ -91,44 +84,18 @@
         try {
             if (!UserPrivacyStatus.getInstance().isMeasurementEnabled()) {
                 sLogger.d(TAG + ": measurement control is revoked.");
-                sendError(callback);
+                sendError(callback, Constants.STATUS_PERSONALIZATION_DISABLED);
                 return;
             }
 
             String url =
                     AppManifestConfigHelper.getFcRemoteServerUrlFromOdpSettings(
                             mApplicationContext, mCallingService.getPackageName());
-
-            // Check for override manifest url property, if package is debuggable
-            if (PackageUtils.isPackageDebuggable(
-                    mApplicationContext, mCallingService.getPackageName())) {
-                if (SystemProperties.get(OVERRIDE_FC_SERVER_URL_PACKAGE, "")
-                        .equals(mCallingService.getPackageName())) {
-                    String overrideManifestUrl = SystemProperties.get(OVERRIDE_FC_SERVER_URL, "");
-                    if (!overrideManifestUrl.isEmpty()) {
-                        sLogger.d(
-                                TAG
-                                        + ": Overriding fc server URL for package "
-                                        + mCallingService.getPackageName()
-                                        + " to "
-                                        + overrideManifestUrl);
-                        url = overrideManifestUrl;
-                    }
-                    String deviceConfigOverrideUrl =
-                            DeviceConfig.getString(
-                                    /* namespace= */ "on_device_personalization",
-                                    /* name= */ OVERRIDE_FC_SERVER_URL,
-                                    /* defaultValue= */ "");
-                    if (!deviceConfigOverrideUrl.isEmpty()) {
-                        sLogger.d(
-                                TAG
-                                        + ": Overriding fc server URL for package "
-                                        + mCallingService.getPackageName()
-                                        + " to "
-                                        + deviceConfigOverrideUrl);
-                        url = deviceConfigOverrideUrl;
-                    }
-                }
+            String overrideUrl =
+                    DebugUtils.getFcServerOverrideUrl(
+                            mApplicationContext, mCallingService.getPackageName());
+            if (!overrideUrl.isEmpty()) {
+                url = overrideUrl;
             }
 
             if (url == null) {
@@ -136,7 +103,7 @@
                         TAG
                                 + ": Missing remote server URL for package: "
                                 + mCallingService.getPackageName());
-                sendError(callback);
+                sendError(callback, Constants.STATUS_FCP_MANIFEST_INVALID);
                 return;
             }
 
@@ -179,9 +146,11 @@
                             sendError(callback);
                         }
                     });
-        } catch (IOException | PackageManager.NameNotFoundException e) {
+        } catch (IOException | IllegalArgumentException e) {
+            // The AppManifestConfigHelper methods throw IllegalArgumentExceptions when
+            // parsings fails or the fc settings URL is missing.
             sLogger.e(TAG + ": Error while scheduling federatedCompute", e);
-            sendError(callback);
+            sendError(callback, Constants.STATUS_FCP_MANIFEST_INVALID);
         }
     }
 
@@ -217,7 +186,7 @@
                 });
     }
 
-    private void sendSuccess(@NonNull IFederatedComputeCallback callback) {
+    private static void sendSuccess(@NonNull IFederatedComputeCallback callback) {
         try {
             callback.onSuccess();
         } catch (RemoteException e) {
@@ -225,9 +194,13 @@
         }
     }
 
-    private void sendError(@NonNull IFederatedComputeCallback callback) {
+    private static void sendError(@NonNull IFederatedComputeCallback callback) {
+        sendError(callback, ClientConstants.STATUS_INTERNAL_ERROR);
+    }
+
+    private static void sendError(@NonNull IFederatedComputeCallback callback, int errorCode) {
         try {
-            callback.onFailure(ClientConstants.STATUS_INTERNAL_ERROR);
+            callback.onFailure(errorCode);
         } catch (RemoteException e) {
             sLogger.e(TAG + ": Callback error", e);
         }
diff --git a/src/com/android/ondevicepersonalization/services/federatedcompute/OdpExampleStoreService.java b/src/com/android/ondevicepersonalization/services/federatedcompute/OdpExampleStoreService.java
index c351ec6..d6c23b5 100644
--- a/src/com/android/ondevicepersonalization/services/federatedcompute/OdpExampleStoreService.java
+++ b/src/com/android/ondevicepersonalization/services/federatedcompute/OdpExampleStoreService.java
@@ -17,6 +17,7 @@
 package com.android.ondevicepersonalization.services.federatedcompute;
 
 import android.adservices.ondevicepersonalization.Constants;
+import android.adservices.ondevicepersonalization.IsolatedServiceException;
 import android.adservices.ondevicepersonalization.TrainingExampleRecord;
 import android.adservices.ondevicepersonalization.TrainingExamplesInputParcel;
 import android.adservices.ondevicepersonalization.TrainingExamplesOutputParcel;
@@ -32,10 +33,12 @@
 
 import com.android.odp.module.common.Clock;
 import com.android.odp.module.common.MonotonicClock;
+import com.android.odp.module.common.PackageUtils;
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 import com.android.ondevicepersonalization.internal.util.OdpParceledListSlice;
 import com.android.ondevicepersonalization.services.Flags;
 import com.android.ondevicepersonalization.services.FlagsFactory;
+import com.android.ondevicepersonalization.services.OdpServiceException;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
 import com.android.ondevicepersonalization.services.data.DataAccessPermission;
 import com.android.ondevicepersonalization.services.data.DataAccessServiceImpl;
@@ -48,6 +51,7 @@
 import com.android.ondevicepersonalization.services.process.PluginProcessRunner;
 import com.android.ondevicepersonalization.services.process.ProcessRunner;
 import com.android.ondevicepersonalization.services.process.SharedIsolatedProcessRunner;
+import com.android.ondevicepersonalization.services.util.AllowListUtils;
 import com.android.ondevicepersonalization.services.util.StatsUtils;
 
 import com.google.common.util.concurrent.FluentFuture;
@@ -58,6 +62,7 @@
 
 import java.util.Objects;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 /** Implementation of ExampleStoreService for OnDevicePersonalization */
 public final class OdpExampleStoreService extends ExampleStoreService {
@@ -106,6 +111,7 @@
     @Override
     public void startQuery(@NonNull Bundle params, @NonNull QueryCallback callback) {
         try {
+            long startTime = mInjector.getClock().currentTimeMillis();
             ContextData contextData =
                     ContextData.fromByteArray(
                             Objects.requireNonNull(
@@ -126,6 +132,13 @@
             if (!UserPrivacyStatus.getInstance().isMeasurementEnabled()) {
                 privacyStatusEligible = false;
                 sLogger.w(TAG + ": Measurement control is not given.");
+                StatsUtils.writeServiceRequestMetrics(
+                        Constants.API_NAME_SERVICE_ON_TRAINING_EXAMPLE,
+                        packageName,
+                        null,
+                        mInjector.getClock(),
+                        Constants.STATUS_PERSONALIZATION_DISABLED,
+                        startTime);
             }
 
             // Cancel job if on longer valid. This is written to the table during scheduling
@@ -133,6 +146,15 @@
             // during maintenance for uninstalled packages.
             ComponentName owner = ComponentName.createRelative(packageName, ownerClassName);
             EventState eventStatePopulation = eventDao.getEventState(populationName, owner);
+            if (eventStatePopulation == null) {
+                StatsUtils.writeServiceRequestMetrics(
+                        Constants.API_NAME_SERVICE_ON_TRAINING_EXAMPLE,
+                        packageName,
+                        null,
+                        mInjector.getClock(),
+                        Constants.STATUS_KEY_NOT_FOUND,
+                        startTime);
+            }
             if (!privacyStatusEligible || eventStatePopulation == null) {
                 sLogger.w("Job was either cancelled or package was uninstalled");
                 // Cancel job.
@@ -194,20 +216,13 @@
                             .loadIsolatedService(
                                     TASK_NAME,
                                     ComponentName.createRelative(packageName, className));
-            ListenableFuture<TrainingExamplesOutputParcel> resultFuture =
+            ListenableFuture<Bundle> resultFuture =
                     FluentFuture.from(loadFuture)
                             .transformAsync(
                                     result ->
                                             executeOnTrainingExamples(
                                                     result, input.build(), packageName),
                                     OnDevicePersonalizationExecutors.getBackgroundExecutor())
-                            .transform(
-                                    result -> {
-                                        return result.getParcelable(
-                                                Constants.EXTRA_RESULT,
-                                                TrainingExamplesOutputParcel.class);
-                                    },
-                                    OnDevicePersonalizationExecutors.getBackgroundExecutor())
                             .withTimeout(
                                     mInjector.getFlags().getIsolatedServiceDeadlineSeconds(),
                                     TimeUnit.SECONDS,
@@ -215,29 +230,72 @@
 
             Futures.addCallback(
                     resultFuture,
-                    new FutureCallback<TrainingExamplesOutputParcel>() {
+                    new FutureCallback<Bundle>() {
                         @Override
-                        public void onSuccess(
-                                TrainingExamplesOutputParcel trainingExamplesOutputParcel) {
-                            OdpParceledListSlice<TrainingExampleRecord> trainingExampleRecordList =
-                                    trainingExamplesOutputParcel.getTrainingExampleRecords();
-
-                            if (trainingExampleRecordList == null
-                                    || trainingExampleRecordList.getList().size()
-                                            < eligibilityMinExample) {
-                                callback.onStartQueryFailure(
-                                        ClientConstants.STATUS_NOT_ENOUGH_DATA);
-                            } else {
-                                callback.onStartQuerySuccess(
-                                        OdpExampleStoreIteratorFactory.getInstance()
-                                                .createIterator(
-                                                        trainingExampleRecordList.getList()));
+                        public void onSuccess(Bundle result) {
+                            int status = Constants.STATUS_SUCCESS;
+                            try {
+                                TrainingExamplesOutputParcel trainingExamplesOutputParcel =
+                                        result.getParcelable(
+                                                Constants.EXTRA_RESULT,
+                                                TrainingExamplesOutputParcel.class);
+                                if (trainingExamplesOutputParcel == null) {
+                                    status = Constants.STATUS_NAME_NOT_FOUND;
+                                    callback.onStartQueryFailure(
+                                            ClientConstants.STATUS_INTERNAL_ERROR);
+                                    return;
+                                }
+                                OdpParceledListSlice<TrainingExampleRecord>
+                                        trainingExampleRecordList =
+                                                trainingExamplesOutputParcel
+                                                        .getTrainingExampleRecords();
+                                if (trainingExampleRecordList == null
+                                        || trainingExampleRecordList.getList().isEmpty()
+                                        || trainingExampleRecordList.getList().size()
+                                                < eligibilityMinExample) {
+                                    status = Constants.STATUS_SUCCESS_EMPTY_RESULT;
+                                    callback.onStartQueryFailure(
+                                            ClientConstants.STATUS_NOT_ENOUGH_DATA);
+                                } else {
+                                    callback.onStartQuerySuccess(
+                                            OdpExampleStoreIteratorFactory.getInstance()
+                                                    .createIterator(
+                                                            trainingExampleRecordList.getList()));
+                                }
+                            } finally {
+                                StatsUtils.writeServiceRequestMetrics(
+                                        Constants.API_NAME_SERVICE_ON_TRAINING_EXAMPLE,
+                                        packageName,
+                                        result,
+                                        mInjector.getClock(),
+                                        status,
+                                        startTime);
                             }
                         }
 
                         @Override
                         public void onFailure(Throwable t) {
+                            int status = Constants.STATUS_INTERNAL_ERROR;
+                            if (t instanceof TimeoutException) {
+                                status = Constants.STATUS_TIMEOUT;
+                            } else if (t instanceof OdpServiceException exp) {
+                                if (exp.getCause() instanceof IsolatedServiceException
+                                        && isLogIsolatedServiceErrorCodeNonAggregatedAllowed(
+                                                packageName)) {
+                                    status = ((IsolatedServiceException) exp.getCause())
+                                            .getErrorCode();
+                                } else {
+                                    status = exp.getErrorCode();
+                                }
+                            }
                             sLogger.w(t, "%s : Request failed.", TAG);
+                            StatsUtils.writeServiceRequestMetrics(
+                                    Constants.API_NAME_SERVICE_ON_TRAINING_EXAMPLE,
+                                    packageName,
+                                    null,
+                                    mInjector.getClock(),
+                                    status,
+                                    startTime);
                             callback.onStartQueryFailure(ClientConstants.STATUS_INTERNAL_ERROR);
                         }
                     },
@@ -253,6 +311,9 @@
                                     OnDevicePersonalizationExecutors.getBackgroundExecutor());
         } catch (Exception e) {
             sLogger.w(e, "%s : Start query failed.", TAG);
+            StatsUtils.writeServiceRequestMetrics(
+                    Constants.API_NAME_SERVICE_ON_TRAINING_EXAMPLE,
+                    Constants.STATUS_INTERNAL_ERROR);
             callback.onStartQueryFailure(ClientConstants.STATUS_INTERNAL_ERROR);
         }
     }
@@ -276,39 +337,46 @@
                         /* eventDataPermission */ DataAccessPermission.READ_ONLY);
         serviceParams.putBinder(Constants.EXTRA_DATA_ACCESS_SERVICE_BINDER, binder);
         UserDataAccessor userDataAccessor = new UserDataAccessor();
-        UserData userData = userDataAccessor.getUserDataWithAppInstall();
+        UserData userData;
+        // By default, we don't provide platform data for federated learning flow.
+        if (isPlatformDataProvided(packageName)) {
+            userData = userDataAccessor.getUserDataWithAppInstall();
+        } else {
+            userData = userDataAccessor.getUserData();
+        }
         serviceParams.putParcelable(Constants.EXTRA_USER_DATA, userData);
-        ListenableFuture<Bundle> result =
-                mInjector
-                        .getProcessRunner()
-                        .runIsolatedService(
-                                isolatedServiceInfo, Constants.OP_TRAINING_EXAMPLE, serviceParams);
-        return FluentFuture.from(result)
-                .transform(
-                        val -> {
-                            StatsUtils.writeServiceRequestMetrics(
-                                    Constants.API_NAME_SERVICE_ON_TRAINING_EXAMPLE,
-                                    val, mInjector.getClock(),
-                                    Constants.STATUS_SUCCESS,
-                                    isolatedServiceInfo.getStartTimeMillis());
-                            return val;
-                        },
-                        OnDevicePersonalizationExecutors.getBackgroundExecutor())
-                .catchingAsync(
-                        Exception.class,
-                        e -> {
-                            StatsUtils.writeServiceRequestMetrics(
-                                    Constants.API_NAME_SERVICE_ON_TRAINING_EXAMPLE,
-                                    /* result= */ null, mInjector.getClock(),
-                                    Constants.STATUS_INTERNAL_ERROR,
-                                    isolatedServiceInfo.getStartTimeMillis());
-                            return Futures.immediateFailedFuture(e);
-                        },
-                        OnDevicePersonalizationExecutors.getBackgroundExecutor());
+        return mInjector
+                .getProcessRunner()
+                .runIsolatedService(
+                        isolatedServiceInfo, Constants.OP_TRAINING_EXAMPLE, serviceParams);
     }
 
     // used for tests to provide mock/real implementation of context.
     private Context getContext() {
         return this.getApplicationContext();
     }
+
+    private boolean isPlatformDataProvided(String packageName) {
+        try {
+            return AllowListUtils.isAllowListed(
+                    packageName,
+                    PackageUtils.getCertDigest(getContext(), packageName),
+                    mInjector.getFlags().getDefaultPlatformDataForExecuteAllowlist());
+        } catch (Exception e) {
+            sLogger.d(TAG + ": allow list error", e);
+            return false;
+        }
+    }
+
+    private boolean isLogIsolatedServiceErrorCodeNonAggregatedAllowed(String packageName) {
+        try {
+            return AllowListUtils.isAllowListed(
+                    packageName,
+                    null,
+                    mInjector.getFlags().getLogIsolatedServiceErrorCodeNonAggregatedAllowlist());
+        } catch (Exception e) {
+            sLogger.d(e, TAG + ": check isLogIsolatedServiceErrorCodeNonAggregatedAllowed error");
+            return false;
+        }
+    }
 }
diff --git a/src/com/android/ondevicepersonalization/services/inference/IsolatedModelServiceImpl.java b/src/com/android/ondevicepersonalization/services/inference/IsolatedModelServiceImpl.java
index b14f519..86d1203 100644
--- a/src/com/android/ondevicepersonalization/services/inference/IsolatedModelServiceImpl.java
+++ b/src/com/android/ondevicepersonalization/services/inference/IsolatedModelServiceImpl.java
@@ -21,6 +21,7 @@
 import android.adservices.ondevicepersonalization.InferenceOutput;
 import android.adservices.ondevicepersonalization.InferenceOutputParcel;
 import android.adservices.ondevicepersonalization.ModelId;
+import android.adservices.ondevicepersonalization.OnDevicePersonalizationException;
 import android.adservices.ondevicepersonalization.aidl.IDataAccessService;
 import android.adservices.ondevicepersonalization.aidl.IDataAccessServiceCallback;
 import android.adservices.ondevicepersonalization.aidl.IIsolatedModelService;
@@ -94,28 +95,37 @@
             IIsolatedModelServiceCallback callback) {
         try {
             Trace.beginSection("IsolatedModelService#RunInference");
+            // We already validate requests in ModelManager and double check in case.
             Object[] inputs = convertToObjArray(inputParcel.getInputData().getList());
-            if (inputs.length == 0) {
-                sendError(callback);
+            if (inputs == null || inputs.length == 0) {
+                sLogger.e("Input data can not be empty for inference.");
+                sendError(callback, OnDevicePersonalizationException.ERROR_INFERENCE_FAILED);
+            }
+            Map<Integer, Object> outputs = outputParcel.getData();
+            if (outputs.isEmpty()) {
+                sLogger.e("Output data can not be empty for inference.");
+                sendError(callback, OnDevicePersonalizationException.ERROR_INFERENCE_FAILED);
             }
 
             ModelId modelId = inputParcel.getModelId();
             ParcelFileDescriptor modelFd = fetchModel(binder, modelId);
+            if (modelFd == null) {
+                sLogger.e(TAG + ": Failed to fetch model %s.", modelId.getKey());
+                sendError(
+                        callback, OnDevicePersonalizationException.ERROR_INFERENCE_MODEL_NOT_FOUND);
+                return;
+            }
             ByteBuffer byteBuffer = IoUtils.getByteBufferFromFd(modelFd);
             if (byteBuffer == null) {
                 closeFd(modelFd);
-                sendError(callback);
+                sendError(
+                        callback, OnDevicePersonalizationException.ERROR_INFERENCE_MODEL_NOT_FOUND);
             }
             InterpreterApi interpreter =
                     InterpreterApi.create(
                             byteBuffer,
                             new InterpreterApi.Options()
                                     .setNumThreads(inputParcel.getCpuNumThread()));
-            Map<Integer, Object> outputs = outputParcel.getData();
-            if (outputs.isEmpty() || inputs.length == 0) {
-                closeFd(modelFd);
-                sendError(callback);
-            }
 
             // TODO(b/323469981): handle batch size better. Currently TFLite will throws error if
             // batchSize doesn't match input data size.
@@ -138,7 +148,7 @@
         } catch (Exception e) {
             // Catch all exceptions including TFLite errors.
             sLogger.e(e, TAG + ": Failed to run inference job.");
-            sendError(callback);
+            sendError(callback, OnDevicePersonalizationException.ERROR_INFERENCE_FAILED);
         }
     }
 
@@ -149,8 +159,9 @@
             try {
                 ObjectInputStream ois = new ObjectInputStream(bais);
                 output[i] = ois.readObject();
-            } catch (IOException | ClassNotFoundException e) {
-                throw new RuntimeException(e);
+            } catch (Exception e) {
+                sLogger.e(e, "Failed to parse inference input");
+                return null;
             }
         }
         return output;
@@ -191,23 +202,22 @@
             Bundle result = asyncResult.take();
             ParcelFileDescriptor modelFd =
                     result.getParcelable(Constants.EXTRA_RESULT, ParcelFileDescriptor.class);
-            Objects.requireNonNull(modelFd);
             return modelFd;
-        } catch (InterruptedException | RemoteException e) {
-            sLogger.e(TAG + ": Failed to fetch model from DataAccessService", e);
-            throw new IllegalStateException(e);
+        } catch (Exception e) {
+            sLogger.e(e, TAG + ": Failed to fetch model from DataAccessService");
+            return null;
         }
     }
 
-    private void sendError(@NonNull IIsolatedModelServiceCallback callback) {
+    private static void sendError(@NonNull IIsolatedModelServiceCallback callback, int errorCode) {
         try {
-            callback.onError(Constants.STATUS_INTERNAL_ERROR);
+            callback.onError(errorCode);
         } catch (RemoteException e) {
             sLogger.e(TAG + ": Callback error", e);
         }
     }
 
-    private void sendResult(
+    private static void sendResult(
             @NonNull Bundle result, @NonNull IIsolatedModelServiceCallback callback) {
         try {
             callback.onSuccess(result);
diff --git a/src/com/android/ondevicepersonalization/services/maintenance/OnDevicePersonalizationMaintenanceJob.java b/src/com/android/ondevicepersonalization/services/maintenance/OnDevicePersonalizationMaintenanceJob.java
index a489ec1..5202cb2 100644
--- a/src/com/android/ondevicepersonalization/services/maintenance/OnDevicePersonalizationMaintenanceJob.java
+++ b/src/com/android/ondevicepersonalization/services/maintenance/OnDevicePersonalizationMaintenanceJob.java
@@ -16,8 +16,6 @@
 
 package com.android.ondevicepersonalization.services.maintenance;
 
-import static android.content.pm.PackageManager.GET_META_DATA;
-
 import static com.android.adservices.shared.proto.JobPolicy.BatteryType.BATTERY_TYPE_REQUIRE_NOT_LOW;
 import static com.android.adservices.shared.spe.JobServiceConstants.JOB_ENABLED_STATUS_DISABLED_FOR_KILL_SWITCH_ON;
 import static com.android.adservices.shared.spe.JobServiceConstants.JOB_ENABLED_STATUS_ENABLED;
@@ -25,8 +23,6 @@
 
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
 
 import com.android.adservices.shared.proto.JobPolicy;
 import com.android.adservices.shared.spe.framework.ExecutionResult;
@@ -39,9 +35,9 @@
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
+import com.android.ondevicepersonalization.services.data.errors.AggregatedErrorCodesLogger;
 import com.android.ondevicepersonalization.services.data.events.EventsDao;
 import com.android.ondevicepersonalization.services.data.vendor.OnDevicePersonalizationVendorDataDao;
-import com.android.ondevicepersonalization.services.enrollment.PartnerEnrollmentChecker;
 import com.android.ondevicepersonalization.services.manifest.AppManifestConfigHelper;
 import com.android.ondevicepersonalization.services.sharedlibrary.spe.OdpJobScheduler;
 import com.android.ondevicepersonalization.services.sharedlibrary.spe.OdpJobServiceFactory;
@@ -49,7 +45,7 @@
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 
-import java.util.ArrayList;
+import java.util.List;
 
 /** The background job to handle the OnDevicePersonalization maintenance. */
 public final class OnDevicePersonalizationMaintenanceJob implements JobWorker {
@@ -138,29 +134,11 @@
 
     @VisibleForTesting
     void cleanupVendorData(Context context) throws Exception {
-        ArrayList<ComponentName> services = new ArrayList<>();
-
-        for (PackageInfo packageInfo :
-                context.getPackageManager()
-                        .getInstalledPackages(PackageManager.PackageInfoFlags.of(GET_META_DATA))) {
-            String packageName = packageInfo.packageName;
-
-            if (AppManifestConfigHelper.manifestContainsOdpSettings(context, packageName)) {
-                if (!PartnerEnrollmentChecker.isIsolatedServiceEnrolled(packageName)) {
-                    sLogger.d(TAG + ": service %s has ODP manifest, but not enrolled", packageName);
-                    continue;
-                }
-
-                sLogger.d(TAG + ": service %s has ODP manifest and is enrolled", packageName);
-
-                String serviceClass =
-                        AppManifestConfigHelper.getServiceNameFromOdpSettings(context, packageName);
-                ComponentName service = ComponentName.createRelative(packageName, serviceClass);
-                services.add(service);
-            }
-        }
+        List<ComponentName> services =
+                AppManifestConfigHelper.getOdpServices(context, /* enrolledOnly= */ true);
 
         OnDevicePersonalizationVendorDataDao.deleteVendorTables(context, services);
         deleteEventsAndQueries(context);
+        AggregatedErrorCodesLogger.cleanupErrorData(context);
     }
 }
diff --git a/src/com/android/ondevicepersonalization/services/maintenance/OnDevicePersonalizationMaintenanceJobService.java b/src/com/android/ondevicepersonalization/services/maintenance/OnDevicePersonalizationMaintenanceJobService.java
index 01da274..fa32e29 100644
--- a/src/com/android/ondevicepersonalization/services/maintenance/OnDevicePersonalizationMaintenanceJobService.java
+++ b/src/com/android/ondevicepersonalization/services/maintenance/OnDevicePersonalizationMaintenanceJobService.java
@@ -17,7 +17,6 @@
 package com.android.ondevicepersonalization.services.maintenance;
 
 import static android.app.job.JobScheduler.RESULT_SUCCESS;
-import static android.content.pm.PackageManager.GET_META_DATA;
 
 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON;
 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_FAILED;
@@ -31,18 +30,15 @@
 import android.app.job.JobService;
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
 
 import com.android.adservices.shared.spe.JobServiceConstants.JobSchedulingResultCode;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.odp.module.common.PackageUtils;
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
+import com.android.ondevicepersonalization.services.data.errors.AggregatedErrorCodesLogger;
 import com.android.ondevicepersonalization.services.data.events.EventsDao;
 import com.android.ondevicepersonalization.services.data.vendor.OnDevicePersonalizationVendorDataDao;
-import com.android.ondevicepersonalization.services.enrollment.PartnerEnrollmentChecker;
 import com.android.ondevicepersonalization.services.manifest.AppManifestConfigHelper;
 import com.android.ondevicepersonalization.services.statsd.joblogging.OdpJobServiceLogger;
 
@@ -50,7 +46,7 @@
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 
-import java.util.ArrayList;
+import java.util.List;
 
 /** JobService to handle the OnDevicePersonalization maintenance */
 public class OnDevicePersonalizationMaintenanceJobService extends JobService {
@@ -109,29 +105,12 @@
 
     @VisibleForTesting
     static void cleanupVendorData(Context context) throws Exception {
-        ArrayList<ComponentName> services = new ArrayList<>();
-
-        for (PackageInfo packageInfo : context.getPackageManager().getInstalledPackages(
-                PackageManager.PackageInfoFlags.of(GET_META_DATA))) {
-            String packageName = packageInfo.packageName;
-            if (AppManifestConfigHelper.manifestContainsOdpSettings(
-                    context, packageName)) {
-                if (!PartnerEnrollmentChecker.isIsolatedServiceEnrolled(packageName)) {
-                    sLogger.d(TAG + ": service %s has ODP manifest, but not enrolled",
-                            packageName);
-                    continue;
-                }
-                sLogger.d(TAG + ": service %s has ODP manifest and is enrolled", packageName);
-                String certDigest = PackageUtils.getCertDigest(context, packageName);
-                String serviceClass = AppManifestConfigHelper.getServiceNameFromOdpSettings(
-                        context, packageName);
-                ComponentName service = ComponentName.createRelative(packageName, serviceClass);
-                services.add(service);
-            }
-        }
+        List<ComponentName> services =
+                AppManifestConfigHelper.getOdpServices(context, /* enrolledOnly= */ true);
 
         OnDevicePersonalizationVendorDataDao.deleteVendorTables(context, services);
         deleteEventsAndQueries(context);
+        AggregatedErrorCodesLogger.cleanupErrorData(context);
     }
 
     @Override
@@ -162,15 +141,12 @@
 
         mFuture =
                 Futures.submit(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                sLogger.d(TAG + ": Running maintenance job");
-                                try {
-                                    cleanupVendorData(context);
-                                } catch (Exception e) {
-                                    sLogger.e(TAG + ": Failed to cleanup vendorData", e);
-                                }
+                        () -> {
+                            sLogger.d(TAG + ": Running maintenance job");
+                            try {
+                                cleanupVendorData(context);
+                            } catch (Exception e) {
+                                sLogger.e(TAG + ": Failed to cleanup vendorData", e);
                             }
                         },
                         OnDevicePersonalizationExecutors.getBackgroundExecutor());
@@ -182,8 +158,7 @@
                     public void onSuccess(Void result) {
                         sLogger.d(TAG + ": Maintenance job completed.");
                         boolean wantsReschedule = false;
-                        OdpJobServiceLogger.getInstance(
-                                        OnDevicePersonalizationMaintenanceJobService.this)
+                        OdpJobServiceLogger.getInstance(context)
                                 .recordJobFinished(
                                         MAINTENANCE_TASK_JOB_ID,
                                         /* isSuccessful= */ true,
@@ -197,8 +172,7 @@
                     public void onFailure(Throwable t) {
                         sLogger.e(TAG + ": Failed to handle JobService: " + params.getJobId(), t);
                         boolean wantsReschedule = false;
-                        OdpJobServiceLogger.getInstance(
-                                        OnDevicePersonalizationMaintenanceJobService.this)
+                        OdpJobServiceLogger.getInstance(context)
                                 .recordJobFinished(
                                         MAINTENANCE_TASK_JOB_ID,
                                         /* isSuccessful= */ false,
diff --git a/src/com/android/ondevicepersonalization/services/manifest/AppManifestConfig.java b/src/com/android/ondevicepersonalization/services/manifest/AppManifestConfig.java
index 037e9a4..c5ac5bc 100644
--- a/src/com/android/ondevicepersonalization/services/manifest/AppManifestConfig.java
+++ b/src/com/android/ondevicepersonalization/services/manifest/AppManifestConfig.java
@@ -24,7 +24,7 @@
     private final String mServiceName;
     private final String mFcRemoteServerUrl;
 
-    public AppManifestConfig(String downloadUrl, String serviceName, String fcRemoteServerUrl) {
+    AppManifestConfig(String downloadUrl, String serviceName, String fcRemoteServerUrl) {
         mDownloadUrl = downloadUrl;
         mServiceName = serviceName;
         mFcRemoteServerUrl = fcRemoteServerUrl;
diff --git a/src/com/android/ondevicepersonalization/services/manifest/AppManifestConfigHelper.java b/src/com/android/ondevicepersonalization/services/manifest/AppManifestConfigHelper.java
index b2447fe..066a871 100644
--- a/src/com/android/ondevicepersonalization/services/manifest/AppManifestConfigHelper.java
+++ b/src/com/android/ondevicepersonalization/services/manifest/AppManifestConfigHelper.java
@@ -16,17 +16,28 @@
 
 package com.android.ondevicepersonalization.services.manifest;
 
+import static android.content.pm.PackageManager.GET_META_DATA;
+
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.content.res.XmlResourceParser;
 
+import com.android.ondevicepersonalization.internal.util.LoggerFactory;
+import com.android.ondevicepersonalization.services.enrollment.PartnerEnrollmentChecker;
+
+import com.google.common.collect.ImmutableList;
+
 /**
  * Helper class for parsing and checking app manifest configs
  */
 public final class AppManifestConfigHelper {
     private static final String ON_DEVICE_PERSONALIZATION_CONFIG_PROPERTY =
             "android.ondevicepersonalization.ON_DEVICE_PERSONALIZATION_CONFIG";
+    private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
+    private static final String TAG = AppManifestConfigHelper.class.getSimpleName();
 
     private AppManifestConfigHelper() {
     }
@@ -34,12 +45,11 @@
     /**
      * Determines if the given package's manifest contains ODP settings
      *
-     * @param context     the context of the API call.
+     * @param context the context of the API call.
      * @param packageName the packageName of the package whose manifest config will be read
      * @return true if the ODP setting exists, false otherwise
      */
-    public static Boolean manifestContainsOdpSettings(Context context,
-            String packageName) {
+    public static boolean manifestContainsOdpSettings(Context context, String packageName) {
         PackageManager pm = context.getPackageManager();
         try {
             pm.getProperty(ON_DEVICE_PERSONALIZATION_CONFIG_PROPERTY, packageName);
@@ -49,9 +59,14 @@
         return true;
     }
 
+    /**
+     * Returns the ODP manifest config for a package.
+     *
+     * <p>Throws a {@link RuntimeException} if the package, its ODP settings are not found or cannot
+     * be parsed.
+     */
     /** Returns the ODP manifest config for a package. */
-    public static AppManifestConfig getAppManifestConfig(Context context,
-            String packageName) {
+    public static AppManifestConfig getAppManifestConfig(Context context, String packageName) {
         if (!manifestContainsOdpSettings(context, packageName)) {
             // TODO(b/241941021) Determine correct exception to throw
             throw new IllegalArgumentException(
@@ -103,4 +118,67 @@
             String packageName) {
         return getAppManifestConfig(context, packageName).getFcRemoteServerUrl();
     }
+
+    /**
+     * Get the list of packages enrolled for ODP, by checking manifest for ODP settings.
+     *
+     * @param context The context of the calling process.
+     * @param enrolledOnly Whether to only include packages that pass the enrollment check.
+     * @return The list of packages that contain ODP manifest settings (and potentially enrolled)
+     */
+    public static ImmutableList<String> getOdpPackages(Context context, boolean enrolledOnly) {
+        ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
+        for (PackageInfo packageInfo :
+                context.getPackageManager()
+                        .getInstalledPackages(PackageManager.PackageInfoFlags.of(GET_META_DATA))) {
+            String packageName = packageInfo.packageName;
+
+            if (manifestContainsOdpSettings(context, packageName)) {
+                if (enrolledOnly
+                        && !PartnerEnrollmentChecker.isIsolatedServiceEnrolled(packageName)) {
+                    sLogger.d(TAG + ": package %s has ODP manifest, but not enrolled", packageName);
+                    continue;
+                }
+
+                String enrolledString = enrolledOnly ? "and is enrolled" : "";
+                sLogger.d(TAG + ": package %s has ODP manifest " + enrolledString, packageName);
+                builder.add(packageName);
+            }
+        }
+        return builder.build();
+    }
+
+    /**
+     * Get the list of services enrolled for ODP.
+     *
+     * @param context The context of the calling process.
+     * @param enrolledOnly Whether to only include services that pass the enrollment check.
+     * @return The list of matching Services with ODP manifest settings (and are potentially
+     *     enrolled).
+     */
+    public static ImmutableList<ComponentName> getOdpServices(
+            Context context, boolean enrolledOnly) {
+        ImmutableList.Builder<ComponentName> builder = new ImmutableList.Builder<>();
+        for (PackageInfo packageInfo :
+                context.getPackageManager()
+                        .getInstalledPackages(PackageManager.PackageInfoFlags.of(GET_META_DATA))) {
+            String packageName = packageInfo.packageName;
+
+            if (manifestContainsOdpSettings(context, packageName)) {
+                if (enrolledOnly
+                        && !PartnerEnrollmentChecker.isIsolatedServiceEnrolled(packageName)) {
+                    sLogger.d(TAG + ": service %s has ODP manifest, but not enrolled", packageName);
+                    continue;
+                }
+
+                String enrolledString = enrolledOnly ? "and is enrolled" : "";
+                sLogger.d(TAG + ": service %s has ODP manifest " + enrolledString, packageName);
+
+                String serviceClass = getServiceNameFromOdpSettings(context, packageName);
+                ComponentName service = ComponentName.createRelative(packageName, serviceClass);
+                builder.add(service);
+            }
+        }
+        return builder.build();
+    }
 }
diff --git a/src/com/android/ondevicepersonalization/services/manifest/AppManifestConfigParser.java b/src/com/android/ondevicepersonalization/services/manifest/AppManifestConfigParser.java
index 05c6be6..21b6ad2 100644
--- a/src/com/android/ondevicepersonalization/services/manifest/AppManifestConfigParser.java
+++ b/src/com/android/ondevicepersonalization/services/manifest/AppManifestConfigParser.java
@@ -26,7 +26,7 @@
 import java.io.IOException;
 
 /** Parser and validator for OnDevicePersonalization app manifest configs. */
-public class AppManifestConfigParser {
+class AppManifestConfigParser {
     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
     private static final String TAG = "AppManifestConfigParser";
     private static final String TAG_ON_DEVICE_PERSONALIZATION_CONFIG = "on-device-personalization";
@@ -45,8 +45,8 @@
      *
      * @param parser the XmlParser representing the OnDevicePersonalization app manifest config
      */
-    public static AppManifestConfig getConfig(XmlPullParser parser) throws IOException,
-            XmlPullParserException {
+    static AppManifestConfig getConfig(XmlPullParser parser)
+            throws IOException, XmlPullParserException {
         String downloadUrl = null;
         String serviceName = null;
         String fcServerUrl = null;
diff --git a/src/com/android/ondevicepersonalization/services/process/IsolatedServiceInfo.java b/src/com/android/ondevicepersonalization/services/process/IsolatedServiceInfo.java
index 6cd7000..3a942ca 100644
--- a/src/com/android/ondevicepersonalization/services/process/IsolatedServiceInfo.java
+++ b/src/com/android/ondevicepersonalization/services/process/IsolatedServiceInfo.java
@@ -16,7 +16,6 @@
 
 package com.android.ondevicepersonalization.services.process;
 
-import static android.adservices.ondevicepersonalization.OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_FAILED;
 
 import android.adservices.ondevicepersonalization.aidl.IIsolatedService;
 import android.annotation.NonNull;
@@ -25,7 +24,6 @@
 
 import com.android.federatedcompute.internal.util.AbstractServiceBinder;
 import com.android.ondevicepersonalization.libraries.plugin.PluginController;
-import com.android.ondevicepersonalization.services.OdpServiceException;
 
 import java.util.Objects;
 
@@ -39,19 +37,26 @@
     IsolatedServiceInfo(
             long startTimeMillis,
             @NonNull ComponentName componentName,
+            @NonNull PluginController pluginController) {
+        this(startTimeMillis, componentName, pluginController, /* isolatedServiceBinder= */ null);
+    }
+
+    IsolatedServiceInfo(
+            long startTimeMillis,
+            @NonNull ComponentName componentName,
+            @NonNull AbstractServiceBinder<IIsolatedService> isolatedServiceBinder) {
+        this(startTimeMillis, componentName, /* pluginController= */ null, isolatedServiceBinder);
+    }
+
+    private IsolatedServiceInfo(
+            long startTimeMillis,
+            @NonNull ComponentName componentName,
             @Nullable PluginController pluginController,
-            @Nullable AbstractServiceBinder<IIsolatedService> isolatedServiceBinder)
-            throws OdpServiceException {
+            @Nullable AbstractServiceBinder<IIsolatedService> isolatedServiceBinder) {
         mStartTimeMillis = startTimeMillis;
         mComponentName = Objects.requireNonNull(componentName);
         mPluginController = pluginController;
         mIsolatedServiceBinder = isolatedServiceBinder;
-
-        // TO-DO (323882182): Granular isolated servce failures.
-        if ((mPluginController != null && mIsolatedServiceBinder != null)
-                || (mPluginController == null && mIsolatedServiceBinder == null)) {
-            throw new OdpServiceException(ERROR_ISOLATED_SERVICE_FAILED);
-        }
     }
 
     PluginController getPluginController() {
diff --git a/src/com/android/ondevicepersonalization/services/process/OnDevicePersonalizationPlugin.java b/src/com/android/ondevicepersonalization/services/process/OnDevicePersonalizationPlugin.java
index 1c260b5..edddf1a 100644
--- a/src/com/android/ondevicepersonalization/services/process/OnDevicePersonalizationPlugin.java
+++ b/src/com/android/ondevicepersonalization/services/process/OnDevicePersonalizationPlugin.java
@@ -96,8 +96,10 @@
                             }
                         }
                         @Override public void onError(
-                                int errorCode, int isolatedServiceErrorCode) {
+                                int errorCode, int isolatedServiceErrorCode,
+                                byte[] serializedExceptionInfo) {
                             try {
+                                // TODO(b/326455045): Support detailed error return in plugin.
                                 mPluginCallback.onFailure(FailureType.ERROR_EXECUTING_PLUGIN);
                             } catch (RemoteException e) {
                                 sLogger.e(TAG + ": Callback error.", e);
diff --git a/src/com/android/ondevicepersonalization/services/process/PluginProcessRunner.java b/src/com/android/ondevicepersonalization/services/process/PluginProcessRunner.java
index 98981e0..ab5c8c6 100644
--- a/src/com/android/ondevicepersonalization/services/process/PluginProcessRunner.java
+++ b/src/com/android/ondevicepersonalization/services/process/PluginProcessRunner.java
@@ -23,7 +23,6 @@
 import android.content.Context;
 import android.os.Bundle;
 
-
 import androidx.concurrent.futures.CallbackToFutureAdapter;
 
 import com.android.odp.module.common.Clock;
@@ -153,33 +152,36 @@
             @NonNull ComponentName componentName,
             @NonNull PluginController pluginController) {
         return CallbackToFutureAdapter.getFuture(
-            completer -> {
-                try {
-                    sLogger.d(TAG + ": loadPlugin");
-                    pluginController.load(new PluginCallback() {
-                        @Override public void onSuccess(Bundle bundle) {
-                            try {
-                                completer.set(
-                                        new IsolatedServiceInfo(
-                                            startTimeMillis, componentName,
-                                            pluginController, /* isolatedServiceBinder= */ null));
-                            } catch (OdpServiceException e) {
-                                completer.setException(e);
-                            }
+                completer -> {
+                    try {
+                        sLogger.d(TAG + ": loadPlugin");
+                        pluginController.load(
+                                new PluginCallback() {
+                                    @Override
+                                    public void onSuccess(Bundle bundle) {
 
-                        }
-                        @Override public void onFailure(FailureType failure) {
-                            completer.setException(new OdpServiceException(
-                                    Constants.STATUS_INTERNAL_ERROR,
-                                    String.format("loadPlugin failed. %s", failure.toString())));
-                        }
-                    });
-                } catch (Exception e) {
-                    completer.setException(e);
-                }
-                return "loadPlugin";
-            }
-        );
+                                        completer.set(
+                                                new IsolatedServiceInfo(
+                                                        startTimeMillis,
+                                                        componentName,
+                                                        pluginController));
+                                    }
+
+                                    @Override
+                                    public void onFailure(FailureType failure) {
+                                        completer.setException(
+                                                new OdpServiceException(
+                                                        Constants.STATUS_INTERNAL_ERROR,
+                                                        String.format(
+                                                                "loadPlugin failed. %s",
+                                                                failure.toString())));
+                                    }
+                                });
+                    } catch (Exception e) {
+                        completer.setException(e);
+                    }
+                    return "loadPlugin";
+                });
     }
 
     @NonNull static ListenableFuture<Bundle> executePlugin(
diff --git a/src/com/android/ondevicepersonalization/services/process/SharedIsolatedProcessRunner.java b/src/com/android/ondevicepersonalization/services/process/SharedIsolatedProcessRunner.java
index c2ef5ac..81fe61e 100644
--- a/src/com/android/ondevicepersonalization/services/process/SharedIsolatedProcessRunner.java
+++ b/src/com/android/ondevicepersonalization/services/process/SharedIsolatedProcessRunner.java
@@ -28,6 +28,7 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
+import android.os.Binder;
 import android.os.Bundle;
 
 import androidx.concurrent.futures.CallbackToFutureAdapter;
@@ -37,19 +38,23 @@
 import com.android.odp.module.common.Clock;
 import com.android.odp.module.common.MonotonicClock;
 import com.android.odp.module.common.PackageUtils;
+import com.android.ondevicepersonalization.internal.util.ExceptionInfo;
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
-import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.OdpServiceException;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationApplication;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
+import com.android.ondevicepersonalization.services.StableFlags;
+import com.android.ondevicepersonalization.services.data.errors.AggregatedErrorCodesLogger;
 import com.android.ondevicepersonalization.services.util.AllowListUtils;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.util.concurrent.FluentFuture;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 
 import java.util.Objects;
+import java.util.concurrent.TimeoutException;
 
 /** Utilities for running remote isolated services in a shared isolated process (SIP). Note that
  *  this runner is only selected when the shared_isolated_process_feature_enabled flag is enabled.
@@ -69,6 +74,7 @@
     private final Context mApplicationContext;
     private final Injector mInjector;
 
+    @VisibleForTesting
     static class Injector {
         Clock getClock() {
             return MonotonicClock.getInstance();
@@ -78,9 +84,9 @@
             return OnDevicePersonalizationExecutors.getBackgroundExecutor();
         }
     }
-    SharedIsolatedProcessRunner(
-            @NonNull Context applicationContext,
-            @NonNull Injector injector) {
+
+    @VisibleForTesting
+    SharedIsolatedProcessRunner(@NonNull Context applicationContext, @NonNull Injector injector) {
         mApplicationContext = Objects.requireNonNull(applicationContext);
         mInjector = Objects.requireNonNull(injector);
     }
@@ -110,20 +116,34 @@
             return FluentFuture.from(isolatedServiceFuture)
                     .transformAsync(
                             (isolatedService) -> {
-                                try {
-                                    return Futures.immediateFuture(new IsolatedServiceInfo(
-                                            mInjector.getClock().elapsedRealtime(), componentName,
-                                            /* pluginController= */ null, isolatedService));
-                                } catch (Exception e) {
-                                    return Futures.immediateFailedFuture(e);
-                                }
-                            }, mInjector.getExecutor())
+                                return Futures.immediateFuture(
+                                        new IsolatedServiceInfo(
+                                                mInjector.getClock().elapsedRealtime(),
+                                                componentName,
+                                                isolatedService));
+                            },
+                            mInjector.getExecutor())
                     .catchingAsync(
                             Exception.class,
-                            Futures::immediateFailedFuture,
+                            e -> {
+                                sLogger.d(
+                                        TAG
+                                                + ": loading of isolated service failed for "
+                                                + componentName,
+                                        e);
+                                // Return OdpServiceException if the exception thrown was not
+                                // already an OdpServiceException.
+                                if (e instanceof OdpServiceException) {
+                                    return Futures.immediateFailedFuture(e);
+                                }
+                                return Futures.immediateFailedFuture(
+                                        new OdpServiceException(
+                                            Constants.STATUS_ISOLATED_SERVICE_LOADING_FAILED, e));
+                            },
                             mInjector.getExecutor());
         } catch (Exception e) {
-            return Futures.immediateFailedFuture(e);
+            return Futures.immediateFailedFuture(
+                    new OdpServiceException(Constants.STATUS_ISOLATED_SERVICE_LOADING_FAILED, e));
         }
     }
 
@@ -133,36 +153,76 @@
     public ListenableFuture<Bundle> runIsolatedService(
             @NonNull IsolatedServiceInfo isolatedProcessInfo, int operationCode,
             @NonNull Bundle serviceParams) {
-        return CallbackToFutureAdapter.getFuture(
-                completer -> {
-                    isolatedProcessInfo.getIsolatedServiceBinder()
-                            .getService(Runnable::run)
-                            .onRequest(
-                                    operationCode, serviceParams,
+        IIsolatedService service;
+        try {
+            service = isolatedProcessInfo.getIsolatedServiceBinder().getService(Runnable::run);
+        } catch (Exception e) {
+            // Failure in loading/connecting to the IsolatedService vs actual issue
+            // in running the IsolatedService code via the onRequest call below.
+            sLogger.d(TAG + ": unable to get the IsolatedService binder.", e);
+            return Futures.immediateFailedFuture(
+                    new OdpServiceException(Constants.STATUS_ISOLATED_SERVICE_LOADING_FAILED));
+        }
+
+        ListenableFuture<Bundle> callbackFuture =
+                CallbackToFutureAdapter.getFuture(
+                        completer -> {
+                            service.onRequest(
+                                    operationCode,
+                                    serviceParams,
                                     new IIsolatedServiceCallback.Stub() {
-                                        @Override public void onSuccess(Bundle result) {
+                                        @Override
+                                        public void onSuccess(Bundle result) {
                                             completer.set(result);
                                         }
 
-                                        // TO-DO (323882182): Granular isolated servce failures.
-                                        @Override public void onError(
-                                                int errorCode, int isolatedServiceErrorCode) {
-                                            if (isolatedServiceErrorCode > 0
-                                                        && isolatedServiceErrorCode < 128) {
-                                                completer.setException(
-                                                        new OdpServiceException(
-                                                                Constants.STATUS_SERVICE_FAILED,
-                                                                new IsolatedServiceException(
-                                                                    isolatedServiceErrorCode)));
-                                            } else {
-                                                completer.setException(
-                                                        new OdpServiceException(
-                                                                Constants.STATUS_SERVICE_FAILED));
+                                        @Override
+                                        public void onError(
+                                                int errorCode,
+                                                int isolatedServiceErrorCode,
+                                                byte[] serializedExceptionInfo) {
+                                            Exception cause =
+                                                    ExceptionInfo.fromByteArray(
+                                                            serializedExceptionInfo);
+                                            if (isolatedServiceErrorCode > 0) {
+                                                final long token = Binder.clearCallingIdentity();
+                                                try {
+                                                    ListenableFuture<?> unused =
+                                                        AggregatedErrorCodesLogger
+                                                            .logIsolatedServiceErrorCode(
+                                                                isolatedServiceErrorCode,
+                                                                isolatedProcessInfo
+                                                                    .getComponentName(),
+                                                                mApplicationContext);
+                                                } finally {
+                                                    Binder.restoreCallingIdentity(token);
+                                                }
+                                                cause =
+                                                        new IsolatedServiceException(
+                                                                isolatedServiceErrorCode, cause);
                                             }
+                                            completer.setException(
+                                                    new OdpServiceException(
+                                                            Constants.STATUS_SERVICE_FAILED,
+                                                            cause));
                                         }
                                     });
-                    return null;
-                });
+                            // used for debugging purpose only.
+                            return "IsolatedService.onRequest";
+                        });
+        return FluentFuture.from(callbackFuture)
+                .catchingAsync(
+                        Throwable.class, // Catch FutureGarbageCollectedException
+                        e -> {
+                            return (e instanceof IsolatedServiceException
+                                            || e instanceof OdpServiceException)
+                                    ? Futures.immediateFailedFuture(e)
+                                    : Futures.immediateFailedFuture(
+                                            new TimeoutException(
+                                                    "Callback to future adapter was garbage"
+                                                            + " collected."));
+                        },
+                        mInjector.getExecutor());
     }
 
     /** Unbinds from the remote isolated service. */
@@ -194,9 +254,10 @@
                 instanceName, bindFlag, IIsolatedService.Stub::asInterface);
     }
 
+    @VisibleForTesting
     String getSipInstanceName(String packageName) {
         String partnerAppsList =
-                (String) FlagsFactory.getFlags().getStableFlag(KEY_TRUSTED_PARTNER_APPS_LIST);
+                (String) StableFlags.get(KEY_TRUSTED_PARTNER_APPS_LIST);
         String packageCertificate = null;
         try {
             packageCertificate = PackageUtils.getCertDigest(mApplicationContext, packageName);
@@ -206,12 +267,11 @@
         boolean isPartnerApp = AllowListUtils.isAllowListed(
                 packageName, packageCertificate, partnerAppsList);
         String sipInstanceName = isPartnerApp ? TRUSTED_PARTNER_APPS_SIP : UNKNOWN_APPS_SIP;
-        return (boolean) FlagsFactory.getFlags()
-                .getStableFlag(KEY_IS_ART_IMAGE_LOADING_OPTIMIZATION_ENABLED)
+        return (boolean) StableFlags.get(KEY_IS_ART_IMAGE_LOADING_OPTIMIZATION_ENABLED)
                     ? sipInstanceName + "_disable_art_image_" : sipInstanceName;
     }
 
-    boolean isSharedIsolatedProcessRequested(ComponentName service) throws Exception {
+    private boolean isSharedIsolatedProcessRequested(ComponentName service) throws Exception {
         if (!SdkLevel.isAtLeastU()) {
             return false;
         }
@@ -219,8 +279,13 @@
         PackageManager pm = mApplicationContext.getPackageManager();
         ServiceInfo si = pm.getServiceInfo(service, PackageManager.GET_META_DATA);
 
+        sLogger.d(TAG + "Package manager = " + pm);
         if ((si.flags & si.FLAG_ISOLATED_PROCESS) == 0) {
-            throw new IllegalArgumentException("ODP client services should run in isolated processes.");
+            sLogger.e(
+                    TAG, "ODP client service not configured to run in isolated process " + service);
+            throw new OdpServiceException(
+                    Constants.STATUS_MANIFEST_PARSING_FAILED,
+                    "ODP client services should run in isolated processes.");
         }
 
         return (si.flags & si.FLAG_ALLOW_SHARED_ISOLATED_PROCESS) != 0;
diff --git a/src/com/android/ondevicepersonalization/services/request/AppRequestFlow.java b/src/com/android/ondevicepersonalization/services/request/AppRequestFlow.java
index ec10049..64d1b00 100644
--- a/src/com/android/ondevicepersonalization/services/request/AppRequestFlow.java
+++ b/src/com/android/ondevicepersonalization/services/request/AppRequestFlow.java
@@ -18,9 +18,12 @@
 
 import android.adservices.ondevicepersonalization.CalleeMetadata;
 import android.adservices.ondevicepersonalization.Constants;
+import android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceRequest;
 import android.adservices.ondevicepersonalization.ExecuteInputParcel;
+import android.adservices.ondevicepersonalization.ExecuteOptionsParcel;
 import android.adservices.ondevicepersonalization.ExecuteOutputParcel;
 import android.adservices.ondevicepersonalization.RenderingConfig;
+import android.adservices.ondevicepersonalization.UserData;
 import android.adservices.ondevicepersonalization.aidl.IExecuteCallback;
 import android.adservices.ondevicepersonalization.aidl.IIsolatedModelService;
 import android.annotation.NonNull;
@@ -55,6 +58,7 @@
 import com.android.ondevicepersonalization.services.util.CryptUtils;
 import com.android.ondevicepersonalization.services.util.DebugUtils;
 import com.android.ondevicepersonalization.services.util.LogUtils;
+import com.android.ondevicepersonalization.services.util.NoiseUtil;
 import com.android.ondevicepersonalization.services.util.StatsUtils;
 
 import com.google.common.util.concurrent.FluentFuture;
@@ -66,7 +70,9 @@
 
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -80,14 +86,14 @@
     private final String mCallingPackageName;
     @NonNull
     private final ComponentName mService;
-    @NonNull
-    private final Bundle mWrappedParams;
+    @NonNull private final Bundle mWrappedParams;
     @NonNull
     private final IExecuteCallback mCallback;
     @NonNull
     private final Context mContext;
     private final long mStartTimeMillis;
     private final long mServiceEntryTimeMillis;
+    private final ExecuteOptionsParcel mOptions;
 
     @NonNull
     private AtomicReference<IsolatedModelServiceProvider> mModelServiceProvider =
@@ -97,7 +103,7 @@
     private byte[] mSerializedAppParams;
 
     @VisibleForTesting
-    static class Injector {
+    public static class Injector {
         ListeningExecutorService getExecutor() {
             return OnDevicePersonalizationExecutors.getBackgroundExecutor();
         }
@@ -106,7 +112,7 @@
             return MonotonicClock.getInstance();
         }
 
-        Flags getFlags() {
+        public Flags getFlags() {
             return FlagsFactory.getFlags();
         }
 
@@ -114,12 +120,17 @@
             return OnDevicePersonalizationExecutors.getScheduledExecutor();
         }
 
-        boolean shouldValidateExecuteOutput() {
+        /** Returns whether should validate rendering configuration keys. */
+        public boolean shouldValidateExecuteOutput() {
             return DeviceConfig.getBoolean(
                     /* namespace= */ "on_device_personalization",
                     /* name= */ "debug.validate_rendering_config_keys",
                     /* defaultValue= */ true);
         }
+
+        public NoiseUtil getNoiseUtil() {
+            return new NoiseUtil();
+        }
     }
 
     @NonNull
@@ -132,13 +143,22 @@
             @NonNull IExecuteCallback callback,
             @NonNull Context context,
             long startTimeMillis,
-            long serviceEntryTimeMillis) {
-        this(callingPackageName, service, wrappedParams,
-                callback, context, startTimeMillis, serviceEntryTimeMillis, new Injector());
+            long serviceEntryTimeMillis,
+            ExecuteOptionsParcel options) {
+        this(
+                callingPackageName,
+                service,
+                wrappedParams,
+                callback,
+                context,
+                startTimeMillis,
+                serviceEntryTimeMillis,
+                options,
+                new Injector());
     }
 
     @VisibleForTesting
-    AppRequestFlow(
+    public AppRequestFlow(
             @NonNull String callingPackageName,
             @NonNull ComponentName service,
             @NonNull Bundle wrappedParams,
@@ -146,6 +166,7 @@
             @NonNull Context context,
             long startTimeMillis,
             long serviceEntryTimeMillis,
+            ExecuteOptionsParcel options,
             @NonNull Injector injector) {
         sLogger.d(TAG + ": AppRequestFlow created.");
         mCallingPackageName = Objects.requireNonNull(callingPackageName);
@@ -155,6 +176,7 @@
         mContext = Objects.requireNonNull(context);
         mStartTimeMillis = startTimeMillis;
         mServiceEntryTimeMillis =  serviceEntryTimeMillis;
+        mOptions = options;
         mInjector = Objects.requireNonNull(injector);
     }
 
@@ -163,13 +185,15 @@
         mStartServiceTimeMillis.set(mInjector.getClock().elapsedRealtime());
 
         try {
-            ByteArrayParceledSlice paramsBuffer = Objects.requireNonNull(
-                    mWrappedParams.getParcelable(
-                            Constants.EXTRA_APP_PARAMS_SERIALIZED, ByteArrayParceledSlice.class));
+            ByteArrayParceledSlice paramsBuffer =
+                    Objects.requireNonNull(
+                            mWrappedParams.getParcelable(
+                                    Constants.EXTRA_APP_PARAMS_SERIALIZED,
+                                    ByteArrayParceledSlice.class));
             mSerializedAppParams = Objects.requireNonNull(paramsBuffer.getByteArray());
         } catch (Exception e) {
             sLogger.d(TAG + ": Failed to extract app params.", e);
-            sendErrorResult(Constants.STATUS_INTERNAL_ERROR, e);
+            sendErrorResult(Constants.STATUS_INTERNAL_ERROR, 0, e);
             return false;
         }
 
@@ -180,13 +204,21 @@
                             mContext, mService.getPackageName()));
         } catch (Exception e) {
             sLogger.d(TAG + ": Failed to read manifest.", e);
-            sendErrorResult(Constants.STATUS_NAME_NOT_FOUND, e);
+            sendErrorResult(
+                    Constants.STATUS_MANIFEST_PARSING_FAILED, /* isolatedServiceErrorCode= */ 0, e);
             return false;
         }
 
         if (!mService.getClassName().equals(config.getServiceName())) {
             sLogger.d(TAG + ": service class not found");
-            sendErrorResult(Constants.STATUS_CLASS_NOT_FOUND, 0);
+            sendErrorResult(
+                    Constants.STATUS_MANIFEST_MISCONFIGURED,
+                    /* isolatedServiceErrorCode= */ 0,
+                    new ClassNotFoundException(
+                            "Expected: "
+                                    + mService.getClassName()
+                                    + " Found: "
+                                    + config.getServiceName()));
             return false;
         }
 
@@ -200,6 +232,8 @@
 
     @Override
     public Bundle getServiceParams() {
+        sLogger.d(TAG + ": getting service params.");
+
         DataAccessPermission localDataPermission = DataAccessPermission.READ_WRITE;
         if (!UserPrivacyStatus.getInstance().isMeasurementEnabled()) {
             localDataPermission = DataAccessPermission.READ_ONLY;
@@ -221,9 +255,11 @@
         serviceParams.putBinder(
                 Constants.EXTRA_FEDERATED_COMPUTE_SERVICE_BINDER,
                 new FederatedComputeServiceImpl(mService, mContext));
-        serviceParams.putParcelable(
-                Constants.EXTRA_USER_DATA,
-                new UserDataAccessor().getUserData());
+        UserData userData =
+                isPlatformDataProvided()
+                        ? new UserDataAccessor().getUserDataWithAppInstall()
+                        : new UserDataAccessor().getUserData();
+        serviceParams.putParcelable(Constants.EXTRA_USER_DATA, userData);
         mModelServiceProvider.set(new IsolatedModelServiceProvider());
         IIsolatedModelService modelService = mModelServiceProvider.get().getModelService(mContext);
         serviceParams.putBinder(Constants.EXTRA_MODEL_SERVICE_BINDER, modelService.asBinder());
@@ -233,12 +269,14 @@
 
     @Override
     public void uploadServiceFlowMetrics(ListenableFuture<Bundle> runServiceFuture) {
+        sLogger.d(TAG + ": uploading service flow metrics.");
         var unused =
                 FluentFuture.from(runServiceFuture)
                         .transform(
                                 val -> {
                                     StatsUtils.writeServiceRequestMetrics(
                                             Constants.API_NAME_SERVICE_ON_EXECUTE,
+                                            mService.getPackageName(),
                                             val,
                                             mInjector.getClock(),
                                             Constants.STATUS_SUCCESS,
@@ -251,6 +289,8 @@
                                 e -> {
                                     StatsUtils.writeServiceRequestMetrics(
                                             Constants.API_NAME_SERVICE_ON_EXECUTE,
+                                            mService.getPackageName(),
+
                                             /* result= */ null,
                                             mInjector.getClock(),
                                             Constants.STATUS_INTERNAL_ERROR,
@@ -303,12 +343,19 @@
                         sLogger.w(TAG + ": Request failed.", t);
                         if (t instanceof OdpServiceException) {
                             OdpServiceException e = (OdpServiceException) t;
+
                             sendErrorResult(
                                     e.getErrorCode(),
                                     DebugUtils.getIsolatedServiceExceptionCode(
-                                        mContext, mService, e));
+                                            mContext, mService, e),
+                                    t);
                         } else {
-                            sendErrorResult(Constants.STATUS_INTERNAL_ERROR, t);
+                            int errorCode =
+                                    t instanceof TimeoutException
+                                            ? Constants.STATUS_ISOLATED_SERVICE_TIMEOUT
+                                            : Constants.STATUS_INTERNAL_ERROR;
+                            sLogger.w(TAG + ": Failing with error code: " + errorCode);
+                            sendErrorResult(errorCode, /* isolatedServiceErrorCode= */ 0, t);
                         }
                     }
                 },
@@ -323,8 +370,11 @@
     private ListenableFuture<ExecuteOutputParcel> validateExecuteOutput(
             ExecuteOutputParcel result) {
         sLogger.d(TAG + ": validateExecuteOutput() started.");
-        if (mInjector.shouldValidateExecuteOutput()) {
-            try {
+        if (!mInjector.shouldValidateExecuteOutput()) {
+            sLogger.d(TAG + ": validateExecuteOutput() skipped.");
+            return Futures.immediateFuture(result);
+        }
+        try {
                 OnDevicePersonalizationVendorDataDao vendorDataDao =
                         OnDevicePersonalizationVendorDataDao.getInstance(mContext,
                                 mService,
@@ -332,14 +382,15 @@
                 if (result.getRenderingConfig() != null) {
                     Set<String> keyset = vendorDataDao.readAllVendorDataKeys();
                     if (!keyset.containsAll(result.getRenderingConfig().getKeys())) {
-                        return Futures.immediateFailedFuture(
-                                new OdpServiceException(Constants.STATUS_SERVICE_FAILED));
+                    return Futures.immediateFailedFuture(
+                            new OdpServiceException(Constants.STATUS_SERVICE_FAILED));
                     }
                 }
             } catch (Exception e) {
-                return Futures.immediateFailedFuture(e);
+            return Futures.immediateFailedFuture(
+                    new OdpServiceException(Constants.STATUS_SERVICE_FAILED));
             }
-        }
+        sLogger.d(TAG + ": validateExecuteOutput() succeeded.");
         return Futures.immediateFuture(result);
     }
 
@@ -384,15 +435,34 @@
             }
             Bundle bundle = new Bundle();
             bundle.putString(Constants.EXTRA_SURFACE_PACKAGE_TOKEN_STRING, token);
-            if (isOutputDataAllowed()) {
-                bundle.putByteArray(Constants.EXTRA_OUTPUT_DATA, result.getOutputData());
-            }
+            // bundle.getInt(key) returns 0 if the key is not found. It can be confused with the
+            // real best value 0, so set it to -1 explicitly to indicate this field is unset.
+            bundle.putInt(
+                    Constants.EXTRA_OUTPUT_BEST_VALUE, processBestValue(result.getBestValue()));
+
             return Futures.immediateFuture(bundle);
         } catch (Exception e) {
             return Futures.immediateFailedFuture(e);
         }
     }
 
+    private int processBestValue(int actualResult) {
+        int bestValue = -1;
+        if (!isOutputDataAllowed()
+                || mOptions.getOutputType()
+                        != ExecuteInIsolatedServiceRequest.OutputSpec.OUTPUT_TYPE_BEST_VALUE) {
+            return bestValue;
+        }
+        // Don't apply noise if partner only uses their own data.
+        if (!isPlatformDataProvided()) {
+            return actualResult;
+        }
+        return mInjector
+                .getNoiseUtil()
+                .applyNoiseToBestValue(
+                        actualResult, mOptions.getMaxIntValue(), ThreadLocalRandom.current());
+    }
+
     private boolean isOutputDataAllowed() {
         try {
             return AllowListUtils.isPairAllowListed(
@@ -407,8 +477,19 @@
         }
     }
 
+    private boolean isPlatformDataProvided() {
+        try {
+            return AllowListUtils.isAllowListed(
+                    mService.getPackageName(),
+                    PackageUtils.getCertDigest(mContext, mService.getPackageName()),
+                    mInjector.getFlags().getDefaultPlatformDataForExecuteAllowlist());
+        } catch (Exception e) {
+            sLogger.d(TAG + ": allow list error", e);
+            return false;
+        }
+    }
+
     private void sendSuccessResult(Bundle result) {
-        int responseCode = Constants.STATUS_SUCCESS;
         try {
             mCallback.onSuccess(
                     result,
@@ -417,31 +498,16 @@
                             .setCallbackInvokeTimeMillis(
                             SystemClock.elapsedRealtime()).build());
         } catch (RemoteException e) {
-            responseCode = Constants.STATUS_INTERNAL_ERROR;
             sLogger.w(TAG + ": Callback error", e);
         }
     }
 
-    private void sendErrorResult(int errorCode, int isolatedServiceErrorCode) {
+    private void sendErrorResult(int errorCode, int isolatedServiceErrorCode, Throwable t) {
         try {
             mCallback.onError(
                     errorCode,
                     isolatedServiceErrorCode,
-                    null,
-                    new CalleeMetadata.Builder()
-                            .setServiceEntryTimeMillis(mServiceEntryTimeMillis)
-                            .setCallbackInvokeTimeMillis(SystemClock.elapsedRealtime()).build());
-        } catch (RemoteException e) {
-            sLogger.w(TAG + ": Callback error", e);
-        }
-    }
-
-    private void sendErrorResult(int errorCode, Throwable t) {
-        try {
-            mCallback.onError(
-                    errorCode,
-                    0,
-                    DebugUtils.getErrorMessage(mContext, t),
+                    DebugUtils.serializeExceptionInfo(mService, t),
                     new CalleeMetadata.Builder()
                             .setServiceEntryTimeMillis(mServiceEntryTimeMillis)
                             .setCallbackInvokeTimeMillis(SystemClock.elapsedRealtime()).build());
diff --git a/src/com/android/ondevicepersonalization/services/request/RenderFlow.java b/src/com/android/ondevicepersonalization/services/request/RenderFlow.java
index 3cd6675..bd2dd9a 100644
--- a/src/com/android/ondevicepersonalization/services/request/RenderFlow.java
+++ b/src/com/android/ondevicepersonalization/services/request/RenderFlow.java
@@ -162,7 +162,7 @@
         try {
             if (!UserPrivacyStatus.getInstance().isProtectedAudienceEnabled()) {
                 sLogger.d(TAG + ": User control is not given for targeting.");
-                sendErrorResult(Constants.STATUS_PERSONALIZATION_DISABLED, 0);
+                sendErrorResult(Constants.STATUS_PERSONALIZATION_DISABLED, 0, null);
                 return false;
             }
 
@@ -175,7 +175,7 @@
                             mContext, servicePackageName));
             mService = ComponentName.createRelative(servicePackageName, serviceClassName);
         } catch (Exception e) {
-            sendErrorResult(Constants.STATUS_INTERNAL_ERROR, 0);
+            sendErrorResult(Constants.STATUS_INTERNAL_ERROR, 0, e);
             return false;
         }
         return true;
@@ -209,28 +209,33 @@
 
     @Override
     public void uploadServiceFlowMetrics(ListenableFuture<Bundle> runServiceFuture) {
-        var unused = FluentFuture.from(runServiceFuture)
-                .transform(
-                        val -> {
-                            StatsUtils.writeServiceRequestMetrics(
-                                    Constants.API_NAME_SERVICE_ON_RENDER,
-                                    val, mInjector.getClock(),
-                                    Constants.STATUS_SUCCESS, mStartServiceTimeMillis);
-                            return val;
-                        },
-                        mInjector.getExecutor()
-                )
-                .catchingAsync(
-                        Exception.class,
-                        e -> {
-                            StatsUtils.writeServiceRequestMetrics(
-                                    Constants.API_NAME_SERVICE_ON_RENDER, /* result= */ null,
-                                    mInjector.getClock(),
-                                    Constants.STATUS_INTERNAL_ERROR, mStartServiceTimeMillis);
-                            return Futures.immediateFailedFuture(e);
-                        },
-                        mInjector.getExecutor()
-                );
+        var unused =
+                FluentFuture.from(runServiceFuture)
+                        .transform(
+                                val -> {
+                                    StatsUtils.writeServiceRequestMetrics(
+                                            Constants.API_NAME_SERVICE_ON_RENDER,
+                                            mService.getPackageName(),
+                                            val,
+                                            mInjector.getClock(),
+                                            Constants.STATUS_SUCCESS,
+                                            mStartServiceTimeMillis);
+                                    return val;
+                                },
+                                mInjector.getExecutor())
+                        .catchingAsync(
+                                Exception.class,
+                                e -> {
+                                    StatsUtils.writeServiceRequestMetrics(
+                                            Constants.API_NAME_SERVICE_ON_RENDER,
+                                            mService.getPackageName(),
+                                            /* result= */ null,
+                                            mInjector.getClock(),
+                                            Constants.STATUS_INTERNAL_ERROR,
+                                            mStartServiceTimeMillis);
+                                    return Futures.immediateFailedFuture(e);
+                                },
+                                mInjector.getExecutor());
     }
 
     @Override
@@ -286,9 +291,10 @@
                             sendErrorResult(
                                     e.getErrorCode(),
                                     DebugUtils.getIsolatedServiceExceptionCode(
-                                            mContext, mService, e));
+                                            mContext, mService, e),
+                                    t);
                         } else {
-                            sendErrorResult(Constants.STATUS_INTERNAL_ERROR, t);
+                            sendErrorResult(Constants.STATUS_INTERNAL_ERROR, 0, t);
                         }
                     }
                 },
@@ -303,7 +309,10 @@
             sendSuccessResult(surfacePackage);
         } else {
             sLogger.w(TAG + ": surfacePackages is null or empty");
-            sendErrorResult(Constants.STATUS_INTERNAL_ERROR, 0);
+            sendErrorResult(
+                    Constants.STATUS_INTERNAL_ERROR,
+                    0,
+                    new IllegalStateException("missing surfacePackage"));
         }
     }
 
@@ -321,26 +330,12 @@
         }
     }
 
-    private void sendErrorResult(int errorCode, int isolatedServiceErrorCode) {
+    private void sendErrorResult(int errorCode, int isolatedServiceErrorCode, Throwable t) {
         try {
             mCallback.onError(
                     errorCode,
                     isolatedServiceErrorCode,
-                    null,
-                    new CalleeMetadata.Builder()
-                            .setServiceEntryTimeMillis(mServiceEntryTimeMillis)
-                            .setCallbackInvokeTimeMillis(SystemClock.elapsedRealtime()).build());
-        } catch (RemoteException e) {
-            sLogger.w(TAG + ": Callback error", e);
-        }
-    }
-
-    private void sendErrorResult(int errorCode, Throwable t) {
-        try {
-            mCallback.onError(
-                    errorCode,
-                    0,
-                    DebugUtils.getErrorMessage(mContext, t),
+                    DebugUtils.serializeExceptionInfo(mService, t),
                     new CalleeMetadata.Builder()
                             .setServiceEntryTimeMillis(mServiceEntryTimeMillis)
                             .setCallbackInvokeTimeMillis(SystemClock.elapsedRealtime()).build());
diff --git a/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowFactory.java b/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowFactory.java
index 50ea958..34f0ca9 100644
--- a/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowFactory.java
+++ b/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowFactory.java
@@ -18,6 +18,7 @@
 
 import android.adservices.ondevicepersonalization.DownloadCompletedOutputParcel;
 import android.adservices.ondevicepersonalization.EventOutputParcel;
+import android.adservices.ondevicepersonalization.ExecuteOptionsParcel;
 import android.adservices.ondevicepersonalization.RequestLogRecord;
 import android.adservices.ondevicepersonalization.aidl.IExecuteCallback;
 import android.adservices.ondevicepersonalization.aidl.IRegisterMeasurementEventCallback;
@@ -27,6 +28,7 @@
 import android.os.Bundle;
 import android.os.IBinder;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.ondevicepersonalization.services.data.events.EventUrlPayload;
 import com.android.ondevicepersonalization.services.display.WebViewFlow;
 import com.android.ondevicepersonalization.services.download.DownloadFlow;
@@ -43,26 +45,104 @@
     public static ServiceFlow createInstance(ServiceFlowType serviceFlowType, Object... args) {
         return switch (serviceFlowType) {
             case APP_REQUEST_FLOW ->
-                    new AppRequestFlow((String) args[0], (ComponentName) args[1], (Bundle) args[2],
-                            (IExecuteCallback) args[3], (Context) args[4], (long) args[5],
-                            (long) args[6]);
+                    new AppRequestFlow(
+                            (String) args[0],
+                            (ComponentName) args[1],
+                            (Bundle) args[2],
+                            (IExecuteCallback) args[3],
+                            (Context) args[4],
+                            (long) args[5],
+                            (long) args[6],
+                            (ExecuteOptionsParcel) args[7]);
             case RENDER_FLOW ->
-                    new RenderFlow((String) args[0], (IBinder) args[1], (int) args[2],
-                            (int) args[3], (int) args[4], (IRequestSurfacePackageCallback) args[5],
-                            (Context) args[6], (long) args[7], (long) args[8]);
+                    new RenderFlow(
+                            (String) args[0],
+                            (IBinder) args[1],
+                            (int) args[2],
+                            (int) args[3],
+                            (int) args[4],
+                            (IRequestSurfacePackageCallback) args[5],
+                            (Context) args[6],
+                            (long) args[7],
+                            (long) args[8]);
             case WEB_TRIGGER_FLOW ->
-                    new WebTriggerFlow((Bundle) args[0], (Context) args[1],
-                            (IRegisterMeasurementEventCallback) args[2], (long) args[3],
+                    new WebTriggerFlow(
+                            (Bundle) args[0],
+                            (Context) args[1],
+                            (IRegisterMeasurementEventCallback) args[2],
+                            (long) args[3],
                             (long) args[4]);
             case WEB_VIEW_FLOW ->
-                    new WebViewFlow((Context) args[0], (ComponentName) args[1], (long) args[2],
-                            (RequestLogRecord) args[3], (FutureCallback<EventOutputParcel>) args[4],
+                    new WebViewFlow(
+                            (Context) args[0],
+                            (ComponentName) args[1],
+                            (long) args[2],
+                            (RequestLogRecord) args[3],
+                            (FutureCallback<EventOutputParcel>) args[4],
                             (EventUrlPayload) args[5]);
             case DOWNLOAD_FLOW ->
-                    new DownloadFlow((String) args[0], (Context) args[1],
+                    new DownloadFlow(
+                            (String) args[0],
+                            (Context) args[1],
                             (FutureCallback<DownloadCompletedOutputParcel>) args[2]);
-            default -> throw new IllegalArgumentException(
-                    "Invalid service flow type: " + serviceFlowType);
+            default ->
+                    throw new IllegalArgumentException(
+                            "Invalid service flow type: " + serviceFlowType);
+        };
+    }
+
+    /** Create a service flow instance give the type for testing only. */
+    @VisibleForTesting
+    public static ServiceFlow createInstanceForTest(
+            ServiceFlowType serviceFlowType, Object... args) {
+        // TODO(b/354265327): only support injector in app request flow. Need update constructor for
+        // testing in other flows.
+        return switch (serviceFlowType) {
+            case APP_REQUEST_FLOW ->
+                    new AppRequestFlow(
+                            (String) args[0],
+                            (ComponentName) args[1],
+                            (Bundle) args[2],
+                            (IExecuteCallback) args[3],
+                            (Context) args[4],
+                            (long) args[5],
+                            (long) args[6],
+                            (ExecuteOptionsParcel) args[7],
+                            (AppRequestFlow.Injector) args[8]);
+            case RENDER_FLOW ->
+                    new RenderFlow(
+                            (String) args[0],
+                            (IBinder) args[1],
+                            (int) args[2],
+                            (int) args[3],
+                            (int) args[4],
+                            (IRequestSurfacePackageCallback) args[5],
+                            (Context) args[6],
+                            (long) args[7],
+                            (long) args[8]);
+            case WEB_TRIGGER_FLOW ->
+                    new WebTriggerFlow(
+                            (Bundle) args[0],
+                            (Context) args[1],
+                            (IRegisterMeasurementEventCallback) args[2],
+                            (long) args[3],
+                            (long) args[4]);
+            case WEB_VIEW_FLOW ->
+                    new WebViewFlow(
+                            (Context) args[0],
+                            (ComponentName) args[1],
+                            (long) args[2],
+                            (RequestLogRecord) args[3],
+                            (FutureCallback<EventOutputParcel>) args[4],
+                            (EventUrlPayload) args[5]);
+            case DOWNLOAD_FLOW ->
+                    new DownloadFlow(
+                            (String) args[0],
+                            (Context) args[1],
+                            (FutureCallback<DownloadCompletedOutputParcel>) args[2]);
+            default ->
+                    throw new IllegalArgumentException(
+                            "Invalid service flow type: " + serviceFlowType);
         };
     }
 }
diff --git a/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowOrchestrator.java b/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowOrchestrator.java
index 42878c1..d5b215e 100644
--- a/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowOrchestrator.java
+++ b/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowOrchestrator.java
@@ -16,8 +16,11 @@
 
 package com.android.ondevicepersonalization.services.serviceflow;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
 
+import java.util.concurrent.Executors;
+
 /** Orchestrator that handles the scheduling of all service flows. */
 public class ServiceFlowOrchestrator {
 
@@ -49,4 +52,13 @@
                     .submit(serviceFlowTask::run);
         };
     }
+
+    /** Schedules a given service flow task with the orchestrator for testing only. */
+    @VisibleForTesting
+    public void scheduleForTest(ServiceFlowType serviceFlowType, Object... args) {
+        ServiceFlow serviceFlow = ServiceFlowFactory.createInstanceForTest(serviceFlowType, args);
+
+        ServiceFlowTask serviceFlowTask = new ServiceFlowTask(serviceFlowType, serviceFlow);
+        Executors.newSingleThreadExecutor().submit(serviceFlowTask::run);
+    }
 }
diff --git a/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowTask.java b/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowTask.java
index 3455b3a..2c95c19 100644
--- a/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowTask.java
+++ b/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowTask.java
@@ -21,8 +21,8 @@
 import android.os.Bundle;
 
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
-import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
+import com.android.ondevicepersonalization.services.StableFlags;
 import com.android.ondevicepersonalization.services.process.IsolatedServiceInfo;
 import com.android.ondevicepersonalization.services.process.PluginProcessRunner;
 import com.android.ondevicepersonalization.services.process.ProcessRunner;
@@ -55,8 +55,7 @@
         mServiceFlowType = serviceFlowType;
         mServiceFlow = serviceFlow;
         mProcessRunner =
-                (boolean) FlagsFactory.getFlags()
-                        .getStableFlag(KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED)
+                (boolean) StableFlags.get(KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED)
                         ? SharedIsolatedProcessRunner.getInstance()
                         : PluginProcessRunner.getInstance();
     }
@@ -80,8 +79,10 @@
     /** Executes the given service flow. */
     public void run() {
         try {
-            if (mIsCompleted || !mServiceFlow.isServiceFlowReady()) {
-                sLogger.d(TAG + ": Unexpected service flow state for " + mServiceFlowType);
+            boolean isServiceFlowReady = mServiceFlow.isServiceFlowReady();
+            if (mIsCompleted || !isServiceFlowReady) {
+                sLogger.d(TAG + " skipped running %s, isCompleted: %s, isServiceFlowReady: %s",
+                        mServiceFlowType, mIsCompleted, isServiceFlowReady);
                 return;
             }
 
diff --git a/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowType.java b/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowType.java
index 52cf2b5..7af72ad 100644
--- a/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowType.java
+++ b/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowType.java
@@ -23,52 +23,35 @@
 import static android.adservices.ondevicepersonalization.Constants.OP_WEB_TRIGGER;
 import static android.adservices.ondevicepersonalization.Constants.OP_WEB_VIEW_EVENT;
 
-import static com.android.ondevicepersonalization.services.PhFlags.KEY_APP_REQUEST_FLOW_DEADLINE_SECONDS;
-import static com.android.ondevicepersonalization.services.PhFlags.KEY_DOWNLOAD_FLOW_DEADLINE_SECONDS;
-import static com.android.ondevicepersonalization.services.PhFlags.KEY_EXAMPLE_STORE_FLOW_DEADLINE_SECONDS;
-import static com.android.ondevicepersonalization.services.PhFlags.KEY_RENDER_FLOW_DEADLINE_SECONDS;
-import static com.android.ondevicepersonalization.services.PhFlags.KEY_WEB_TRIGGER_FLOW_DEADLINE_SECONDS;
-import static com.android.ondevicepersonalization.services.PhFlags.KEY_WEB_VIEW_FLOW_DEADLINE_SECONDS;
-
-import com.android.ondevicepersonalization.services.FlagsFactory;
-
 /** Collection of on-device personalization service flows. */
 public enum ServiceFlowType {
 
     APP_REQUEST_FLOW(
-            "AppRequest", OP_EXECUTE, Priority.HIGH,
-            (int) FlagsFactory.getFlags().getStableFlag(KEY_APP_REQUEST_FLOW_DEADLINE_SECONDS)),
+            "AppRequest", OP_EXECUTE, Priority.HIGH),
 
     RENDER_FLOW(
-            "Render", OP_RENDER, Priority.HIGH,
-            (int) FlagsFactory.getFlags().getStableFlag(KEY_RENDER_FLOW_DEADLINE_SECONDS)),
+            "Render", OP_RENDER, Priority.HIGH),
 
     WEB_TRIGGER_FLOW(
-            "WebTrigger", OP_WEB_TRIGGER, Priority.NORMAL,
-            (int) FlagsFactory.getFlags().getStableFlag(KEY_WEB_TRIGGER_FLOW_DEADLINE_SECONDS)),
+            "WebTrigger", OP_WEB_TRIGGER, Priority.NORMAL),
 
     WEB_VIEW_FLOW(
-            "ComputeEventMetrics", OP_WEB_VIEW_EVENT, Priority.NORMAL,
-            (int) FlagsFactory.getFlags().getStableFlag(KEY_WEB_VIEW_FLOW_DEADLINE_SECONDS)),
+            "WebView", OP_WEB_VIEW_EVENT, Priority.NORMAL),
 
     EXAMPLE_STORE_FLOW(
-            "ExampleStore", OP_TRAINING_EXAMPLE, Priority.NORMAL,
-            (int) FlagsFactory.getFlags().getStableFlag(KEY_EXAMPLE_STORE_FLOW_DEADLINE_SECONDS)),
+            "ExampleStore", OP_TRAINING_EXAMPLE, Priority.NORMAL),
 
     DOWNLOAD_FLOW(
-            "DownloadJob", OP_DOWNLOAD, Priority.LOW,
-            (int) FlagsFactory.getFlags().getStableFlag(KEY_DOWNLOAD_FLOW_DEADLINE_SECONDS));
+            "DownloadJob", OP_DOWNLOAD, Priority.LOW);
 
     final String mTaskName;
     final int mOperationCode;
     final Priority mPriority;
-    final int mExecutionTimeout;
 
-    ServiceFlowType(String taskName, int operationCode, Priority priority, int executionTimeout) {
+    ServiceFlowType(String taskName, int operationCode, Priority priority) {
         mTaskName = taskName;
         mOperationCode = operationCode;
         mPriority = priority;
-        mExecutionTimeout = executionTimeout;
     }
 
     public String getTaskName() {
@@ -83,10 +66,6 @@
         return mPriority;
     }
 
-    public int getExecutionTimeout() {
-        return mExecutionTimeout;
-    }
-
     public enum Priority {
         HIGH,
         NORMAL,
diff --git a/src/com/android/ondevicepersonalization/services/sharedlibrary/spe/OdpJobServiceFactory.java b/src/com/android/ondevicepersonalization/services/sharedlibrary/spe/OdpJobServiceFactory.java
index 8d94612..7540877 100644
--- a/src/com/android/ondevicepersonalization/services/sharedlibrary/spe/OdpJobServiceFactory.java
+++ b/src/com/android/ondevicepersonalization/services/sharedlibrary/spe/OdpJobServiceFactory.java
@@ -22,11 +22,11 @@
 import android.content.Context;
 
 import com.android.adservices.shared.proto.ModuleJobPolicy;
-import com.android.adservices.shared.proto.ProtoParser;
 import com.android.adservices.shared.spe.framework.JobServiceFactory;
 import com.android.adservices.shared.spe.framework.JobWorker;
 import com.android.adservices.shared.spe.logging.JobSchedulingLogger;
 import com.android.adservices.shared.spe.logging.JobServiceLogger;
+import com.android.adservices.shared.util.ProtoParser;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
@@ -86,6 +86,7 @@
                 ModuleJobPolicy policy =
                         ProtoParser.parseBase64EncodedStringToProto(
                                 ModuleJobPolicy.parser(),
+                                ClientErrorLogger.getInstance(),
                                 PROTO_PROPERTY_FOR_LOGCAT,
                                 flags.getOdpModuleJobPolicy());
                 sSingleton =
diff --git a/src/com/android/ondevicepersonalization/services/statsd/OdpStatsdLogger.java b/src/com/android/ondevicepersonalization/services/statsd/OdpStatsdLogger.java
index 3b7c39b..d958a33 100644
--- a/src/com/android/ondevicepersonalization/services/statsd/OdpStatsdLogger.java
+++ b/src/com/android/ondevicepersonalization/services/statsd/OdpStatsdLogger.java
@@ -17,9 +17,11 @@
 package com.android.ondevicepersonalization.services.statsd;
 
 import static com.android.ondevicepersonalization.OnDevicePersonalizationStatsLog.ONDEVICEPERSONALIZATION_API_CALLED;
+import static com.android.ondevicepersonalization.OnDevicePersonalizationStatsLog.ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__ADSERVICES_GET_COMMON_STATES;
 import static com.android.ondevicepersonalization.OnDevicePersonalizationStatsLog.ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__EVENT_URL_CREATE_WITH_REDIRECT;
 import static com.android.ondevicepersonalization.OnDevicePersonalizationStatsLog.ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__EVENT_URL_CREATE_WITH_RESPONSE;
 import static com.android.ondevicepersonalization.OnDevicePersonalizationStatsLog.ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__EXECUTE;
+import static com.android.ondevicepersonalization.OnDevicePersonalizationStatsLog.ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__FEDERATED_COMPUTE_CANCEL;
 import static com.android.ondevicepersonalization.OnDevicePersonalizationStatsLog.ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__FEDERATED_COMPUTE_SCHEDULE;
 import static com.android.ondevicepersonalization.OnDevicePersonalizationStatsLog.ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__LOCAL_DATA_GET;
 import static com.android.ondevicepersonalization.OnDevicePersonalizationStatsLog.ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__LOCAL_DATA_KEYSET;
@@ -28,6 +30,7 @@
 import static com.android.ondevicepersonalization.OnDevicePersonalizationStatsLog.ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__LOG_READER_GET_JOINED_EVENTS;
 import static com.android.ondevicepersonalization.OnDevicePersonalizationStatsLog.ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__LOG_READER_GET_REQUESTS;
 import static com.android.ondevicepersonalization.OnDevicePersonalizationStatsLog.ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__MODEL_MANAGER_RUN;
+import static com.android.ondevicepersonalization.OnDevicePersonalizationStatsLog.ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__NOTIFY_MEASUREMENT_EVENT;
 import static com.android.ondevicepersonalization.OnDevicePersonalizationStatsLog.ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__REMOTE_DATA_GET;
 import static com.android.ondevicepersonalization.OnDevicePersonalizationStatsLog.ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__REMOTE_DATA_KEYSET;
 import static com.android.ondevicepersonalization.OnDevicePersonalizationStatsLog.ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__REQUEST_SURFACE_PACKAGE;
@@ -46,9 +49,11 @@
 public class OdpStatsdLogger {
     private static volatile OdpStatsdLogger sStatsdLogger = null;
     private static final Set<Integer> sApiNames = Set.of(
+            ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__ADSERVICES_GET_COMMON_STATES,
             ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__EVENT_URL_CREATE_WITH_REDIRECT,
             ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__EVENT_URL_CREATE_WITH_RESPONSE,
             ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__EXECUTE,
+            ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__FEDERATED_COMPUTE_CANCEL,
             ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__FEDERATED_COMPUTE_SCHEDULE,
             ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__LOCAL_DATA_GET,
             ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__LOCAL_DATA_KEYSET,
@@ -57,6 +62,7 @@
             ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__LOG_READER_GET_JOINED_EVENTS,
             ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__LOG_READER_GET_REQUESTS,
             ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__MODEL_MANAGER_RUN,
+            ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__NOTIFY_MEASUREMENT_EVENT,
             ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__REMOTE_DATA_GET,
             ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__REMOTE_DATA_KEYSET,
             ON_DEVICE_PERSONALIZATION_API_CALLED__API_NAME__REQUEST_SURFACE_PACKAGE,
diff --git a/src/com/android/ondevicepersonalization/services/statsd/joblogging/OdpJobServiceLogger.java b/src/com/android/ondevicepersonalization/services/statsd/joblogging/OdpJobServiceLogger.java
index b82bea3..063229a 100644
--- a/src/com/android/ondevicepersonalization/services/statsd/joblogging/OdpJobServiceLogger.java
+++ b/src/com/android/ondevicepersonalization/services/statsd/joblogging/OdpJobServiceLogger.java
@@ -36,12 +36,12 @@
 /** A background job logger to log ODP background job stats. */
 public final class OdpJobServiceLogger extends JobServiceLogger {
     @GuardedBy("SINGLETON_LOCK")
-    private static volatile OdpJobServiceLogger sSingleton;
+    private static OdpJobServiceLogger sSingleton;
 
     private static final Object SINGLETON_LOCK = new Object();
 
     /** Create an instance of {@link JobServiceLogger}. */
-    public OdpJobServiceLogger(
+    private OdpJobServiceLogger(
             Context context,
             Clock clock,
             StatsdJobServiceLogger statsdLogger,
diff --git a/src/com/android/ondevicepersonalization/services/util/AllowListUtils.java b/src/com/android/ondevicepersonalization/services/util/AllowListUtils.java
index b1fa932..a9ccd56 100644
--- a/src/com/android/ondevicepersonalization/services/util/AllowListUtils.java
+++ b/src/com/android/ondevicepersonalization/services/util/AllowListUtils.java
@@ -31,10 +31,12 @@
     public static boolean isAllowListed(final String entityName,
                                         final String packageCertificate,
                                         @NonNull final String allowList) {
+        if (allowList == null) {
+            return false;
+        }
         if (ALLOW_ALL.equals(allowList)) {
             return true;
         }
-
         if (entityName == null || entityName.trim().isEmpty()) {
             return false;
         }
@@ -82,6 +84,8 @@
         String[] entityAndCert = entityInAllowList.split(CERT_SPLITTER);
         if (entityAndCert == null) {
             return false;
+        } else if (ALLOW_ALL.equals(entityInAllowList)) {
+            return true;
         } else if (entityAndCert.length == 1 && entityAndCert[0] != null
                 && !entityAndCert[0].isBlank()) {
             return entityAndCert[0].equals(entityName);
diff --git a/src/com/android/ondevicepersonalization/services/util/DebugUtils.java b/src/com/android/ondevicepersonalization/services/util/DebugUtils.java
index dbf3acc..1f5e095 100644
--- a/src/com/android/ondevicepersonalization/services/util/DebugUtils.java
+++ b/src/com/android/ondevicepersonalization/services/util/DebugUtils.java
@@ -21,13 +21,17 @@
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.os.Build;
+import android.os.SystemProperties;
 import android.provider.Settings;
 
 import com.android.odp.module.common.PackageUtils;
+import com.android.ondevicepersonalization.internal.util.ExceptionInfo;
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.OdpServiceException;
+import com.android.ondevicepersonalization.services.OnDevicePersonalizationApplication;
 
 import java.util.Objects;
 
@@ -35,6 +39,12 @@
 public class DebugUtils {
     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
     private static final String TAG = DebugUtils.class.getSimpleName();
+    private static final int MAX_EXCEPTION_CHAIN_DEPTH = 3;
+
+    private static final String OVERRIDE_FC_SERVER_URL_PACKAGE =
+            "debug.ondevicepersonalization.override_fc_server_url_package";
+    private static final String OVERRIDE_FC_SERVER_URL =
+            "debug.ondevicepersonalization.override_fc_server_url";
 
     /** Returns true if the device is debuggable. */
     public static boolean isDeveloperModeEnabled(@NonNull Context context) {
@@ -65,17 +75,59 @@
         return 0;
     }
 
-    /** Returns the exception message if debugging is allowed. */
-    public static String getErrorMessage(@NonNull Context context, Throwable t) {
+    /** Serializes an exception chain to a byte[] */
+    public static byte[] serializeExceptionInfo(
+            ComponentName service, Throwable t) {
         try {
-            if (t != null && isDeveloperModeEnabled(context)
-                    && FlagsFactory.getFlags().isIsolatedServiceDebuggingEnabled()) {
-                return t.getClass().getSimpleName() + ": " + t.getMessage();
+            Context context = OnDevicePersonalizationApplication.getAppContext();
+            if (t == null || !isDeveloperModeEnabled(context)
+                    || !FlagsFactory.getFlags().isIsolatedServiceDebuggingEnabled()
+                    || !PackageUtils.isPackageDebuggable(context, service.getPackageName())) {
+                return null;
             }
+
+            return ExceptionInfo.toByteArray(t, MAX_EXCEPTION_CHAIN_DEPTH);
         } catch (Exception e) {
-            sLogger.e(e, TAG + ": failed to get message");
+            sLogger.e(e, TAG + ": failed to serialize exception info");
+            return null;
         }
-        return null;
+    }
+
+    /**
+     * Returns an override URL for federated compute for the provided package if one exists, else
+     * returns empty if a matching override is not found.
+     *
+     * @param applicationContext the application context.
+     * @param packageName the package for which to check for override.
+     * @return override URL or empty string if an override is not found.
+     */
+    public static String getFcServerOverrideUrl(Context applicationContext, String packageName) {
+        String url = "";
+        // Check for override manifest url property, if package is debuggable
+        try {
+            if (!PackageUtils.isPackageDebuggable(applicationContext, packageName)) {
+                return url;
+            }
+        } catch (PackageManager.NameNotFoundException nne) {
+            sLogger.e(TAG + ": failed to get override URL for package." + nne);
+            return url;
+        }
+
+        // Check system properties first
+        if (SystemProperties.get(OVERRIDE_FC_SERVER_URL_PACKAGE, "").equals(packageName)) {
+            String overrideManifestUrl = SystemProperties.get(OVERRIDE_FC_SERVER_URL, "");
+            if (!overrideManifestUrl.isEmpty()) {
+                sLogger.d(
+                        TAG
+                                + ": Overriding FC server URL from system properties for package"
+                                + packageName
+                                + " to "
+                                + overrideManifestUrl);
+                url = overrideManifestUrl;
+            }
+        }
+
+        return url;
     }
 
     private DebugUtils() {}
diff --git a/src/com/android/ondevicepersonalization/services/util/NoiseUtil.java b/src/com/android/ondevicepersonalization/services/util/NoiseUtil.java
new file mode 100644
index 0000000..b3cd783
--- /dev/null
+++ b/src/com/android/ondevicepersonalization/services/util/NoiseUtil.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 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.ondevicepersonalization.services.util;
+
+import com.android.ondevicepersonalization.internal.util.LoggerFactory;
+import com.android.ondevicepersonalization.services.FlagsFactory;
+
+import java.util.concurrent.ThreadLocalRandom;
+
+/** Util class for adding noise to returned result. */
+public class NoiseUtil {
+    private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
+    private static final String TAG = NoiseUtil.class.getSimpleName();
+
+    /**
+     * Add noise to {@link OnDevicePersonalizationManager#executeInIsolatedService} with best value
+     * option.
+     */
+    public int applyNoiseToBestValue(int actualValue, int maxValue, ThreadLocalRandom random) {
+        if (actualValue < 0 || actualValue > maxValue) {
+            sLogger.e(
+                    TAG + ": returned int value %d is not in the range [0, %d].",
+                    actualValue,
+                    maxValue);
+            return -1;
+        }
+        int noisedValue = actualValue;
+        boolean shouldSelectRandomValue =
+                random.nextDouble() < FlagsFactory.getFlags().getNoiseForExecuteBestValue();
+        if (shouldSelectRandomValue) {
+            while (noisedValue == actualValue) {
+                noisedValue = random.nextInt(maxValue);
+            }
+        }
+        return noisedValue;
+    }
+}
diff --git a/src/com/android/ondevicepersonalization/services/util/StatsUtils.java b/src/com/android/ondevicepersonalization/services/util/StatsUtils.java
index ae0dd1f..9a41fbc 100644
--- a/src/com/android/ondevicepersonalization/services/util/StatsUtils.java
+++ b/src/com/android/ondevicepersonalization/services/util/StatsUtils.java
@@ -42,28 +42,31 @@
         return callerLatencyMillis - calleeLatencyMillis;
     }
 
-    /** Writes app request usage to statsd. */
-    public static void writeAppRequestMetrics(
-            int apiName, Clock clock, int responseCode, long startTimeMillis) {
-        int latencyMillis = (int) (clock.elapsedRealtime() - startTimeMillis);
-        ApiCallStats callStats = new ApiCallStats.Builder(apiName)
-                .setLatencyMillis(latencyMillis)
-                .setResponseCode(responseCode)
-                .build();
+    /** Writes service request usage to statsd. Mainly for failure case. */
+    public static void writeServiceRequestMetrics(int apiName, int responseCode) {
+        ApiCallStats callStats =
+                new ApiCallStats.Builder(apiName).setResponseCode(responseCode).build();
         OdpStatsdLogger.getInstance().logApiCallStats(callStats);
     }
 
     /** Writes service request usage to statsd. */
     public static void writeServiceRequestMetrics(
-            int apiName, Bundle result, Clock clock, int responseCode, long startTimeMillis) {
+            int apiName,
+            String sdkPackageName,
+            Bundle result,
+            Clock clock,
+            int responseCode,
+            long startTimeMillis) {
         int latencyMillis = (int) (clock.elapsedRealtime() - startTimeMillis);
         int overheadLatencyMillis =
                 (int) StatsUtils.getOverheadLatencyMillis(latencyMillis, result);
-        ApiCallStats callStats = new ApiCallStats.Builder(apiName)
-                .setLatencyMillis(latencyMillis)
-                .setOverheadLatencyMillis(overheadLatencyMillis)
-                .setResponseCode(responseCode)
-                .build();
+        ApiCallStats callStats =
+                new ApiCallStats.Builder(apiName)
+                        .setLatencyMillis(latencyMillis)
+                        .setOverheadLatencyMillis(overheadLatencyMillis)
+                        .setResponseCode(responseCode)
+                        .setSdkPackageName(sdkPackageName == null ? "" : sdkPackageName)
+                        .build();
         OdpStatsdLogger.getInstance().logApiCallStats(callStats);
     }
     private StatsUtils() {}
diff --git a/src/com/android/ondevicepersonalization/services/webtrigger/WebTriggerFlow.java b/src/com/android/ondevicepersonalization/services/webtrigger/WebTriggerFlow.java
index 00cec81..669b2f1 100644
--- a/src/com/android/ondevicepersonalization/services/webtrigger/WebTriggerFlow.java
+++ b/src/com/android/ondevicepersonalization/services/webtrigger/WebTriggerFlow.java
@@ -218,28 +218,33 @@
 
     @Override
     public void uploadServiceFlowMetrics(ListenableFuture<Bundle> runServiceFuture) {
-        var unused = FluentFuture.from(runServiceFuture)
-                .transform(
-                        val -> {
-                            StatsUtils.writeServiceRequestMetrics(
-                                    Constants.API_NAME_SERVICE_ON_WEB_TRIGGER,
-                                    val, mInjector.getClock(),
-                                    Constants.STATUS_SUCCESS, mStartServiceTimeMillis);
-                            return val;
-                        },
-                        mInjector.getExecutor()
-                )
-                .catchingAsync(
-                        Exception.class,
-                        e -> {
-                            StatsUtils.writeServiceRequestMetrics(
-                                    Constants.API_NAME_SERVICE_ON_WEB_TRIGGER, /* result= */ null,
-                                    mInjector.getClock(),
-                                    Constants.STATUS_INTERNAL_ERROR, mStartServiceTimeMillis);
-                            return Futures.immediateFailedFuture(e);
-                        },
-                        mInjector.getExecutor()
-                );
+        var unused =
+                FluentFuture.from(runServiceFuture)
+                        .transform(
+                                val -> {
+                                    StatsUtils.writeServiceRequestMetrics(
+                                            Constants.API_NAME_SERVICE_ON_WEB_TRIGGER,
+                                            mServiceParcel.getIsolatedService().getPackageName(),
+                                            val,
+                                            mInjector.getClock(),
+                                            Constants.STATUS_SUCCESS,
+                                            mStartServiceTimeMillis);
+                                    return val;
+                                },
+                                mInjector.getExecutor())
+                        .catchingAsync(
+                                Exception.class,
+                                e -> {
+                                    StatsUtils.writeServiceRequestMetrics(
+                                            Constants.API_NAME_SERVICE_ON_WEB_TRIGGER,
+                                            mServiceParcel.getIsolatedService().getPackageName(),
+                                            /* result= */ null,
+                                            mInjector.getClock(),
+                                            Constants.STATUS_INTERNAL_ERROR,
+                                            mStartServiceTimeMillis);
+                                    return Futures.immediateFailedFuture(e);
+                                },
+                                mInjector.getExecutor());
     }
 
     @Override
diff --git a/tests/commontests/Android.bp b/tests/commontests/Android.bp
new file mode 100644
index 0000000..ab9ff95
--- /dev/null
+++ b/tests/commontests/Android.bp
@@ -0,0 +1,67 @@
+// Copyright (C) 2024 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+    default_team: "trendy_team_android_rubidium",
+}
+
+android_test {
+    name: "CommonUtilsTests",
+    srcs: [
+        "src/**/*.java",
+        ":common-ondevicepersonalization-sources",
+    ],
+    defaults: [
+        // For ExtendedMockito dependencies.
+        "modules-utils-testable-device-config-defaults",
+    ],
+    libs: [
+        "android.test.base.stubs.system",
+        "android.test.runner.stubs.system",
+        "auto_value_annotations",
+        "framework-annotations-lib",
+        "framework-ondevicepersonalization.impl",
+        "truth",
+    ],
+    static_libs: [
+        "androidx.test.ext.junit",
+        "androidx.test.ext.truth",
+        "androidx.test.rules",
+        "federated-compute-java-proto-lite",
+        "flatbuffers-java",
+        "mockito-target-extended-minus-junit4",
+        "modules-utils-build",
+        "modules-utils-preconditions",
+        "libprotobuf-java-lite",
+    ],
+    manifest: "AndroidManifest.xml",
+    plugins: ["auto_value_plugin"],
+    sdk_version: "module_current",
+    target_sdk_version: "current",
+    min_sdk_version: "Tiramisu",
+    certificate: "platform",
+    compile_multilib: "both",
+    test_config: "AndroidTest.xml",
+    test_suites: [
+        "general-tests",
+        "mts-ondevicepersonalization",
+    ],
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+        "libfcp_cpp_dep_jni",
+        "libfcp_hpke_jni",
+    ],
+}
diff --git a/tests/commontests/AndroidManifest.xml b/tests/commontests/AndroidManifest.xml
new file mode 100644
index 0000000..696aa97
--- /dev/null
+++ b/tests/commontests/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.odp.module.commontests" >
+
+    <!-- Used for scheduling connectivity jobs -->
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <!-- Used for persisting scheduled jobs -->
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+    <uses-permission android:name="android.permission.BIND_EXAMPLE_STORE_SERVICE" />
+
+    <application android:label="CommonUtilsTests"
+                 android:debuggable="true">
+        <uses-library android:name="android.test.runner"/>
+
+    </application>
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.odp.module.commontests"
+                     android:label="Tests of common shared utilities for odp module"/>
+</manifest>
\ No newline at end of file
diff --git a/tests/commontests/AndroidTest.xml b/tests/commontests/AndroidTest.xml
new file mode 100644
index 0000000..617277c
--- /dev/null
+++ b/tests/commontests/AndroidTest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 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="Configuration for Common Utils unit tests">
+    <option name="config-descriptor:metadata" key="component" value="framework" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="test-file-name" value="CommonUtilsTests.apk"/>
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="hidden-api-checks" value="false" />
+        <option name="package" value="com.android.odp.module.commontests"/>
+    </test>
+
+    <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+        <option name="mainline-module-package-name" value="com.google.android.ondevicepersonalization" />
+    </object>
+    <option name="config-descriptor:metadata" key="mainline-param" value="com.google.android.ondevicepersonalization.apex" />
+</configuration>
diff --git a/tests/commontests/src/com/android/odp/module/common/HttpClientTest.java b/tests/commontests/src/com/android/odp/module/common/HttpClientTest.java
new file mode 100644
index 0000000..fc30684
--- /dev/null
+++ b/tests/commontests/src/com/android/odp/module/common/HttpClientTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2024 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.odp.module.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Spy;
+import org.mockito.quality.Strictness;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@RunWith(JUnit4.class)
+public final class HttpClientTest {
+    @Rule
+    public final ExtendedMockitoRule extendedMockitoRule =
+            new ExtendedMockitoRule.Builder(this).setStrictness(Strictness.LENIENT).build();
+
+    private static final int DEFAULT_RETRY_LIMIT = 3;
+    private static final int HTTP_UNAVAILABLE = 503;
+    private static final int HTTP_OK = 200;
+
+    @Spy
+    private HttpClient mHttpClient =
+            new HttpClient(DEFAULT_RETRY_LIMIT, MoreExecutors.newDirectExecutorService());
+
+    @Test
+    public void testPerformGetRequestFailsWithRetry() throws Exception {
+        String failureMessage = "FAIL!";
+        OdpHttpResponse testFailedResponse =
+                new OdpHttpResponse.Builder()
+                        .setHeaders(new HashMap<>())
+                        .setPayload(failureMessage.getBytes(UTF_8))
+                        .setStatusCode(HTTP_UNAVAILABLE)
+                        .build();
+        TestHttpIOSupplier testSupplier = new TestHttpIOSupplier(testFailedResponse);
+
+        OdpHttpResponse returnedResponse = mHttpClient.performRequestWithRetry(testSupplier);
+
+        assertEquals(DEFAULT_RETRY_LIMIT, testSupplier.mCallCount.get());
+        assertThat(returnedResponse.getStatusCode()).isEqualTo(HTTP_UNAVAILABLE);
+        assertTrue(returnedResponse.getHeaders().isEmpty());
+        assertThat(returnedResponse.getPayload()).isEqualTo(failureMessage.getBytes(UTF_8));
+    }
+
+    @Test
+    public void testPerformGetRequestSuccessWithRetry() throws Exception {
+        Map<String, List<String>> mockHeaders = new HashMap<>();
+        mockHeaders.put("Header1", Arrays.asList("Value1"));
+        String failureMessage = "FAIL!";
+        String successMessage = "Success!";
+        OdpHttpResponse testFailedResponse =
+                new OdpHttpResponse.Builder()
+                        .setHeaders(new HashMap<>())
+                        .setPayload(failureMessage.getBytes(UTF_8))
+                        .setStatusCode(HTTP_UNAVAILABLE)
+                        .build();
+        OdpHttpResponse testSuccessfulResponse =
+                new OdpHttpResponse.Builder()
+                        .setPayload(successMessage.getBytes(UTF_8))
+                        .setStatusCode(HTTP_OK)
+                        .setHeaders(mockHeaders)
+                        .build();
+        TestHttpIOSupplier testSupplier =
+                new TestHttpIOSupplier(
+                        testSuccessfulResponse, testFailedResponse, DEFAULT_RETRY_LIMIT - 1);
+
+        OdpHttpResponse response = mHttpClient.performRequestWithRetry(testSupplier);
+
+        assertEquals(DEFAULT_RETRY_LIMIT, testSupplier.mCallCount.get());
+        assertThat(response.getStatusCode()).isEqualTo(HTTP_OK);
+        assertThat(response.getHeaders()).isEqualTo(mockHeaders);
+    }
+
+    private static final class TestHttpIOSupplier
+            implements HttpClient.HttpIOSupplier<OdpHttpResponse> {
+        private final AtomicInteger mCallCount = new AtomicInteger(0);
+
+        private final OdpHttpResponse mSuccessfulResponse;
+        private final OdpHttpResponse mFailedResponse;
+        private final int mNumFailedCalls;
+
+        private TestHttpIOSupplier(OdpHttpResponse failedResponse) {
+            this(null, failedResponse, 0);
+        }
+
+        private TestHttpIOSupplier(
+                OdpHttpResponse successfulResponse,
+                OdpHttpResponse failedResponse,
+                int numFailedCalls) {
+            this.mSuccessfulResponse = successfulResponse;
+            this.mFailedResponse = failedResponse;
+            this.mNumFailedCalls = numFailedCalls;
+        }
+
+        @Override
+        public OdpHttpResponse get() throws IOException {
+            int callCount = mCallCount.incrementAndGet();
+            if (mSuccessfulResponse == null) {
+                return mFailedResponse;
+            }
+
+            return callCount > mNumFailedCalls ? mSuccessfulResponse : mFailedResponse;
+        }
+    }
+}
diff --git a/tests/commontests/src/com/android/odp/module/common/HttpClientUtilsTest.java b/tests/commontests/src/com/android/odp/module/common/HttpClientUtilsTest.java
new file mode 100644
index 0000000..d3e8d17
--- /dev/null
+++ b/tests/commontests/src/com/android/odp/module/common/HttpClientUtilsTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.odp.module.common;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.when;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.android.odp.module.common.HttpClientUtils.HttpMethod;
+import com.android.odp.module.common.HttpClientUtils.HttpURLConnectionSupplier;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class HttpClientUtilsTest {
+    private static final String TEST_URI = "https://valid.com";
+    private static final Map<String, String> TEST_REQUEST_HEADERS = Map.of("Foo", "Bar");
+
+    private static final OdpHttpRequest DEFAULT_GET_REQUEST =
+            OdpHttpRequest.create(
+                    "https://google.com",
+                    HttpClientUtils.HttpMethod.GET,
+                    new HashMap<>(),
+                    HttpClientUtils.EMPTY_BODY);
+
+    private static final Map<String, List<String>> TEST_RESPONSE_HEADERS =
+            ImmutableMap.of(
+                    "x-content", ImmutableList.of("1", "2"), "api-key", ImmutableList.of("xyz"));
+
+    private static final OdpHttpResponse TEST_RESPONSE =
+            new OdpHttpResponse.Builder()
+                    .setStatusCode(200)
+                    .setPayload("payload".getBytes(UTF_8))
+                    .setHeaders(TEST_RESPONSE_HEADERS)
+                    .build();
+
+    private static final OdpHttpRequest TEST_EMPTY_REQUEST =
+            OdpHttpRequest.create(
+                    TEST_URI, HttpMethod.GET, TEST_REQUEST_HEADERS, HttpClientUtils.EMPTY_BODY);
+
+    private static final String TEST_FAILURE_MESSAGE = "FAIL!";
+    private static final String TEST_SUCCESS_MESSAGE = "Success!";
+    @Mock private HttpURLConnectionSupplier mHttpURLConnectionSupplier;
+    @Mock private HttpURLConnection mMockHttpURLConnection;
+
+    @Before
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        doReturn(mMockHttpURLConnection).when(mHttpURLConnectionSupplier).get();
+    }
+
+    @Test
+    public void testGetTotalSentBytes_emptyBody() {
+        assertThat(HttpClientUtils.getTotalSentBytes(TEST_EMPTY_REQUEST)).isEqualTo(42);
+    }
+
+    @Test
+    public void testGetTotalReceivedBytes() {
+        assertThat(HttpClientUtils.getTotalReceivedBytes(TEST_RESPONSE)).isEqualTo(43);
+    }
+
+    @Test
+    public void testUnableToOpenconnection_returnFailure() throws Exception {
+        OdpHttpRequest request =
+                OdpHttpRequest.create(
+                        "https://google.com",
+                        HttpClientUtils.HttpMethod.POST,
+                        new HashMap<>(),
+                        HttpClientUtils.EMPTY_BODY);
+        doThrow(new IOException()).when(mHttpURLConnectionSupplier).get();
+
+        assertThrows(
+                IOException.class,
+                () -> HttpClientUtils.performRequest(request, mHttpURLConnectionSupplier, false));
+    }
+
+    @Test
+    public void testPerformGetRequestSuccess() throws Exception {
+        InputStream mockStream = new ByteArrayInputStream(TEST_SUCCESS_MESSAGE.getBytes(UTF_8));
+        Map<String, List<String>> mockHeaders = new HashMap<>();
+        mockHeaders.put("Header1", Arrays.asList("Value1"));
+        when(mMockHttpURLConnection.getInputStream()).thenReturn(mockStream);
+        when(mMockHttpURLConnection.getResponseCode()).thenReturn(200);
+        when(mMockHttpURLConnection.getHeaderFields()).thenReturn(mockHeaders);
+        when(mMockHttpURLConnection.getContentLengthLong())
+                .thenReturn((long) TEST_SUCCESS_MESSAGE.length());
+
+        OdpHttpResponse response =
+                HttpClientUtils.performRequest(
+                        DEFAULT_GET_REQUEST, mHttpURLConnectionSupplier, false);
+
+        assertThat(response.getStatusCode()).isEqualTo(200);
+        assertThat(response.getHeaders()).isEqualTo(mockHeaders);
+        assertThat(response.getPayload()).isEqualTo(TEST_SUCCESS_MESSAGE.getBytes(UTF_8));
+    }
+
+    @Test
+    public void testPerformGetRequestPayloadIntoFileSuccess() throws Exception {
+        InputStream mockStream = new ByteArrayInputStream(TEST_SUCCESS_MESSAGE.getBytes(UTF_8));
+        Map<String, List<String>> mockHeaders = new HashMap<>();
+        mockHeaders.put("Header1", Arrays.asList("Value1"));
+        when(mMockHttpURLConnection.getInputStream()).thenReturn(mockStream);
+        when(mMockHttpURLConnection.getResponseCode()).thenReturn(200);
+        when(mMockHttpURLConnection.getHeaderFields()).thenReturn(mockHeaders);
+        when(mMockHttpURLConnection.getContentLengthLong())
+                .thenReturn((long) TEST_SUCCESS_MESSAGE.length());
+
+        OdpHttpResponse response =
+                HttpClientUtils.performRequest(
+                        DEFAULT_GET_REQUEST, mHttpURLConnectionSupplier, true);
+
+        assertThat(response.getStatusCode()).isEqualTo(200);
+        assertThat(response.getHeaders()).isEqualTo(mockHeaders);
+        assertThat(response.getPayload()).isEqualTo(null);
+        assertThat(response.getPayloadFileName()).isNotEmpty();
+        assertThat(new FileInputStream(response.getPayloadFileName()).readAllBytes())
+                .isEqualTo(TEST_SUCCESS_MESSAGE.getBytes(UTF_8));
+    }
+
+    @Test
+    public void testPerformGetRequestFails() throws Exception {
+        InputStream mockStream = new ByteArrayInputStream(TEST_FAILURE_MESSAGE.getBytes(UTF_8));
+        when(mMockHttpURLConnection.getErrorStream()).thenReturn(mockStream);
+        when(mMockHttpURLConnection.getResponseCode()).thenReturn(503);
+        when(mMockHttpURLConnection.getHeaderFields()).thenReturn(new HashMap<>());
+        when(mMockHttpURLConnection.getContentLengthLong())
+                .thenReturn((long) TEST_FAILURE_MESSAGE.length());
+
+        OdpHttpResponse response =
+                HttpClientUtils.performRequest(
+                        DEFAULT_GET_REQUEST, mHttpURLConnectionSupplier, false);
+
+        assertThat(response.getStatusCode()).isEqualTo(503);
+        assertTrue(response.getHeaders().isEmpty());
+        assertThat(response.getPayload()).isEqualTo(TEST_FAILURE_MESSAGE.getBytes(UTF_8));
+    }
+
+    @Test
+    public void testSetup() throws Exception {
+        URL testURL = new URL(TEST_URI);
+
+        URLConnection urlConnection = HttpClientUtils.setup(testURL);
+
+        assertEquals(HttpClientUtils.NETWORK_CONNECT_TIMEOUT_MS, urlConnection.getConnectTimeout());
+        assertEquals(HttpClientUtils.NETWORK_READ_TIMEOUT_MS, urlConnection.getReadTimeout());
+    }
+}
diff --git a/tests/commontests/src/com/android/odp/module/common/OdpHttpRequestTest.java b/tests/commontests/src/com/android/odp/module/common/OdpHttpRequestTest.java
new file mode 100644
index 0000000..bbb71c9
--- /dev/null
+++ b/tests/commontests/src/com/android/odp/module/common/OdpHttpRequestTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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.odp.module.common;
+
+import static com.android.odp.module.common.HttpClientUtils.ACCEPT_ENCODING_HDR;
+import static com.android.odp.module.common.HttpClientUtils.GZIP_ENCODING_HDR;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.HashMap;
+
+@RunWith(JUnit4.class)
+public final class OdpHttpRequestTest {
+    private static final byte[] PAYLOAD = "non_empty_request_body".getBytes();
+
+    @Test
+    public void testCreateRequestInvalidUri_fails() throws Exception {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        OdpHttpRequest.create(
+                                "http://invalid.com",
+                                HttpClientUtils.HttpMethod.GET,
+                                new HashMap<>(),
+                                PAYLOAD));
+    }
+
+    @Test
+    public void testCreateWithInvalidRequestBody_fails() throws Exception {
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        OdpHttpRequest.create(
+                                "https://valid.com",
+                                HttpClientUtils.HttpMethod.GET,
+                                new HashMap<>(),
+                                PAYLOAD));
+    }
+
+    @Test
+    public void testCreateWithContentLengthHeader_fails() throws Exception {
+        HashMap<String, String> headers = new HashMap<>();
+        headers.put("Content-Length", "1234");
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        OdpHttpRequest.create(
+                                "https://valid.com",
+                                HttpClientUtils.HttpMethod.POST,
+                                headers,
+                                PAYLOAD));
+    }
+
+    @Test
+    public void createGetRequest_valid() throws Exception {
+        String expectedUri = "https://valid.com";
+        OdpHttpRequest request =
+                OdpHttpRequest.create(
+                        expectedUri,
+                        HttpClientUtils.HttpMethod.GET,
+                        new HashMap<>(),
+                        HttpClientUtils.EMPTY_BODY);
+
+        assertThat(request.getUri()).isEqualTo(expectedUri);
+        assertThat(request.getHttpMethod()).isEqualTo(HttpClientUtils.HttpMethod.GET);
+        assertThat(request.getBody()).isEqualTo(HttpClientUtils.EMPTY_BODY);
+        assertTrue(request.getExtraHeaders().isEmpty());
+    }
+
+    @Test
+    public void createGetRequestWithHeader_valid() throws Exception {
+        String expectedUri = "https://valid.com";
+        HashMap<String, String> expectedHeaders = new HashMap<>();
+        expectedHeaders.put("Foo", "Bar");
+
+        OdpHttpRequest request =
+                OdpHttpRequest.create(
+                        expectedUri,
+                        HttpClientUtils.HttpMethod.GET,
+                        expectedHeaders,
+                        HttpClientUtils.EMPTY_BODY);
+
+        assertThat(request.getUri()).isEqualTo(expectedUri);
+        assertThat(request.getExtraHeaders()).isEqualTo(expectedHeaders);
+    }
+
+    @Test
+    public void createPostRequestWithoutBody_valid() {
+        String expectedUri = "https://valid.com";
+
+        OdpHttpRequest request =
+                OdpHttpRequest.create(
+                        expectedUri,
+                        HttpClientUtils.HttpMethod.POST,
+                        new HashMap<>(),
+                        HttpClientUtils.EMPTY_BODY);
+
+        assertThat(request.getUri()).isEqualTo(expectedUri);
+        assertTrue(request.getExtraHeaders().isEmpty());
+        assertThat(request.getHttpMethod()).isEqualTo(HttpClientUtils.HttpMethod.POST);
+        assertThat(request.getBody()).isEqualTo(HttpClientUtils.EMPTY_BODY);
+    }
+
+    @Test
+    public void createPostRequestWithBody_valid() {
+        String expectedUri = "https://valid.com";
+
+        OdpHttpRequest request =
+                OdpHttpRequest.create(
+                        expectedUri, HttpClientUtils.HttpMethod.POST, new HashMap<>(), PAYLOAD);
+
+        assertThat(request.getUri()).isEqualTo(expectedUri);
+        assertThat(request.getHttpMethod()).isEqualTo(HttpClientUtils.HttpMethod.POST);
+        assertThat(request.getBody()).isEqualTo(PAYLOAD);
+    }
+
+    @Test
+    public void createPostRequestWithBodyHeader_valid() {
+        String expectedUri = "https://valid.com";
+        HashMap<String, String> expectedHeaders = new HashMap<>();
+        expectedHeaders.put("Foo", "Bar");
+
+        OdpHttpRequest request =
+                OdpHttpRequest.create(
+                        expectedUri, HttpClientUtils.HttpMethod.POST, expectedHeaders, PAYLOAD);
+
+        assertThat(request.getUri()).isEqualTo(expectedUri);
+        assertThat(request.getHttpMethod()).isEqualTo(HttpClientUtils.HttpMethod.POST);
+        assertThat(request.getBody()).isEqualTo(PAYLOAD);
+        assertThat(request.getExtraHeaders()).isEqualTo(expectedHeaders);
+    }
+
+    @Test
+    public void createGetRequestWithAcceptCompression_valid() {
+        String expectedUri = "https://valid.com";
+        HashMap<String, String> headerList = new HashMap<>();
+        headerList.put(ACCEPT_ENCODING_HDR, GZIP_ENCODING_HDR);
+        OdpHttpRequest request =
+                OdpHttpRequest.create(
+                        expectedUri, HttpClientUtils.HttpMethod.POST, headerList, PAYLOAD);
+
+        assertThat(request.getUri()).isEqualTo(expectedUri);
+        assertThat(request.getHttpMethod()).isEqualTo(HttpClientUtils.HttpMethod.POST);
+        HashMap<String, String> expectedHeaders = new HashMap<>();
+        expectedHeaders.put(HttpClientUtils.CONTENT_LENGTH_HDR, String.valueOf(22));
+        expectedHeaders.put(ACCEPT_ENCODING_HDR, GZIP_ENCODING_HDR);
+        assertThat(request.getExtraHeaders()).isEqualTo(expectedHeaders);
+    }
+}
diff --git a/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/FederatedComputeHttpResponseTest.java b/tests/commontests/src/com/android/odp/module/common/OdpHttpResponseTest.java
similarity index 75%
rename from tests/federatedcomputetests/src/com/android/federatedcompute/services/http/FederatedComputeHttpResponseTest.java
rename to tests/commontests/src/com/android/odp/module/common/OdpHttpResponseTest.java
index d993663..cdccbd5 100644
--- a/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/FederatedComputeHttpResponseTest.java
+++ b/tests/commontests/src/com/android/odp/module/common/OdpHttpResponseTest.java
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.federatedcompute.services.http;
+package com.android.odp.module.common;
 
-import static com.android.federatedcompute.services.http.HttpClientUtil.OCTET_STREAM;
+import static com.android.odp.module.common.HttpClientUtils.OCTET_STREAM;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -36,7 +36,7 @@
 import java.util.Map;
 
 @RunWith(JUnit4.class)
-public final class FederatedComputeHttpResponseTest {
+public final class OdpHttpResponseTest {
     @Test
     public void testBuildWithAllValues() {
         final int responseCode = 200;
@@ -48,8 +48,8 @@
                         "api-key",
                         ImmutableList.of("xyz"));
 
-        FederatedComputeHttpResponse response =
-                new FederatedComputeHttpResponse.Builder()
+        OdpHttpResponse response =
+                new OdpHttpResponse.Builder()
                         .setStatusCode(responseCode)
                         .setPayload(payload)
                         .setHeaders(headers)
@@ -63,8 +63,8 @@
     @Test
     public void testBuildWithMinimalRequiredValues() {
         final int responseCode = 200;
-        FederatedComputeHttpResponse response =
-                new FederatedComputeHttpResponse.Builder().setStatusCode(responseCode).build();
+        OdpHttpResponse response =
+                new OdpHttpResponse.Builder().setStatusCode(responseCode).build();
 
         assertThat(response.getStatusCode()).isEqualTo(responseCode);
     }
@@ -73,20 +73,17 @@
     public void testBuildStatusCodeNull_invalid() {
         assertThrows(
                 IllegalArgumentException.class,
-                () ->
-                        new FederatedComputeHttpResponse.Builder()
-                                .setPayload("payload".getBytes(UTF_8))
-                                .build());
+                () -> new OdpHttpResponse.Builder().setPayload("payload".getBytes(UTF_8)).build());
     }
 
     @Test
     public void testGetBody_success() {
         final byte[] uncompressedBody = "payload".getBytes(UTF_8);
         Map<String, List<String>> expectedHeaders = new HashMap<>();
-        expectedHeaders.put(HttpClientUtil.CONTENT_TYPE_HDR, ImmutableList.of(OCTET_STREAM));
+        expectedHeaders.put(HttpClientUtils.CONTENT_TYPE_HDR, ImmutableList.of(OCTET_STREAM));
 
-        FederatedComputeHttpResponse response =
-                new FederatedComputeHttpResponse.Builder()
+        OdpHttpResponse response =
+                new OdpHttpResponse.Builder()
                         .setStatusCode(200)
                         .setPayload(uncompressedBody)
                         .setHeaders(expectedHeaders)
diff --git a/tests/cts/configtest/AndroidManifest.xml b/tests/cts/configtest/AndroidManifest.xml
index 0b1e262..4b4b731 100644
--- a/tests/cts/configtest/AndroidManifest.xml
+++ b/tests/cts/configtest/AndroidManifest.xml
@@ -16,7 +16,7 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.ondevicepersonalization.cts.configtest">
-    <application android:label="CtsOnDevicePersonalizationConfigTests">
+    <application android:debuggable="true" android:label="CtsOnDevicePersonalizationConfigTests">
         <uses-library android:name="android.test.runner" />
         <activity
             android:name=".TestActivity"
diff --git a/tests/cts/endtoend/Android.bp b/tests/cts/endtoend/Android.bp
index ab2e36c..38b9ae0 100644
--- a/tests/cts/endtoend/Android.bp
+++ b/tests/cts/endtoend/Android.bp
@@ -32,10 +32,13 @@
         "androidx.test.ext.truth",
         "androidx.test.rules",
         "compatibility-device-util-axt",
+        "flag-junit",
         "hamcrest-library",
+        "ondevicepersonalization_flags_lib",
         "ondevicepersonalization-testing-sample-service-api",
         "ondevicepersonalization-testing-utils",
         "platform-test-rules",
+        "platform-compat-test-rules",
     ],
     libs: [
         "sdk_public_33_android.test.base",
diff --git a/tests/cts/endtoend/AndroidManifest.xml b/tests/cts/endtoend/AndroidManifest.xml
index 286223b..832a202 100644
--- a/tests/cts/endtoend/AndroidManifest.xml
+++ b/tests/cts/endtoend/AndroidManifest.xml
@@ -16,8 +16,10 @@
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.ondevicepersonalization.cts.e2e">
-
-    <application android:label="CtsOnDevicePersonalizationE2ETests">
+    <uses-sdk android:minSdkVersion="33"
+              android:targetSdkVersion="33" />
+    <application android:debuggable="true"
+        android:label="CtsOnDevicePersonalizationE2ETests">
         <uses-library android:name="android.test.runner" />
         <activity
             android:name=".TestActivity"
diff --git a/tests/cts/endtoend/AndroidTest.xml b/tests/cts/endtoend/AndroidTest.xml
index 78bc830..5d31241 100644
--- a/tests/cts/endtoend/AndroidTest.xml
+++ b/tests/cts/endtoend/AndroidTest.xml
@@ -19,6 +19,7 @@
     <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
     <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
     <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user_on_secondary_display" />
 
     <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
         <option name="cleanup-apks" value="true"/>
diff --git a/tests/cts/endtoend/src/com/android/ondevicepersonalization/cts/e2e/CtsOdpManagerTests.java b/tests/cts/endtoend/src/com/android/ondevicepersonalization/cts/e2e/CtsOdpManagerTests.java
index d7192bc..03707d4 100644
--- a/tests/cts/endtoend/src/com/android/ondevicepersonalization/cts/e2e/CtsOdpManagerTests.java
+++ b/tests/cts/endtoend/src/com/android/ondevicepersonalization/cts/e2e/CtsOdpManagerTests.java
@@ -15,7 +15,8 @@
  */
 package com.android.ondevicepersonalization.cts.e2e;
 
-import static org.junit.Assert.assertArrayEquals;
+
+import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -23,6 +24,8 @@
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
+import android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceRequest;
+import android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceResponse;
 import android.adservices.ondevicepersonalization.OnDevicePersonalizationException;
 import android.adservices.ondevicepersonalization.OnDevicePersonalizationManager;
 import android.adservices.ondevicepersonalization.OnDevicePersonalizationManager.ExecuteResult;
@@ -32,10 +35,14 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.net.Uri;
 import android.os.PersistableBundle;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.util.Base64;
 
 import androidx.test.core.app.ApplicationProvider;
 
+import com.android.adservices.ondevicepersonalization.flags.Flags;
 import com.android.compatibility.common.util.ShellUtils;
 import com.android.ondevicepersonalization.testing.sampleserviceapi.SampleServiceApi;
 import com.android.ondevicepersonalization.testing.utils.DeviceSupportHelper;
@@ -44,6 +51,7 @@
 import org.junit.After;
 import org.junit.Assume;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -65,11 +73,16 @@
     private static final int LARGE_BLOB_SIZE = 10485760;
     private static final int DELAY_MILLIS = 2000;
 
+    private static final String TEST_POPULATION_NAME = "criteo_app_test_task";
+
     private final Context mContext = ApplicationProvider.getApplicationContext();
 
     @Parameterized.Parameter(0)
     public boolean mIsSipFeatureEnabled;
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Parameterized.Parameters
     public static Collection<Object[]> data() {
         return Arrays.asList(new Object[][] {{true}, {false}});
@@ -225,7 +238,7 @@
                 Executors.newSingleThreadExecutor(),
                 receiver);
         assertNull(receiver.getResult());
-        assertTrue(receiver.getException() instanceof IllegalStateException);
+        assertThat(receiver.getException()).isInstanceOf(IllegalStateException.class);
     }
 
     @Test
@@ -234,15 +247,18 @@
                 mContext.getSystemService(OnDevicePersonalizationManager.class);
         assertNotNull(manager);
         var receiver = new ResultReceiver<ExecuteResult>();
+
         manager.execute(
                 new ComponentName("com.example.odptargetingapp2", "someclass"),
                 PersistableBundle.EMPTY,
                 Executors.newSingleThreadExecutor(),
                 receiver);
+
         assertNull(receiver.getResult());
-        assertTrue(receiver.getException() instanceof NameNotFoundException);
+        assertThat(receiver.getException()).isInstanceOf(NameNotFoundException.class);
     }
 
+
     @Test
     public void testExecuteReturnsClassNotFoundIfServiceClassNotFound()
             throws InterruptedException {
@@ -250,13 +266,15 @@
                 mContext.getSystemService(OnDevicePersonalizationManager.class);
         assertNotNull(manager);
         var receiver = new ResultReceiver<ExecuteResult>();
+
         manager.execute(
                 new ComponentName(SERVICE_PACKAGE, "someclass"),
                 PersistableBundle.EMPTY,
                 Executors.newSingleThreadExecutor(),
                 receiver);
+
         assertNull(receiver.getResult());
-        assertTrue(receiver.getException() instanceof ClassNotFoundException);
+        assertThat(receiver.getException()).isInstanceOf(ClassNotFoundException.class);
     }
 
     @Test
@@ -318,7 +336,7 @@
     }
 
     @Test
-    public void testExecuteWithOutputData() throws InterruptedException {
+    public void testExecuteWithOutputDataDisabled() throws InterruptedException {
         OnDevicePersonalizationManager manager =
                 mContext.getSystemService(OnDevicePersonalizationManager.class);
         assertNotNull(manager);
@@ -334,7 +352,7 @@
                 Executors.newSingleThreadExecutor(),
                 receiver);
         assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
-        assertArrayEquals(new byte[] {'A'}, receiver.getResult().getOutputData());
+        assertThat(receiver.getResult().getOutputData()).isNull();
     }
 
     @Test
@@ -446,7 +464,7 @@
                 receiver);
         assertTrue(receiver.isError());
         assertNull(receiver.getResult());
-        assertTrue(receiver.getException() instanceof OnDevicePersonalizationException);
+        assertThat(receiver.getException()).isInstanceOf(OnDevicePersonalizationException.class);
         assertEquals(
                 ((OnDevicePersonalizationException) receiver.getException()).getErrorCode(),
                 OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_FAILED);
@@ -469,7 +487,7 @@
                 receiver);
         assertTrue(receiver.isError());
         assertNull(receiver.getResult());
-        assertTrue(receiver.getException() instanceof OnDevicePersonalizationException);
+        assertThat(receiver.getException()).isInstanceOf(OnDevicePersonalizationException.class);
         assertEquals(
                 ((OnDevicePersonalizationException) receiver.getException()).getErrorCode(),
                 OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_FAILED);
@@ -720,15 +738,32 @@
                 mContext.getSystemService(OnDevicePersonalizationManager.class);
         assertNotNull(manager);
         var receiver = new ResultReceiver<ExecuteResult>();
-        PersistableBundle appParams = new PersistableBundle();
-        appParams.putString(
-                SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_SCHEDULE_FEDERATED_JOB);
-        appParams.putString(SampleServiceApi.KEY_POPULATION_NAME, "criteo_app_test_task");
+        PersistableBundle appParams = getScheduleFCJobParams(/* useLegacyApi= */ true);
+
         manager.execute(
                 new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS),
                 appParams,
                 Executors.newSingleThreadExecutor(),
                 receiver);
+
+        assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_FCP_SCHEDULE_WITH_OUTCOME_RECEIVER_ENABLED)
+    public void testExecuteWithScheduleFederatedJobWithOutcomeReceiver() throws Exception {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        var receiver = new ResultReceiver<ExecuteResult>();
+        PersistableBundle appParams = getScheduleFCJobParams(/* useLegacyApi= */ false);
+
+        manager.execute(
+                new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS),
+                appParams,
+                Executors.newSingleThreadExecutor(),
+                receiver);
+
         assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
     }
 
@@ -749,4 +784,727 @@
                 receiver);
         assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
     }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceThrowsNPEIfExecutorMissing() {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(
+                                new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                        .setAppParams(PersistableBundle.EMPTY)
+                        .build();
+
+        assertThrows(
+                NullPointerException.class,
+                () -> manager.executeInIsolatedService(request, null, new ResultReceiver<>()));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceThrowsNPEIfReceiverMissing() {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(
+                                new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                        .setAppParams(PersistableBundle.EMPTY)
+                        .build();
+
+        assertThrows(
+                NullPointerException.class,
+                () ->
+                        manager.executeInIsolatedService(
+                                request, Executors.newSingleThreadExecutor(), null));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceThrowsIAEIfPackageNameMissing() {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(new ComponentName("", SERVICE_CLASS))
+                        .setAppParams(PersistableBundle.EMPTY)
+                        .build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        manager.executeInIsolatedService(
+                                request,
+                                Executors.newSingleThreadExecutor(),
+                                new ResultReceiver<>()));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceThrowsIAEIfClassNameMissing()
+            throws InterruptedException {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(new ComponentName(SERVICE_PACKAGE, ""))
+                        .setAppParams(PersistableBundle.EMPTY)
+                        .build();
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        manager.executeInIsolatedService(
+                                request,
+                                Executors.newSingleThreadExecutor(),
+                                new ResultReceiver<>()));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceReturnsIllegalStateIfServiceNotEnrolled()
+            throws InterruptedException {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(
+                                new ComponentName("somepackage", "someclass"))
+                        .setAppParams(PersistableBundle.EMPTY)
+                        .build();
+        var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+
+        manager.executeInIsolatedService(request, Executors.newSingleThreadExecutor(), receiver);
+
+        assertNull(receiver.getResult());
+        assertTrue(receiver.getException() instanceof IllegalStateException);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceReturnsNameNotFoundIfServiceNotInstalled()
+            throws InterruptedException {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(
+                                new ComponentName("com.example.odptargetingapp2", "someclass"))
+                        .setAppParams(PersistableBundle.EMPTY)
+                        .build();
+
+        manager.executeInIsolatedService(request, Executors.newSingleThreadExecutor(), receiver);
+
+        assertNull(receiver.getResult());
+        assertThat(receiver.getException()).isInstanceOf(OnDevicePersonalizationException.class);
+        OnDevicePersonalizationException exception =
+                (OnDevicePersonalizationException) receiver.getException();
+        assertThat(exception.getErrorCode())
+                .isEqualTo(
+                        OnDevicePersonalizationException
+                                .ERROR_ISOLATED_SERVICE_MANIFEST_PARSING_FAILED);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceReturnsManifestParsingErrorIfServiceClassNotFound()
+            throws InterruptedException {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(
+                                new ComponentName(SERVICE_PACKAGE, "someclass"))
+                        .setAppParams(PersistableBundle.EMPTY)
+                        .build();
+
+        manager.executeInIsolatedService(request, Executors.newSingleThreadExecutor(), receiver);
+
+        assertNull(receiver.getResult());
+        assertThat(receiver.getException()).isInstanceOf(OnDevicePersonalizationException.class);
+        OnDevicePersonalizationException exception =
+                (OnDevicePersonalizationException) receiver.getException();
+        assertThat(exception.getErrorCode())
+                .isEqualTo(
+                        OnDevicePersonalizationException
+                                .ERROR_ISOLATED_SERVICE_MANIFEST_PARSING_FAILED);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceNoOp() throws InterruptedException {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(
+                                new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                        .setAppParams(PersistableBundle.EMPTY)
+                        .build();
+
+        manager.executeInIsolatedService(request, Executors.newSingleThreadExecutor(), receiver);
+
+        assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+        SurfacePackageToken token = receiver.getResult().getSurfacePackageToken();
+        assertNull(token);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceWithRenderAndLogging() throws InterruptedException {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+        PersistableBundle appParams = new PersistableBundle();
+        appParams.putString(SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_RENDER_AND_LOG);
+        appParams.putString(SampleServiceApi.KEY_RENDERING_CONFIG_IDS, "id1");
+        PersistableBundle logData = new PersistableBundle();
+        logData.putString("id", "a1");
+        logData.putDouble("val", 5.0);
+        appParams.putPersistableBundle(SampleServiceApi.KEY_LOG_DATA, logData);
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(
+                                new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                        .setAppParams(appParams)
+                        .build();
+
+        manager.executeInIsolatedService(request, Executors.newSingleThreadExecutor(), receiver);
+
+        assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+        SurfacePackageToken token = receiver.getResult().getSurfacePackageToken();
+        assertNotNull(token);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceWithRender() throws InterruptedException {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+        PersistableBundle appParams = new PersistableBundle();
+        appParams.putString(SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_RENDER_AND_LOG);
+        appParams.putString(SampleServiceApi.KEY_RENDERING_CONFIG_IDS, "id1");
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(
+                                new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                        .setAppParams(appParams)
+                        .build();
+
+        manager.executeInIsolatedService(request, Executors.newSingleThreadExecutor(), receiver);
+
+        assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+        SurfacePackageToken token = receiver.getResult().getSurfacePackageToken();
+        assertNotNull(token);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceReadRemoteData() throws InterruptedException {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+        PersistableBundle appParams = new PersistableBundle();
+        appParams.putString(SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_READ_REMOTE_DATA);
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(
+                                new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                        .setAppParams(appParams)
+                        .build();
+
+        manager.executeInIsolatedService(request, Executors.newSingleThreadExecutor(), receiver);
+
+        assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceReadUserData() throws InterruptedException {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+        PersistableBundle appParams = new PersistableBundle();
+        appParams.putString(SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_READ_USER_DATA);
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(
+                                new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                        .setAppParams(appParams)
+                        .build();
+
+        manager.executeInIsolatedService(request, Executors.newSingleThreadExecutor(), receiver);
+
+        assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceWithLogging() throws InterruptedException {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+        PersistableBundle appParams = new PersistableBundle();
+        appParams.putString(SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_RENDER_AND_LOG);
+        PersistableBundle logData = new PersistableBundle();
+        logData.putString("id", "a1");
+        logData.putDouble("val", 5.0);
+        appParams.putPersistableBundle(SampleServiceApi.KEY_LOG_DATA, logData);
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(
+                                new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                        .setAppParams(appParams)
+                        .build();
+
+        manager.executeInIsolatedService(request, Executors.newSingleThreadExecutor(), receiver);
+        assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+        SurfacePackageToken token = receiver.getResult().getSurfacePackageToken();
+        assertNull(token);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceReadLog() throws InterruptedException {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        final long now = System.currentTimeMillis();
+
+        {
+            var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+            PersistableBundle appParams = new PersistableBundle();
+            appParams.putString(
+                    SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_RENDER_AND_LOG);
+            PersistableBundle logData = new PersistableBundle();
+            logData.putLong(SampleServiceApi.KEY_EXPECTED_LOG_DATA_KEY, now);
+            appParams.putPersistableBundle(SampleServiceApi.KEY_LOG_DATA, logData);
+            ExecuteInIsolatedServiceRequest request =
+                    new ExecuteInIsolatedServiceRequest.Builder(
+                                    new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                            .setAppParams(appParams)
+                            .build();
+
+            manager.executeInIsolatedService(
+                    request, Executors.newSingleThreadExecutor(), receiver);
+            assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+        }
+
+        // Add delay between writing and read from db to reduce flakiness.
+        Thread.sleep(DELAY_MILLIS);
+
+        {
+            var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+            PersistableBundle appParams = new PersistableBundle();
+            appParams.putString(SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_READ_LOG);
+            appParams.putLong(SampleServiceApi.KEY_EXPECTED_LOG_DATA_VALUE, now);
+            ExecuteInIsolatedServiceRequest request =
+                    new ExecuteInIsolatedServiceRequest.Builder(
+                                    new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                            .setAppParams(appParams)
+                            .build();
+
+            manager.executeInIsolatedService(
+                    request, Executors.newSingleThreadExecutor(), receiver);
+            assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceReturnsErrorIfServiceThrows()
+            throws InterruptedException {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+        PersistableBundle appParams = new PersistableBundle();
+        appParams.putString(SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_THROW_EXCEPTION);
+        appParams.putString(SampleServiceApi.KEY_EXCEPTION_CLASS, "java.lang.NullPointerException");
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(
+                                new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                        .setAppParams(appParams)
+                        .build();
+
+        manager.executeInIsolatedService(request, Executors.newSingleThreadExecutor(), receiver);
+        assertTrue(receiver.isError());
+        assertNull(receiver.getResult());
+        assertTrue(receiver.getException() instanceof OnDevicePersonalizationException);
+        assertEquals(
+                ((OnDevicePersonalizationException) receiver.getException()).getErrorCode(),
+                OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_FAILED);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceReturnsErrorIfServiceReturnsError()
+            throws InterruptedException {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+        PersistableBundle appParams = new PersistableBundle();
+        appParams.putString(
+                SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_FAIL_WITH_ERROR_CODE);
+        appParams.putInt(SampleServiceApi.KEY_ERROR_CODE, 10);
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(
+                                new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                        .setAppParams(appParams)
+                        .build();
+
+        manager.executeInIsolatedService(request, Executors.newSingleThreadExecutor(), receiver);
+        assertTrue(receiver.isError());
+        assertNull(receiver.getResult());
+        assertTrue(receiver.getException() instanceof OnDevicePersonalizationException);
+        assertEquals(
+                ((OnDevicePersonalizationException) receiver.getException()).getErrorCode(),
+                OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_FAILED);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceWriteAndReadLocalData() throws InterruptedException {
+        final String tableKey = "testKey_" + System.currentTimeMillis();
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+
+        // Write 1 byte.
+        {
+            var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+            PersistableBundle appParams = new PersistableBundle();
+            appParams.putString(
+                    SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_WRITE_LOCAL_DATA);
+            appParams.putString(SampleServiceApi.KEY_TABLE_KEY, tableKey);
+            appParams.putString(
+                    SampleServiceApi.KEY_BASE64_VALUE, Base64.encodeToString(new byte[] {'A'}, 0));
+            ExecuteInIsolatedServiceRequest request =
+                    new ExecuteInIsolatedServiceRequest.Builder(
+                                    new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                            .setAppParams(appParams)
+                            .build();
+
+            manager.executeInIsolatedService(
+                    request, Executors.newSingleThreadExecutor(), receiver);
+
+            assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+        }
+
+        // Add delay between writing and read from db to reduce flakiness.
+        Thread.sleep(DELAY_MILLIS);
+
+        // Read and check whether value matches written value.
+        {
+            var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+            PersistableBundle appParams = new PersistableBundle();
+            appParams.putString(
+                    SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_READ_LOCAL_DATA);
+            appParams.putString(SampleServiceApi.KEY_TABLE_KEY, tableKey);
+            appParams.putString(
+                    SampleServiceApi.KEY_BASE64_VALUE, Base64.encodeToString(new byte[] {'A'}, 0));
+            ExecuteInIsolatedServiceRequest request =
+                    new ExecuteInIsolatedServiceRequest.Builder(
+                                    new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                            .setAppParams(appParams)
+                            .build();
+
+            manager.executeInIsolatedService(
+                    request, Executors.newSingleThreadExecutor(), receiver);
+
+            assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+        }
+
+        // Add delay between writing and read from db to reduce flakiness.
+        Thread.sleep(DELAY_MILLIS);
+
+        // Remove.
+        {
+            var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+            PersistableBundle appParams = new PersistableBundle();
+            appParams.putString(
+                    SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_WRITE_LOCAL_DATA);
+            appParams.putString(SampleServiceApi.KEY_TABLE_KEY, tableKey);
+            ExecuteInIsolatedServiceRequest request =
+                    new ExecuteInIsolatedServiceRequest.Builder(
+                                    new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                            .setAppParams(appParams)
+                            .build();
+
+            manager.executeInIsolatedService(
+                    request, Executors.newSingleThreadExecutor(), receiver);
+
+            assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+        }
+
+        // Add delay between writing and read from db to reduce flakiness.
+        Thread.sleep(DELAY_MILLIS);
+
+        // Read and check whether value was removed.
+        {
+            var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+            PersistableBundle appParams = new PersistableBundle();
+            appParams.putString(
+                    SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_READ_LOCAL_DATA);
+            appParams.putString(SampleServiceApi.KEY_TABLE_KEY, tableKey);
+            ExecuteInIsolatedServiceRequest request =
+                    new ExecuteInIsolatedServiceRequest.Builder(
+                                    new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                            .setAppParams(appParams)
+                            .build();
+
+            manager.executeInIsolatedService(
+                    request, Executors.newSingleThreadExecutor(), receiver);
+
+            assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceWriteAndReadLargeLocalData()
+            throws InterruptedException {
+        final String tableKey = "testKey_" + System.currentTimeMillis();
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+
+        // Write 10MB.
+        {
+            var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+            PersistableBundle appParams = new PersistableBundle();
+            appParams.putString(
+                    SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_WRITE_LOCAL_DATA);
+            appParams.putString(SampleServiceApi.KEY_TABLE_KEY, tableKey);
+            appParams.putString(
+                    SampleServiceApi.KEY_BASE64_VALUE, Base64.encodeToString(new byte[] {'A'}, 0));
+            appParams.putInt(SampleServiceApi.KEY_TABLE_VALUE_REPEAT_COUNT, LARGE_BLOB_SIZE);
+            ExecuteInIsolatedServiceRequest request =
+                    new ExecuteInIsolatedServiceRequest.Builder(
+                                    new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                            .setAppParams(appParams)
+                            .build();
+
+            manager.executeInIsolatedService(
+                    request, Executors.newSingleThreadExecutor(), receiver);
+
+            assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+        }
+
+        // Add delay between writing and read from db to reduce flakiness.
+        Thread.sleep(DELAY_MILLIS);
+
+        // Read and check whether value matches written value.
+        {
+            var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+            PersistableBundle appParams = new PersistableBundle();
+            appParams.putString(
+                    SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_READ_LOCAL_DATA);
+            appParams.putString(SampleServiceApi.KEY_TABLE_KEY, tableKey);
+            appParams.putString(
+                    SampleServiceApi.KEY_BASE64_VALUE, Base64.encodeToString(new byte[] {'A'}, 0));
+            appParams.putInt(SampleServiceApi.KEY_TABLE_VALUE_REPEAT_COUNT, LARGE_BLOB_SIZE);
+            ExecuteInIsolatedServiceRequest request =
+                    new ExecuteInIsolatedServiceRequest.Builder(
+                                    new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                            .setAppParams(appParams)
+                            .build();
+
+            manager.executeInIsolatedService(
+                    request, Executors.newSingleThreadExecutor(), receiver);
+
+            assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+        }
+
+        // Add delay between writing and read from db to reduce flakiness.
+        Thread.sleep(DELAY_MILLIS);
+
+        // Remove.
+        {
+            var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+            PersistableBundle appParams = new PersistableBundle();
+            appParams.putString(
+                    SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_WRITE_LOCAL_DATA);
+            appParams.putString(SampleServiceApi.KEY_TABLE_KEY, tableKey);
+            ExecuteInIsolatedServiceRequest request =
+                    new ExecuteInIsolatedServiceRequest.Builder(
+                                    new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                            .setAppParams(appParams)
+                            .build();
+
+            manager.executeInIsolatedService(
+                    request, Executors.newSingleThreadExecutor(), receiver);
+
+            assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+        }
+
+        // Add delay between writing and read from db to reduce flakiness.
+        Thread.sleep(DELAY_MILLIS);
+
+        // Read and check whether value was removed.
+        {
+            var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+            PersistableBundle appParams = new PersistableBundle();
+            appParams.putString(
+                    SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_READ_LOCAL_DATA);
+            appParams.putString(SampleServiceApi.KEY_TABLE_KEY, tableKey);
+            ExecuteInIsolatedServiceRequest request =
+                    new ExecuteInIsolatedServiceRequest.Builder(
+                                    new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                            .setAppParams(appParams)
+                            .build();
+
+            manager.executeInIsolatedService(
+                    request, Executors.newSingleThreadExecutor(), receiver);
+
+            assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceSendLargeBlob() throws InterruptedException {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+        PersistableBundle appParams = new PersistableBundle();
+        appParams.putString(
+                SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_CHECK_VALUE_LENGTH);
+        byte[] buffer = new byte[LARGE_BLOB_SIZE];
+        for (int i = 0; i < LARGE_BLOB_SIZE; ++i) {
+            buffer[i] = 'A';
+        }
+        appParams.putString(SampleServiceApi.KEY_BASE64_VALUE, Base64.encodeToString(buffer, 0));
+        appParams.putInt(SampleServiceApi.KEY_VALUE_LENGTH, LARGE_BLOB_SIZE);
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(
+                                new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                        .setAppParams(appParams)
+                        .build();
+
+        manager.executeInIsolatedService(request, Executors.newSingleThreadExecutor(), receiver);
+        assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceWithModelInference() throws Exception {
+        final String tableKey = "model_" + System.currentTimeMillis();
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        Uri modelUri =
+                Uri.parse(
+                        "android.resource://"
+                                + ApplicationProvider.getApplicationContext().getPackageName()
+                                + "/raw/model");
+        Context context = ApplicationProvider.getApplicationContext();
+        InputStream in = context.getContentResolver().openInputStream(modelUri);
+        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+        byte[] buf = new byte[4096];
+        int bytesRead;
+        while ((bytesRead = in.read(buf)) != -1) {
+            outputStream.write(buf, 0, bytesRead);
+        }
+        byte[] buffer = outputStream.toByteArray();
+        outputStream.close();
+        // Write model to local data.
+        {
+            var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+            PersistableBundle appParams = new PersistableBundle();
+            appParams.putString(
+                    SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_WRITE_LOCAL_DATA);
+            appParams.putString(SampleServiceApi.KEY_TABLE_KEY, tableKey);
+            appParams.putString(
+                    SampleServiceApi.KEY_BASE64_VALUE, Base64.encodeToString(buffer, 0));
+            ExecuteInIsolatedServiceRequest request =
+                    new ExecuteInIsolatedServiceRequest.Builder(
+                                    new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                            .setAppParams(appParams)
+                            .build();
+
+            manager.executeInIsolatedService(
+                    request, Executors.newSingleThreadExecutor(), receiver);
+
+            assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+        }
+
+        // Add delay between writing and read from db to reduce flakiness.
+        Thread.sleep(DELAY_MILLIS);
+
+        // Run model inference
+        {
+            var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+            PersistableBundle appParams = new PersistableBundle();
+            appParams.putString(
+                    SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_RUN_MODEL_INFERENCE);
+            appParams.putString(SampleServiceApi.KEY_TABLE_KEY, tableKey);
+            appParams.putDouble(SampleServiceApi.KEY_INFERENCE_RESULT, 0.5922908);
+            ExecuteInIsolatedServiceRequest request =
+                    new ExecuteInIsolatedServiceRequest.Builder(
+                                    new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                            .setAppParams(appParams)
+                            .build();
+
+            manager.executeInIsolatedService(
+                    request, Executors.newSingleThreadExecutor(), receiver);
+
+            assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+        }
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceWithScheduleFederatedJob() throws Exception {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(
+                                new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                        .setAppParams(getScheduleFCJobParams(/* useLegacyApi= */ true))
+                        .build();
+
+        manager.executeInIsolatedService(request, Executors.newSingleThreadExecutor(), receiver);
+        assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceWithCancelFederatedJob() throws Exception {
+        OnDevicePersonalizationManager manager =
+                mContext.getSystemService(OnDevicePersonalizationManager.class);
+        assertNotNull(manager);
+        var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+        PersistableBundle appParams = new PersistableBundle();
+        appParams.putString(
+                SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_CANCEL_FEDERATED_JOB);
+        appParams.putString(SampleServiceApi.KEY_POPULATION_NAME, "criteo_app_test_task");
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(
+                                new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS))
+                        .setAppParams(appParams)
+                        .build();
+
+        manager.executeInIsolatedService(request, Executors.newSingleThreadExecutor(), receiver);
+        assertTrue(receiver.getErrorMessage(), receiver.isSuccess());
+    }
+
+    private static PersistableBundle getScheduleFCJobParams(boolean useLegacyApi) {
+        PersistableBundle appParams = new PersistableBundle();
+        appParams.putString(
+                SampleServiceApi.KEY_OPCODE,
+                useLegacyApi
+                        ? SampleServiceApi.OPCODE_SCHEDULE_FEDERATED_JOB
+                        : SampleServiceApi.OPCODE_SCHEDULE_FEDERATED_JOB_V2);
+        appParams.putString(SampleServiceApi.KEY_POPULATION_NAME, TEST_POPULATION_NAME);
+        return appParams;
+    }
 }
diff --git a/tests/cts/endtoend/src/com/android/ondevicepersonalization/cts/e2e/DataClassesTest.java b/tests/cts/endtoend/src/com/android/ondevicepersonalization/cts/e2e/DataClassesTest.java
index 323f478..89a8ae6 100644
--- a/tests/cts/endtoend/src/com/android/ondevicepersonalization/cts/e2e/DataClassesTest.java
+++ b/tests/cts/endtoend/src/com/android/ondevicepersonalization/cts/e2e/DataClassesTest.java
@@ -16,21 +16,28 @@
 
 package com.android.ondevicepersonalization.cts.e2e;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import android.adservices.ondevicepersonalization.AppInfo;
 import android.adservices.ondevicepersonalization.DownloadCompletedOutput;
 import android.adservices.ondevicepersonalization.EventLogRecord;
 import android.adservices.ondevicepersonalization.EventOutput;
+import android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceRequest;
+import android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceResponse;
 import android.adservices.ondevicepersonalization.ExecuteOutput;
 import android.adservices.ondevicepersonalization.FederatedComputeInput;
+import android.adservices.ondevicepersonalization.FederatedComputeScheduleRequest;
 import android.adservices.ondevicepersonalization.FederatedComputeScheduler;
 import android.adservices.ondevicepersonalization.IsolatedServiceException;
 import android.adservices.ondevicepersonalization.MeasurementWebTriggerEventParams;
 import android.adservices.ondevicepersonalization.RenderOutput;
 import android.adservices.ondevicepersonalization.RenderingConfig;
 import android.adservices.ondevicepersonalization.RequestLogRecord;
+import android.adservices.ondevicepersonalization.SurfacePackageToken;
 import android.adservices.ondevicepersonalization.TrainingExampleRecord;
 import android.adservices.ondevicepersonalization.TrainingExamplesOutput;
 import android.adservices.ondevicepersonalization.TrainingInterval;
@@ -39,10 +46,17 @@
 import android.content.ContentValues;
 import android.net.Uri;
 import android.os.PersistableBundle;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.adservices.ondevicepersonalization.flags.Flags;
+import com.android.ondevicepersonalization.testing.sampleserviceapi.SampleServiceApi;
+
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -56,6 +70,14 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class DataClassesTest {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    private static final String SERVICE_PACKAGE =
+            "com.android.ondevicepersonalization.testing.sampleservice";
+    private static final String SERVICE_CLASS =
+            "com.android.ondevicepersonalization.testing.sampleservice.SampleService";
+
     /**
      * Test builder and getters for ExecuteOutput.
      */
@@ -65,17 +87,16 @@
         row.put("a", 5);
         ExecuteOutput data =
                 new ExecuteOutput.Builder()
-                    .setRequestLogRecord(new RequestLogRecord.Builder().addRow(row).build())
-                    .setRenderingConfig(new RenderingConfig.Builder().addKey("abc").build())
-                    .addEventLogRecord(new EventLogRecord.Builder().setType(1).build())
-                    .setOutputData(new byte[]{1})
-                    .build();
+                        .setRequestLogRecord(new RequestLogRecord.Builder().addRow(row).build())
+                        .setRenderingConfig(new RenderingConfig.Builder().addKey("abc").build())
+                        .addEventLogRecord(new EventLogRecord.Builder().setType(1).build())
+                        .build();
 
         assertEquals(
                 5, data.getRequestLogRecord().getRows().get(0).getAsInteger("a").intValue());
         assertEquals("abc", data.getRenderingConfig().getKeys().get(0));
         assertEquals(1, data.getEventLogRecords().get(0).getType());
-        assertArrayEquals(new byte[]{1}, data.getOutputData());
+        assertThat(data.getOutputData()).isNull();
     }
 
     /**
@@ -179,6 +200,29 @@
                 params.getTrainingInterval().getSchedulingMode());
     }
 
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_FCP_SCHEDULE_WITH_OUTCOME_RECEIVER_ENABLED)
+    public void testFederatedComputeSchedulerRequest() {
+        // Test for Data classes associated with FederatedComputeScheduler's schedule API.
+        String testPopulation = "testPopulation";
+        Duration testInterval = Duration.ofSeconds(5);
+        int testSchedulingMode = TrainingInterval.SCHEDULING_MODE_RECURRENT;
+        TrainingInterval testData =
+                new TrainingInterval.Builder()
+                        .setSchedulingMode(testSchedulingMode)
+                        .setMinimumInterval(testInterval)
+                        .build();
+
+        FederatedComputeScheduler.Params params = new FederatedComputeScheduler.Params(testData);
+        FederatedComputeScheduleRequest request =
+                new FederatedComputeScheduleRequest(params, testPopulation);
+
+        assertEquals(testPopulation, request.getPopulationName());
+        assertEquals(testInterval, request.getParams().getTrainingInterval().getMinimumInterval());
+        assertEquals(
+                testSchedulingMode, request.getParams().getTrainingInterval().getSchedulingMode());
+    }
+
     /** Test for RequestLogRecord class. */
     @Test
     public void testRequestLogRecord() {
@@ -297,7 +341,106 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DATA_CLASS_MISSING_CTORS_AND_GETTERS_ENABLED)
     public void testIsolatedServiceException() {
-        assertEquals(42, new IsolatedServiceException(42).getErrorCode());
+        IsolatedServiceException e = new IsolatedServiceException(42);
+        assertEquals(42, e.getErrorCode());
+
+        e = new IsolatedServiceException(42, new NullPointerException());
+        assertEquals(42, e.getErrorCode());
+        assertTrue(e.getCause() instanceof NullPointerException);
+
+        e = new IsolatedServiceException(42, "errr", new NullPointerException());
+        assertEquals(42, e.getErrorCode());
+        assertEquals("errr", e.getMessage());
+        assertTrue(e.getCause() instanceof NullPointerException);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_DATA_CLASS_MISSING_CTORS_AND_GETTERS_ENABLED)
+    public void testAppInfo() {
+        AppInfo appInfo = new AppInfo(true);
+        assertThat(appInfo.isInstalled()).isTrue();
+
+        appInfo = new AppInfo(false);
+        assertThat(appInfo.isInstalled()).isFalse();
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceRequest() {
+        ComponentName service = new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS);
+        PersistableBundle appParams = new PersistableBundle();
+        appParams.putString(SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_RENDER_AND_LOG);
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(service)
+                        .setAppParams(appParams)
+                        .setOutputSpec(
+                                ExecuteInIsolatedServiceRequest.OutputSpec.buildBestValueSpec(100))
+                        .build();
+
+        assertThat(request.getService()).isEqualTo(service);
+        assertThat(request.getAppParams()).isEqualTo(appParams);
+        assertThat(request.getOutputSpec().getMaxIntValue()).isEqualTo(100);
+        assertThat(request.getOutputSpec().getOutputType())
+                .isEqualTo(ExecuteInIsolatedServiceRequest.OutputSpec.OUTPUT_TYPE_BEST_VALUE);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceRequest_nullOutputSpec() {
+        ComponentName service = new ComponentName(SERVICE_PACKAGE, SERVICE_CLASS);
+        PersistableBundle appParams = new PersistableBundle();
+        appParams.putString(SampleServiceApi.KEY_OPCODE, SampleServiceApi.OPCODE_RENDER_AND_LOG);
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(service)
+                        .setAppParams(appParams)
+                        .build();
+
+        assertThat(request.getService()).isEqualTo(service);
+        assertThat(request.getAppParams()).isEqualTo(appParams);
+        assertThat(request.getOutputSpec().getMaxIntValue()).isEqualTo(-1);
+        assertThat(request.getOutputSpec().getOutputType())
+                .isEqualTo(ExecuteInIsolatedServiceRequest.OutputSpec.OUTPUT_TYPE_NULL);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceResponse() {
+        SurfacePackageToken token = new SurfacePackageToken("token");
+        ExecuteInIsolatedServiceResponse response = new ExecuteInIsolatedServiceResponse(token, 10);
+
+        assertThat(response.getBestValue()).isEqualTo(10);
+        assertThat(response.getSurfacePackageToken()).isEqualTo(token);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteInIsolatedServiceResponse_nullBestValue() {
+        SurfacePackageToken token = new SurfacePackageToken("token");
+        ExecuteInIsolatedServiceResponse response = new ExecuteInIsolatedServiceResponse(token);
+
+        assertThat(response.getBestValue()).isEqualTo(-1);
+        assertThat(response.getSurfacePackageToken()).isEqualTo(token);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_EXECUTE_IN_ISOLATED_SERVICE_API_ENABLED)
+    public void testExecuteOutputWithBestValue() {
+        ContentValues row = new ContentValues();
+        row.put("a", 5);
+        ExecuteOutput data =
+                new ExecuteOutput.Builder()
+                        .setRequestLogRecord(new RequestLogRecord.Builder().addRow(row).build())
+                        .setRenderingConfig(new RenderingConfig.Builder().addKey("abc").build())
+                        .addEventLogRecord(new EventLogRecord.Builder().setType(1).build())
+                        .setBestValue(100)
+                        .build();
+
+        assertEquals(5, data.getRequestLogRecord().getRows().get(0).getAsInteger("a").intValue());
+        assertEquals("abc", data.getRenderingConfig().getKeys().get(0));
+        assertEquals(1, data.getEventLogRecords().get(0).getType());
+        assertThat(data.getOutputData()).isNull();
+        assertThat(data.getBestValue()).isEqualTo(100);
     }
 }
diff --git a/tests/cts/endtoend/src/com/android/ondevicepersonalization/cts/e2e/IsolatedWorkerTest.java b/tests/cts/endtoend/src/com/android/ondevicepersonalization/cts/e2e/IsolatedWorkerTest.java
index 265e788..560716f 100644
--- a/tests/cts/endtoend/src/com/android/ondevicepersonalization/cts/e2e/IsolatedWorkerTest.java
+++ b/tests/cts/endtoend/src/com/android/ondevicepersonalization/cts/e2e/IsolatedWorkerTest.java
@@ -29,7 +29,6 @@
 import android.adservices.ondevicepersonalization.EventLogRecord;
 import android.adservices.ondevicepersonalization.EventOutput;
 import android.adservices.ondevicepersonalization.ExecuteInput;
-import android.adservices.ondevicepersonalization.ExecuteInputParcel;
 import android.adservices.ondevicepersonalization.ExecuteOutput;
 import android.adservices.ondevicepersonalization.IsolatedServiceException;
 import android.adservices.ondevicepersonalization.IsolatedWorker;
@@ -46,13 +45,16 @@
 import android.net.Uri;
 import android.os.OutcomeReceiver;
 import android.os.PersistableBundle;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
-import com.android.ondevicepersonalization.internal.util.ByteArrayParceledSlice;
-import com.android.ondevicepersonalization.internal.util.PersistableBundleUtils;
+import com.android.adservices.ondevicepersonalization.flags.Flags;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -65,20 +67,18 @@
  */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@RequiresFlagsEnabled(Flags.FLAG_DATA_CLASS_MISSING_CTORS_AND_GETTERS_ENABLED)
 public class IsolatedWorkerTest {
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     @Test
     public void testOnExecute() throws Exception {
         IsolatedWorker worker = new TestWorker();
         WorkerResultReceiver<ExecuteOutput> receiver = new WorkerResultReceiver<>();
         PersistableBundle bundle = new PersistableBundle();
         bundle.putString("x", "y");
-        ByteArrayParceledSlice slice = new ByteArrayParceledSlice(
-                PersistableBundleUtils.toByteArray(bundle));
-        ExecuteInputParcel inputParcel = new ExecuteInputParcel.Builder()
-                .setAppPackageName("com.example.app")
-                .setSerializedAppParams(slice)
-                .build();
-        worker.onExecute(new ExecuteInput(inputParcel), receiver);
+        worker.onExecute(new ExecuteInput("com.example.app", bundle), receiver);
     }
 
     @Test
@@ -97,8 +97,7 @@
         WorkerResultReceiver<DownloadCompletedOutput> receiver = new WorkerResultReceiver<>();
         TestKeyValueStore store = new TestKeyValueStore(
                 Map.of("a", new byte[]{'A'}, "b", new byte[]{'B'}));
-        worker.onDownloadCompleted(
-                new DownloadCompletedInput.Builder(store).build(), receiver);
+        worker.onDownloadCompleted(new DownloadCompletedInput(store), receiver);
         assertThat(receiver.mResult.getRetainedKeys(), containsInAnyOrder("a", "b"));
     }
 
diff --git a/tests/cts/endtoend/src/com/android/ondevicepersonalization/cts/e2e/RequestSurfacePackageTests.java b/tests/cts/endtoend/src/com/android/ondevicepersonalization/cts/e2e/RequestSurfacePackageTests.java
index 67fee65..e010c62 100644
--- a/tests/cts/endtoend/src/com/android/ondevicepersonalization/cts/e2e/RequestSurfacePackageTests.java
+++ b/tests/cts/endtoend/src/com/android/ondevicepersonalization/cts/e2e/RequestSurfacePackageTests.java
@@ -171,7 +171,7 @@
                 clickableLink.click();
 
                 // Retry if unable to click on the link.
-                Thread.sleep(5 * 1000);
+                Thread.sleep(2500);
 
                 surfacePackage.release();
                 mDevice.pressHome();
diff --git a/tests/cts/service/src/com/android/ondevicepersonalization/testing/sampleservice/SampleWorker.java b/tests/cts/service/src/com/android/ondevicepersonalization/testing/sampleservice/SampleWorker.java
index 01b5d16..6971661 100644
--- a/tests/cts/service/src/com/android/ondevicepersonalization/testing/sampleservice/SampleWorker.java
+++ b/tests/cts/service/src/com/android/ondevicepersonalization/testing/sampleservice/SampleWorker.java
@@ -21,6 +21,8 @@
 import android.adservices.ondevicepersonalization.ExecuteInput;
 import android.adservices.ondevicepersonalization.ExecuteOutput;
 import android.adservices.ondevicepersonalization.FederatedComputeInput;
+import android.adservices.ondevicepersonalization.FederatedComputeScheduleRequest;
+import android.adservices.ondevicepersonalization.FederatedComputeScheduleResponse;
 import android.adservices.ondevicepersonalization.FederatedComputeScheduler;
 import android.adservices.ondevicepersonalization.InferenceInput;
 import android.adservices.ondevicepersonalization.InferenceOutput;
@@ -52,15 +54,19 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 
 class SampleWorker implements IsolatedWorker {
     private static final String TAG = "OdpTestingSampleService";
 
     private static final int ERROR_SAMPLE_SERVICE_FAILED = 1;
+    private static final int SCHEDULE_CALLBACK_TIMEOUT_SECONDS = 5;
 
     private static final String TRANSPARENT_PNG_BASE64 =
             "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAA"
@@ -145,7 +151,9 @@
             } else if (op.equals(SampleServiceApi.OPCODE_READ_LOG)) {
                 result = handleReadLog(appParams);
             } else if (op.equals(SampleServiceApi.OPCODE_SCHEDULE_FEDERATED_JOB)) {
-                result = handleScheduleFederatedJob(appParams);
+                result = handleScheduleFederatedJob(appParams, /* useLegacyScheduleApi= */ true);
+            } else if (op.equals(SampleServiceApi.OPCODE_SCHEDULE_FEDERATED_JOB_V2)) {
+                result = handleScheduleFederatedJob(appParams, /* useLegacyScheduleApi= */ false);
             } else if (op.equals(SampleServiceApi.OPCODE_CANCEL_FEDERATED_JOB)) {
                 result = handleCancelFederatedJob(appParams);
             }
@@ -460,7 +468,8 @@
         receiver.onResult(new RenderOutput.Builder().setContent(html).build());
     }
 
-    private ExecuteOutput handleScheduleFederatedJob(PersistableBundle appParams) {
+    private ExecuteOutput handleScheduleFederatedJob(
+            PersistableBundle appParams, boolean useLegacyScheduleApi) {
         Log.i(TAG, "handleScheduleFederatedJob()");
         String populationName =
                 Objects.requireNonNull(appParams.getString(SampleServiceApi.KEY_POPULATION_NAME));
@@ -472,8 +481,43 @@
                         .setSchedulingMode(TrainingInterval.SCHEDULING_MODE_ONE_TIME)
                         .build();
         FederatedComputeScheduler.Params params = new FederatedComputeScheduler.Params(interval);
-        mFcpScheduler.schedule(params, input);
-        return new ExecuteOutput.Builder().build();
+
+        if (useLegacyScheduleApi) {
+            mFcpScheduler.schedule(params, input);
+            return new ExecuteOutput.Builder().build();
+        }
+
+        // Use new schedule API with outcome-receiver
+        BlockingQueue<Object> asyncResult = new ArrayBlockingQueue<>(1);
+        final Object emptyValue = new Object();
+        FederatedComputeScheduleRequest request =
+                new FederatedComputeScheduleRequest(params, populationName);
+        mFcpScheduler.schedule(
+                request,
+                new OutcomeReceiver<FederatedComputeScheduleResponse, Exception>() {
+                    @Override
+                    public void onResult(FederatedComputeScheduleResponse result) {
+                        Log.e(TAG, "FCP schedule request successful!");
+                        asyncResult.add(result);
+                    }
+
+                    @Override
+                    public void onError(Exception e) {
+                        Log.e(TAG, "FCP schedule request failed: " + e.getMessage());
+                        asyncResult.add(emptyValue);
+                    }
+                });
+
+        // Wait for outcome receiver callback.
+        Object response = null;
+        try {
+            response = asyncResult.poll(SCHEDULE_CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            Log.e(TAG, "Timed out waiting for schedule request to succeed!");
+        }
+        return (response == null || response == emptyValue)
+                ? null
+                : new ExecuteOutput.Builder().build();
     }
 
     private ExecuteOutput handleCancelFederatedJob(PersistableBundle appParams) {
diff --git a/tests/cts/serviceapi/src/com/android/ondevicepersonalization/testing/sampleserviceapi/SampleServiceApi.java b/tests/cts/serviceapi/src/com/android/ondevicepersonalization/testing/sampleserviceapi/SampleServiceApi.java
index 72004ba..4f5d83a 100644
--- a/tests/cts/serviceapi/src/com/android/ondevicepersonalization/testing/sampleserviceapi/SampleServiceApi.java
+++ b/tests/cts/serviceapi/src/com/android/ondevicepersonalization/testing/sampleserviceapi/SampleServiceApi.java
@@ -45,7 +45,14 @@
     public static final String OPCODE_READ_REMOTE_DATA = "read_remote_data";
     public static final String OPCODE_READ_USER_DATA = "read_user_data";
     public static final String OPCODE_READ_LOG = "read_log";
+
+    // Code for the legacy FCP schedule API.
     public static final String OPCODE_SCHEDULE_FEDERATED_JOB = "schedule_federated_job";
+
+    // Code for the new FCP schedule API.
+    public static final String OPCODE_SCHEDULE_FEDERATED_JOB_V2 =
+            "schedule_federated_job_outcome_receiver";
+
     public static final String OPCODE_CANCEL_FEDERATED_JOB = "cancel_federated_job";
 
     // Event types in logs.
diff --git a/tests/federatedcomputetests/src/com/android/federatedcompute/services/common/PhFlagsTest.java b/tests/federatedcomputetests/src/com/android/federatedcompute/services/common/PhFlagsTest.java
index 6a5e421..58665b0 100644
--- a/tests/federatedcomputetests/src/com/android/federatedcompute/services/common/PhFlagsTest.java
+++ b/tests/federatedcomputetests/src/com/android/federatedcompute/services/common/PhFlagsTest.java
@@ -29,6 +29,7 @@
 import static com.android.federatedcompute.services.common.Flags.DEFAULT_TRAINING_MIN_BATTERY_LEVEL;
 import static com.android.federatedcompute.services.common.Flags.ENABLE_CLIENT_ERROR_LOGGING;
 import static com.android.federatedcompute.services.common.Flags.ENCRYPTION_ENABLED;
+import static com.android.federatedcompute.services.common.Flags.FCP_DEFAULT_CHECKPOINT_FILE_SIZE_LIMIT;
 import static com.android.federatedcompute.services.common.Flags.FCP_DEFAULT_MEMORY_SIZE_LIMIT;
 import static com.android.federatedcompute.services.common.Flags.FCP_RECURRENT_RESCHEDULE_LIMIT;
 import static com.android.federatedcompute.services.common.Flags.FCP_RESCHEDULE_LIMIT;
@@ -45,6 +46,7 @@
 import static com.android.federatedcompute.services.common.PhFlags.ENABLE_ELIGIBILITY_TASK;
 import static com.android.federatedcompute.services.common.PhFlags.FCP_BACKGROUND_JOB_LOGGING_SAMPLING_RATE;
 import static com.android.federatedcompute.services.common.PhFlags.FCP_BACKGROUND_JOB_SAMPLING_LOGGING_RATE;
+import static com.android.federatedcompute.services.common.PhFlags.FCP_CHECKPOINT_FILE_SIZE_LIMIT_CONFIG_NAME;
 import static com.android.federatedcompute.services.common.PhFlags.FCP_ENABLE_BACKGROUND_JOBS_LOGGING;
 import static com.android.federatedcompute.services.common.PhFlags.FCP_ENABLE_CLIENT_ERROR_LOGGING;
 import static com.android.federatedcompute.services.common.PhFlags.FCP_ENABLE_ENCRYPTION;
@@ -206,6 +208,11 @@
                 FCP_TASK_LIMIT_PER_PACKAGE_CONFIG_NAME,
                 Integer.toString(DEFAULT_FCP_TASK_LIMIT_PER_PACKAGE),
                 /* makeDefault= */ false);
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                FCP_CHECKPOINT_FILE_SIZE_LIMIT_CONFIG_NAME,
+                Integer.toString(FCP_DEFAULT_CHECKPOINT_FILE_SIZE_LIMIT),
+                /* makeDefault= */ false);
     }
 
     @Test
@@ -620,28 +627,13 @@
 
     @Test
     public void testGetBackgroundJobsLoggingEnabled() {
-        // read a stable flag value and verify it's equal to the default value.
-        boolean stableValue = FlagsFactory.getFlags().getBackgroundJobsLoggingEnabled();
-        assertThat(stableValue).isEqualTo(BACKGROUND_JOB_LOGGING_ENABLED);
-
-        // Now overriding the value from PH.
-        boolean overrideEnabled = !stableValue;
-        DeviceConfig.setProperty(
-                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
-                FCP_ENABLE_BACKGROUND_JOBS_LOGGING,
-                Boolean.toString(overrideEnabled),
-                /* makeDefault= */ false);
-
-        // the flag value remains stable
         assertThat(FlagsFactory.getFlags().getBackgroundJobsLoggingEnabled())
-                .isEqualTo(stableValue);
+                .isEqualTo(true);
     }
 
     @Test
     public void testGetBackgroundJobSamplingLoggingRate() {
         int defaultValue = FCP_BACKGROUND_JOB_SAMPLING_LOGGING_RATE;
-        assertThat(FlagsFactory.getFlags().getBackgroundJobSamplingLoggingRate())
-                .isEqualTo(defaultValue);
 
         // Now overriding the value from PH.
         int overrideRate = defaultValue + 1;
@@ -701,10 +693,33 @@
     }
 
     @Test
+    public void testGetFcpCheckinFileSizeLimit() {
+        // Without Overriding
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                FCP_CHECKPOINT_FILE_SIZE_LIMIT_CONFIG_NAME,
+                Integer.toString(FCP_DEFAULT_CHECKPOINT_FILE_SIZE_LIMIT),
+                /* makeDefault= */ false);
+        assertThat(FlagsFactory.getFlags().getFcpCheckpointFileSizeLimit())
+                .isEqualTo(FCP_DEFAULT_CHECKPOINT_FILE_SIZE_LIMIT);
+
+        // Now overriding the value from PH.
+        int overrideFcpCheckinFileSizeLimit = 1000;
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                FCP_CHECKPOINT_FILE_SIZE_LIMIT_CONFIG_NAME,
+                Integer.toString(overrideFcpCheckinFileSizeLimit),
+                /* makeDefault= */ false);
+
+        Flags phFlags = FlagsFactory.getFlags();
+        assertThat(phFlags.getFcpCheckpointFileSizeLimit())
+                .isEqualTo(overrideFcpCheckinFileSizeLimit);
+    }
+
+    @Test
     public void testGetJobSchedulingLoggingEnabled() {
         // read a stable flag value and verify it's equal to the default value.
         boolean stableValue = FlagsFactory.getFlags().getJobSchedulingLoggingEnabled();
-        assertThat(stableValue).isEqualTo(DEFAULT_JOB_SCHEDULING_LOGGING_ENABLED);
 
         // override the value in device config.
         boolean overrideEnabled = !stableValue;
@@ -722,8 +737,6 @@
     @Test
     public void testGetJobSchedulingLoggingSamplingRate() {
         int defaultValue = DEFAULT_JOB_SCHEDULING_LOGGING_SAMPLING_RATE;
-        assertThat(FlagsFactory.getFlags().getJobSchedulingLoggingSamplingRate())
-                .isEqualTo(defaultValue);
 
         // Override the value in device config.
         int overrideRate = defaultValue + 1;
diff --git a/tests/federatedcomputetests/src/com/android/federatedcompute/services/encryption/BackgroundKeyFetchJobServiceTest.java b/tests/federatedcomputetests/src/com/android/federatedcompute/services/encryption/BackgroundKeyFetchJobServiceTest.java
index b7dcb65..7af1169 100644
--- a/tests/federatedcomputetests/src/com/android/federatedcompute/services/encryption/BackgroundKeyFetchJobServiceTest.java
+++ b/tests/federatedcomputetests/src/com/android/federatedcompute/services/encryption/BackgroundKeyFetchJobServiceTest.java
@@ -49,7 +49,7 @@
 import com.android.federatedcompute.services.data.FederatedComputeDbHelper;
 import com.android.federatedcompute.services.data.FederatedComputeEncryptionKey;
 import com.android.federatedcompute.services.data.FederatedComputeEncryptionKeyDao;
-import com.android.federatedcompute.services.http.HttpClient;
+import com.android.odp.module.common.HttpClient;
 import com.android.odp.module.common.MonotonicClock;
 
 import com.google.common.util.concurrent.FluentFuture;
@@ -98,7 +98,7 @@
         mContext = ApplicationProvider.getApplicationContext();
         mInjector = new TestInjector();
         mEncryptionDao = FederatedComputeEncryptionKeyDao.getInstanceForTest(mContext);
-        mHttpClient = new HttpClient();
+        mHttpClient = new HttpClient(/* retryLimit= */ 3, MoreExecutors.newDirectExecutorService());
         mSpyService = spy(new BackgroundKeyFetchJobService(new TestInjector()));
         doReturn(mSpyService).when(mSpyService).getApplicationContext();
         doNothing().when(mSpyService).jobFinished(any(), anyBoolean());
diff --git a/tests/federatedcomputetests/src/com/android/federatedcompute/services/encryption/FederatedComputeKeyFetchManagerTest.java b/tests/federatedcomputetests/src/com/android/federatedcompute/services/encryption/FederatedComputeKeyFetchManagerTest.java
index 5c1b3c4..8a1ca01 100644
--- a/tests/federatedcomputetests/src/com/android/federatedcompute/services/encryption/FederatedComputeKeyFetchManagerTest.java
+++ b/tests/federatedcomputetests/src/com/android/federatedcompute/services/encryption/FederatedComputeKeyFetchManagerTest.java
@@ -37,10 +37,10 @@
 import com.android.federatedcompute.services.data.FederatedComputeDbHelper;
 import com.android.federatedcompute.services.data.FederatedComputeEncryptionKey;
 import com.android.federatedcompute.services.data.FederatedComputeEncryptionKeyDao;
-import com.android.federatedcompute.services.http.FederatedComputeHttpResponse;
-import com.android.federatedcompute.services.http.HttpClient;
 import com.android.odp.module.common.Clock;
+import com.android.odp.module.common.HttpClient;
 import com.android.odp.module.common.MonotonicClock;
+import com.android.odp.module.common.OdpHttpResponse;
 
 import com.google.common.util.concurrent.Futures;
 
@@ -69,7 +69,7 @@
                     "Content-Type", List.of("json"));
 
     private static final String SAMPLE_RESPONSE_PAYLOAD =
-            """
+                    """
 { "keys": [{ "id": "0cc9b4c9-08bd", "key": "BQo+c1Tw6TaQ+VH/b+9PegZOjHuKAFkl8QdmS0IjRj8" """
                     + "} ] }";
 
@@ -166,7 +166,7 @@
     public void testFetchAndPersistActiveKeys_scheduled_success() throws Exception {
         doReturn(
                         Futures.immediateFuture(
-                                new FederatedComputeHttpResponse.Builder()
+                                new OdpHttpResponse.Builder()
                                         .setHeaders(SAMPLE_RESPONSE_HEADER)
                                         .setPayload(SAMPLE_RESPONSE_PAYLOAD.getBytes())
                                         .setStatusCode(200)
@@ -186,7 +186,7 @@
     public void testFetchAndPersistActiveKeys_nonScheduled_success() throws Exception {
         doReturn(
                         Futures.immediateFuture(
-                                new FederatedComputeHttpResponse.Builder()
+                                new OdpHttpResponse.Builder()
                                         .setHeaders(SAMPLE_RESPONSE_HEADER)
                                         .setPayload(SAMPLE_RESPONSE_PAYLOAD.getBytes())
                                         .setStatusCode(200)
@@ -280,7 +280,7 @@
     public void testFetchAndPersistActiveKeys_scheduledNoDeletion() throws Exception {
         doReturn(
                         Futures.immediateFuture(
-                                new FederatedComputeHttpResponse.Builder()
+                                new OdpHttpResponse.Builder()
                                         .setHeaders(SAMPLE_RESPONSE_HEADER)
                                         .setPayload(SAMPLE_RESPONSE_PAYLOAD.getBytes())
                                         .setStatusCode(200)
@@ -314,7 +314,7 @@
     public void testFetchAndPersistActiveKeys_nonScheduledNoDeletion() throws Exception {
         doReturn(
                         Futures.immediateFuture(
-                                new FederatedComputeHttpResponse.Builder()
+                                new OdpHttpResponse.Builder()
                                         .setHeaders(SAMPLE_RESPONSE_HEADER)
                                         .setPayload(SAMPLE_RESPONSE_PAYLOAD.getBytes())
                                         .setStatusCode(200)
@@ -348,7 +348,7 @@
     public void testFetchAndPersistActiveKeys_scheduledWithDeletion() throws Exception {
         doReturn(
                         Futures.immediateFuture(
-                                new FederatedComputeHttpResponse.Builder()
+                                new OdpHttpResponse.Builder()
                                         .setHeaders(SAMPLE_RESPONSE_HEADER)
                                         .setPayload(SAMPLE_RESPONSE_PAYLOAD.getBytes())
                                         .setStatusCode(200)
@@ -387,7 +387,7 @@
     public void testFetchAndPersistActiveKeys_nonScheduledWithDeletion() throws Exception {
         doReturn(
                         Futures.immediateFuture(
-                                new FederatedComputeHttpResponse.Builder()
+                                new OdpHttpResponse.Builder()
                                         .setHeaders(SAMPLE_RESPONSE_HEADER)
                                         .setPayload(SAMPLE_RESPONSE_PAYLOAD.getBytes())
                                         .setStatusCode(200)
@@ -429,7 +429,7 @@
     public void testGetOrFetchActiveKeys_fetch() {
         doReturn(
                         Futures.immediateFuture(
-                                new FederatedComputeHttpResponse.Builder()
+                                new OdpHttpResponse.Builder()
                                         .setHeaders(SAMPLE_RESPONSE_HEADER)
                                         .setPayload(SAMPLE_RESPONSE_PAYLOAD.getBytes())
                                         .setStatusCode(200)
@@ -458,7 +458,7 @@
                         .build());
         doReturn(
                         Futures.immediateFuture(
-                                new FederatedComputeHttpResponse.Builder()
+                                new OdpHttpResponse.Builder()
                                         .setHeaders(SAMPLE_RESPONSE_HEADER)
                                         .setPayload(SAMPLE_RESPONSE_PAYLOAD.getBytes())
                                         .setStatusCode(200)
diff --git a/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/FederatedComputeHttpRequestTest.java b/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/FederatedComputeHttpRequestTest.java
deleted file mode 100644
index fb5a339..0000000
--- a/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/FederatedComputeHttpRequestTest.java
+++ /dev/null
@@ -1,154 +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.federatedcompute.services.http;
-
-import static com.android.federatedcompute.services.http.HttpClientUtil.ACCEPT_ENCODING_HDR;
-import static com.android.federatedcompute.services.http.HttpClientUtil.GZIP_ENCODING_HDR;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
-
-import com.android.federatedcompute.services.http.HttpClientUtil.HttpMethod;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-import java.util.HashMap;
-
-@RunWith(JUnit4.class)
-public final class FederatedComputeHttpRequestTest {
-    private static final byte[] PAYLOAD = "non_empty_request_body".getBytes();
-
-    @Test
-    public void testCreateRequestInvalidUri_fails() throws Exception {
-        assertThrows(
-                IllegalArgumentException.class,
-                () ->
-                        FederatedComputeHttpRequest.create(
-                                "http://invalid.com", HttpMethod.GET, new HashMap<>(), PAYLOAD));
-    }
-
-    @Test
-    public void testCreateWithInvalidRequestBody_fails() throws Exception {
-        assertThrows(
-                IllegalArgumentException.class,
-                () ->
-                        FederatedComputeHttpRequest.create(
-                                "https://valid.com", HttpMethod.GET, new HashMap<>(), PAYLOAD));
-    }
-
-    @Test
-    public void testCreateWithContentLengthHeader_fails() throws Exception {
-        HashMap<String, String> headers = new HashMap<>();
-        headers.put("Content-Length", "1234");
-        assertThrows(
-                IllegalArgumentException.class,
-                () ->
-                        FederatedComputeHttpRequest.create(
-                                "https://valid.com", HttpMethod.POST, headers, PAYLOAD));
-    }
-
-    @Test
-    public void createGetRequest_valid() throws Exception {
-        String expectedUri = "https://valid.com";
-        FederatedComputeHttpRequest request =
-                FederatedComputeHttpRequest.create(
-                        expectedUri, HttpMethod.GET, new HashMap<>(), HttpClientUtil.EMPTY_BODY);
-
-        assertThat(request.getUri()).isEqualTo(expectedUri);
-        assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.GET);
-        assertThat(request.getBody()).isEqualTo(HttpClientUtil.EMPTY_BODY);
-        assertTrue(request.getExtraHeaders().isEmpty());
-    }
-
-    @Test
-    public void createGetRequestWithHeader_valid() throws Exception {
-        String expectedUri = "https://valid.com";
-        HashMap<String, String> expectedHeaders = new HashMap<>();
-        expectedHeaders.put("Foo", "Bar");
-
-        FederatedComputeHttpRequest request =
-                FederatedComputeHttpRequest.create(
-                        expectedUri, HttpMethod.GET, expectedHeaders, HttpClientUtil.EMPTY_BODY);
-
-        assertThat(request.getUri()).isEqualTo(expectedUri);
-        assertThat(request.getExtraHeaders()).isEqualTo(expectedHeaders);
-    }
-
-    @Test
-    public void createPostRequestWithoutBody_valid() {
-        String expectedUri = "https://valid.com";
-
-        FederatedComputeHttpRequest request =
-                FederatedComputeHttpRequest.create(
-                        expectedUri, HttpMethod.POST, new HashMap<>(), HttpClientUtil.EMPTY_BODY);
-
-        assertThat(request.getUri()).isEqualTo(expectedUri);
-        assertTrue(request.getExtraHeaders().isEmpty());
-        assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.POST);
-        assertThat(request.getBody()).isEqualTo(HttpClientUtil.EMPTY_BODY);
-    }
-
-    @Test
-    public void createPostRequestWithBody_valid() {
-        String expectedUri = "https://valid.com";
-
-        FederatedComputeHttpRequest request =
-                FederatedComputeHttpRequest.create(
-                        expectedUri, HttpMethod.POST, new HashMap<>(), PAYLOAD);
-
-        assertThat(request.getUri()).isEqualTo(expectedUri);
-        assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.POST);
-        assertThat(request.getBody()).isEqualTo(PAYLOAD);
-    }
-
-    @Test
-    public void createPostRequestWithBodyHeader_valid() {
-        String expectedUri = "https://valid.com";
-        HashMap<String, String> expectedHeaders = new HashMap<>();
-        expectedHeaders.put("Foo", "Bar");
-
-        FederatedComputeHttpRequest request =
-                FederatedComputeHttpRequest.create(
-                        expectedUri, HttpMethod.POST, expectedHeaders, PAYLOAD);
-
-        assertThat(request.getUri()).isEqualTo(expectedUri);
-        assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.POST);
-        assertThat(request.getBody()).isEqualTo(PAYLOAD);
-        assertThat(request.getExtraHeaders()).isEqualTo(expectedHeaders);
-    }
-
-    @Test
-    public void createGetRequestWithAcceptCompression_valid() {
-        String expectedUri = "https://valid.com";
-        HashMap<String, String> headerList = new HashMap<>();
-        headerList.put(ACCEPT_ENCODING_HDR, GZIP_ENCODING_HDR);
-        FederatedComputeHttpRequest request =
-                FederatedComputeHttpRequest.create(
-                        expectedUri, HttpMethod.POST, headerList, PAYLOAD);
-
-        assertThat(request.getUri()).isEqualTo(expectedUri);
-        assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.POST);
-        HashMap<String, String> expectedHeaders = new HashMap<>();
-        expectedHeaders.put(HttpClientUtil.CONTENT_LENGTH_HDR, String.valueOf(22));
-        expectedHeaders.put(ACCEPT_ENCODING_HDR, GZIP_ENCODING_HDR);
-        assertThat(request.getExtraHeaders()).isEqualTo(expectedHeaders);
-    }
-}
diff --git a/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/HttpClientTest.java b/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/HttpClientTest.java
deleted file mode 100644
index c616ec2..0000000
--- a/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/HttpClientTest.java
+++ /dev/null
@@ -1,196 +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.federatedcompute.services.http;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.android.federatedcompute.services.http.HttpClientUtil.HttpMethod;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.ArgumentMatchers;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.Spy;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicInteger;
-
-@RunWith(JUnit4.class)
-public final class HttpClientTest {
-    public static final FederatedComputeHttpRequest DEFAULT_GET_REQUEST =
-            FederatedComputeHttpRequest.create(
-                    "https://google.com",
-                    HttpMethod.GET,
-                    new HashMap<>(),
-                    HttpClientUtil.EMPTY_BODY);
-    @Spy private HttpClient mHttpClient = new HttpClient();
-    @Rule public MockitoRule rule = MockitoJUnit.rule();
-    @Mock private HttpURLConnection mMockHttpURLConnection;
-
-    @Test
-    public void testUnableToOpenconnection_returnFailure() throws Exception {
-        FederatedComputeHttpRequest request =
-                FederatedComputeHttpRequest.create(
-                        "https://google.com",
-                        HttpMethod.POST,
-                        new HashMap<>(),
-                        HttpClientUtil.EMPTY_BODY);
-        doThrow(new IOException()).when(mHttpClient).setup(ArgumentMatchers.any());
-
-        assertThrows(IOException.class, () -> mHttpClient.performRequest(request));
-    }
-
-    @Test
-    public void testPerformGetRequestSuccess() throws Exception {
-        String successMessage = "Success!";
-        InputStream mockStream = new ByteArrayInputStream(successMessage.getBytes(UTF_8));
-        Map<String, List<String>> mockHeaders = new HashMap<>();
-        mockHeaders.put("Header1", Arrays.asList("Value1"));
-        when(mMockHttpURLConnection.getInputStream()).thenReturn(mockStream);
-        when(mMockHttpURLConnection.getResponseCode()).thenReturn(200);
-        when(mMockHttpURLConnection.getHeaderFields()).thenReturn(mockHeaders);
-        doReturn(mMockHttpURLConnection).when(mHttpClient).setup(ArgumentMatchers.any());
-        when(mMockHttpURLConnection.getContentLengthLong())
-                .thenReturn((long) successMessage.length());
-
-        FederatedComputeHttpResponse response = mHttpClient.performRequest(DEFAULT_GET_REQUEST);
-
-        assertThat(response.getStatusCode()).isEqualTo(200);
-        assertThat(response.getHeaders()).isEqualTo(mockHeaders);
-        assertThat(response.getPayload()).isEqualTo(successMessage.getBytes(UTF_8));
-    }
-
-    @Test
-    public void testPerformGetRequestFails() throws Exception {
-        String failureMessage = "FAIL!";
-        InputStream mockStream = new ByteArrayInputStream(failureMessage.getBytes(UTF_8));
-        when(mMockHttpURLConnection.getErrorStream()).thenReturn(mockStream);
-        when(mMockHttpURLConnection.getResponseCode()).thenReturn(503);
-        when(mMockHttpURLConnection.getHeaderFields()).thenReturn(new HashMap<>());
-        doReturn(mMockHttpURLConnection).when(mHttpClient).setup(ArgumentMatchers.any());
-        when(mMockHttpURLConnection.getContentLengthLong())
-                .thenReturn((long) failureMessage.length());
-
-        FederatedComputeHttpResponse response = mHttpClient.performRequest(DEFAULT_GET_REQUEST);
-
-        assertThat(response.getStatusCode()).isEqualTo(503);
-        assertTrue(response.getHeaders().isEmpty());
-        assertThat(response.getPayload()).isEqualTo(failureMessage.getBytes(UTF_8));
-    }
-
-    @Test
-    public void testPerformGetRequestFailsWithRetry() throws Exception {
-        String failureMessage = "FAIL!";
-        when(mMockHttpURLConnection.getErrorStream())
-                .then(invocation -> new ByteArrayInputStream(failureMessage.getBytes(UTF_8)));
-        when(mMockHttpURLConnection.getResponseCode()).thenReturn(503);
-        when(mMockHttpURLConnection.getHeaderFields()).thenReturn(new HashMap<>());
-        when(mMockHttpURLConnection.getContentLengthLong())
-                .thenReturn((long) failureMessage.length());
-        doReturn(mMockHttpURLConnection).when(mHttpClient).setup(ArgumentMatchers.any());
-
-        FederatedComputeHttpResponse response =
-                mHttpClient.performRequestWithRetry(DEFAULT_GET_REQUEST);
-
-        verify(mHttpClient, times(3)).performRequest(DEFAULT_GET_REQUEST);
-        assertThat(response.getStatusCode()).isEqualTo(503);
-        assertTrue(response.getHeaders().isEmpty());
-        assertThat(response.getPayload()).isEqualTo(failureMessage.getBytes(UTF_8));
-    }
-
-    @Test
-    public void testPerformGetRequestSuccessWithRetry() throws Exception {
-        String failureMessage = "FAIL!";
-        InputStream mockStream = new ByteArrayInputStream(failureMessage.getBytes(UTF_8));
-        when(mMockHttpURLConnection.getErrorStream()).thenReturn(mockStream);
-        when(mMockHttpURLConnection.getResponseCode()).thenReturn(503);
-        when(mMockHttpURLConnection.getHeaderFields()).thenReturn(new HashMap<>());
-        HttpURLConnection mockSuccessfulHttpURLConnection = Mockito.mock(HttpURLConnection.class);
-        Map<String, List<String>> mockHeaders = new HashMap<>();
-        mockHeaders.put("Header1", Arrays.asList("Value1"));
-        when(mockSuccessfulHttpURLConnection.getOutputStream())
-                .thenReturn(new ByteArrayOutputStream());
-        when(mockSuccessfulHttpURLConnection.getResponseCode()).thenReturn(200);
-        when(mockSuccessfulHttpURLConnection.getHeaderFields()).thenReturn(mockHeaders);
-        final AtomicInteger countCall = new AtomicInteger();
-        doAnswer(
-                        invocation -> {
-                            int count = countCall.incrementAndGet();
-                            if (count < 3) {
-                                return mMockHttpURLConnection;
-                            } else {
-                                return mockSuccessfulHttpURLConnection;
-                            }
-                        })
-                .when(mHttpClient)
-                .setup(ArgumentMatchers.any());
-        when(mMockHttpURLConnection.getContentLengthLong())
-                .thenReturn((long) failureMessage.length());
-
-        FederatedComputeHttpResponse response =
-                mHttpClient.performRequestWithRetry(DEFAULT_GET_REQUEST);
-
-        verify(mHttpClient, times(3)).performRequest(DEFAULT_GET_REQUEST);
-        assertThat(response.getStatusCode()).isEqualTo(200);
-        assertThat(response.getHeaders()).isEqualTo(mockHeaders);
-    }
-
-    @Test
-    public void testPerformPostRequestSuccess() throws Exception {
-        FederatedComputeHttpRequest request =
-                FederatedComputeHttpRequest.create(
-                        "https://google.com",
-                        HttpMethod.POST,
-                        new HashMap<>(),
-                        "payload".getBytes(UTF_8));
-        Map<String, List<String>> mockHeaders = new HashMap<>();
-        mockHeaders.put("Header1", Arrays.asList("Value1"));
-        when(mMockHttpURLConnection.getOutputStream()).thenReturn(new ByteArrayOutputStream());
-        when(mMockHttpURLConnection.getResponseCode()).thenReturn(200);
-        when(mMockHttpURLConnection.getHeaderFields()).thenReturn(mockHeaders);
-        doReturn(mMockHttpURLConnection).when(mHttpClient).setup(ArgumentMatchers.any());
-
-        FederatedComputeHttpResponse response = mHttpClient.performRequest(request);
-
-        assertThat(response.getStatusCode()).isEqualTo(200);
-        assertThat(response.getHeaders()).isEqualTo(mockHeaders);
-    }
-}
diff --git a/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/HttpClientUtilTest.java b/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/HttpClientUtilTest.java
index 1f16aec..127ce13 100644
--- a/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/HttpClientUtilTest.java
+++ b/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/HttpClientUtilTest.java
@@ -24,6 +24,8 @@
 
 import androidx.test.core.app.ApplicationProvider;
 
+import com.android.odp.module.common.HttpClientUtils;
+
 import org.junit.Test;
 
 import java.io.ByteArrayOutputStream;
@@ -45,10 +47,10 @@
         }
         byte[] dataBeforeCompress = outputStream.toByteArray();
 
-        byte[] dataAfterCompress = HttpClientUtil.compressWithGzip(dataBeforeCompress);
+        byte[] dataAfterCompress = HttpClientUtils.compressWithGzip(dataBeforeCompress);
         assertThat(dataAfterCompress.length).isLessThan(dataBeforeCompress.length);
 
-        byte[] unzipData = HttpClientUtil.uncompressWithGzip(dataAfterCompress);
+        byte[] unzipData = HttpClientUtils.uncompressWithGzip(dataAfterCompress);
         assertThat(unzipData).isEqualTo(dataBeforeCompress);
     }
 }
diff --git a/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/HttpFederatedProtocolTest.java b/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/HttpFederatedProtocolTest.java
index c180dd9..b4c0873 100644
--- a/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/HttpFederatedProtocolTest.java
+++ b/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/HttpFederatedProtocolTest.java
@@ -19,15 +19,16 @@
 import static com.android.federatedcompute.services.http.HttpClientUtil.ACCEPT_ENCODING_HDR;
 import static com.android.federatedcompute.services.http.HttpClientUtil.CONTENT_ENCODING_HDR;
 import static com.android.federatedcompute.services.http.HttpClientUtil.CONTENT_LENGTH_HDR;
-import static com.android.federatedcompute.services.http.HttpClientUtil.CONTENT_TYPE_HDR;
 import static com.android.federatedcompute.services.http.HttpClientUtil.FCP_OWNER_ID_DIGEST;
 import static com.android.federatedcompute.services.http.HttpClientUtil.GZIP_ENCODING_HDR;
 import static com.android.federatedcompute.services.http.HttpClientUtil.HTTP_UNAUTHENTICATED_STATUS;
 import static com.android.federatedcompute.services.http.HttpClientUtil.ODP_AUTHENTICATION_KEY;
 import static com.android.federatedcompute.services.http.HttpClientUtil.ODP_AUTHORIZATION_KEY;
 import static com.android.federatedcompute.services.http.HttpClientUtil.ODP_IDEMPOTENCY_KEY;
-import static com.android.federatedcompute.services.http.HttpClientUtil.PROTOBUF_CONTENT_TYPE;
-import static com.android.federatedcompute.services.http.HttpClientUtil.compressWithGzip;
+import static com.android.odp.module.common.FileUtils.createTempFile;
+import static com.android.odp.module.common.HttpClientUtils.CONTENT_TYPE_HDR;
+import static com.android.odp.module.common.HttpClientUtils.PROTOBUF_CONTENT_TYPE;
+import static com.android.odp.module.common.HttpClientUtils.compressWithGzip;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.util.concurrent.Futures.immediateFuture;
@@ -51,6 +52,7 @@
 
 import androidx.test.core.app.ApplicationProvider;
 
+import com.android.federatedcompute.services.common.Flags;
 import com.android.federatedcompute.services.common.NetworkStats;
 import com.android.federatedcompute.services.common.PhFlags;
 import com.android.federatedcompute.services.common.TrainingEventLogger;
@@ -59,14 +61,17 @@
 import com.android.federatedcompute.services.data.ODPAuthorizationToken;
 import com.android.federatedcompute.services.data.ODPAuthorizationTokenDao;
 import com.android.federatedcompute.services.encryption.HpkeJniEncrypter;
-import com.android.federatedcompute.services.http.HttpClientUtil.HttpMethod;
 import com.android.federatedcompute.services.security.AuthorizationContext;
 import com.android.federatedcompute.services.security.KeyAttestation;
 import com.android.federatedcompute.services.testutils.TrainingTestUtil;
 import com.android.federatedcompute.services.training.util.ComputationResult;
 import com.android.modules.utils.testing.ExtendedMockitoRule;
 import com.android.odp.module.common.Clock;
+import com.android.odp.module.common.HttpClient;
+import com.android.odp.module.common.HttpClientUtils;
 import com.android.odp.module.common.MonotonicClock;
+import com.android.odp.module.common.OdpHttpRequest;
+import com.android.odp.module.common.OdpHttpResponse;
 
 import com.google.common.collect.BoundType;
 import com.google.common.collect.ImmutableList;
@@ -82,13 +87,13 @@
 import com.google.internal.federatedcompute.v1.Resource;
 import com.google.internal.federatedcompute.v1.ResourceCapabilities;
 import com.google.internal.federatedcompute.v1.ResourceCompressionFormat;
+import com.google.internal.federatedcompute.v1.UploadInstruction;
 import com.google.ondevicepersonalization.federatedcompute.proto.CreateTaskAssignmentRequest;
 import com.google.ondevicepersonalization.federatedcompute.proto.CreateTaskAssignmentResponse;
 import com.google.ondevicepersonalization.federatedcompute.proto.ReportResultRequest;
 import com.google.ondevicepersonalization.federatedcompute.proto.ReportResultRequest.Result;
 import com.google.ondevicepersonalization.federatedcompute.proto.ReportResultResponse;
 import com.google.ondevicepersonalization.federatedcompute.proto.TaskAssignment;
-import com.google.ondevicepersonalization.federatedcompute.proto.UploadInstruction;
 import com.google.protobuf.ByteString;
 
 import org.json.JSONArray;
@@ -104,6 +109,8 @@
 import org.mockito.quality.Strictness;
 
 import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.util.Arrays;
 import java.util.Collection;
@@ -173,6 +180,21 @@
                     .setContributionResult(ContributionResult.FAIL)
                     .setErrorStatus(FLRunnerResult.ErrorStatus.NOT_ELIGIBLE)
                     .build();
+    private static final FLRunnerResult FL_RUNNER_TENSORFLOW_ERROR_RESULT =
+            FLRunnerResult.newBuilder()
+                    .setContributionResult(ContributionResult.FAIL)
+                    .setErrorStatus(FLRunnerResult.ErrorStatus.TENSORFLOW_ERROR)
+                    .build();
+    private static final FLRunnerResult FL_RUNNER_INVALID_ARGUMENT_RESULT =
+            FLRunnerResult.newBuilder()
+                    .setContributionResult(ContributionResult.FAIL)
+                    .setErrorStatus(FLRunnerResult.ErrorStatus.INVALID_ARGUMENT)
+                    .build();
+    private static final FLRunnerResult FL_RUNNER_EXAMPLE_ITEREATOR_EEROR_RESULT =
+            FLRunnerResult.newBuilder()
+                    .setContributionResult(ContributionResult.FAIL)
+                    .setErrorStatus(FLRunnerResult.ErrorStatus.EXAMPLE_ITERATOR_ERROR)
+                    .build();
     private static final CreateTaskAssignmentRequest
             START_TASK_ASSIGNMENT_REQUEST_WITH_COMPRESSION =
                     CreateTaskAssignmentRequest.newBuilder()
@@ -187,11 +209,11 @@
                                             .build())
                             .build();
 
-    private static final FederatedComputeHttpResponse SUCCESS_EMPTY_HTTP_RESPONSE =
-            new FederatedComputeHttpResponse.Builder().setStatusCode(200).build();
+    private static final OdpHttpResponse SUCCESS_EMPTY_HTTP_RESPONSE =
+            new OdpHttpResponse.Builder().setStatusCode(200).build();
     private static final long ODP_AUTHORIZATION_TOKEN_TTL = 30 * 24 * 60 * 60 * 1000L;
 
-    @Captor private ArgumentCaptor<FederatedComputeHttpRequest> mHttpRequestCaptor;
+    @Captor private ArgumentCaptor<OdpHttpRequest> mHttpRequestCaptor;
 
     @Mock private HttpClient mMockHttpClient;
 
@@ -237,6 +259,8 @@
         doNothing().when(mTrainingEventLogger).logTaskAssignmentAuthSucceeded();
         doReturn(true).when(mMocKFlags).isEncryptionEnabled();
         when(PhFlags.getInstance()).thenReturn(mMocKFlags);
+        when(mMocKFlags.getFcpCheckpointFileSizeLimit())
+                .thenReturn(Flags.FCP_DEFAULT_CHECKPOINT_FILE_SIZE_LIMIT);
     }
 
     @After
@@ -259,14 +283,14 @@
                 .downloadTaskAssignment(createTaskAssignmentResponse.getTaskAssignment())
                 .get();
 
-        List<FederatedComputeHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
+        List<OdpHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
 
         // Verify task assignment request.
-        FederatedComputeHttpRequest actualStartTaskAssignmentRequest = actualHttpRequests.get(0);
+        OdpHttpRequest actualStartTaskAssignmentRequest = actualHttpRequests.get(0);
         checkActualTARequest(actualStartTaskAssignmentRequest, 4);
 
         // Verify fetch resource request.
-        FederatedComputeHttpRequest actualFetchResourceRequest = actualHttpRequests.get(1);
+        OdpHttpRequest actualFetchResourceRequest = actualHttpRequests.get(1);
         ImmutableSet<String> resourceUris = ImmutableSet.of(PLAN_URI, CHECKPOINT_URI);
         assertTrue(resourceUris.contains(actualFetchResourceRequest.getUri()));
         HashMap<String, String> expectedHeaders = new HashMap<>();
@@ -279,10 +303,55 @@
         NetworkStats networkStats = mNetworkStatsArgumentCaptor.getValue();
         assertTrue(networkStats.getDataTransferDurationInMillis() > 0);
         if (mSupportCompression) {
-            assertThat(networkStats.getTotalBytesDownloaded()).isEqualTo(248);
+            assertThat(networkStats.getTotalBytesDownloaded()).isEqualTo(213);
             assertThat(networkStats.getTotalBytesUploaded()).isEqualTo(124);
         } else {
-            assertThat(networkStats.getTotalBytesDownloaded()).isEqualTo(125);
+            assertThat(networkStats.getTotalBytesDownloaded()).isEqualTo(110);
+            assertThat(networkStats.getTotalBytesUploaded()).isEqualTo(78);
+        }
+    }
+
+    @Test
+    public void testIssueCheckinFailure_checkpointTooBig() throws Exception {
+        when(mMocKFlags.getFcpCheckpointFileSizeLimit()).thenReturn(0);
+        setUpHttpFederatedProtocol(
+                createStartTaskAssignmentHttpResponse(),
+                createPlanHttpResponse(),
+                checkpointEmptyHttpResponse(),
+                createReportResultHttpResponse(),
+                SUCCESS_EMPTY_HTTP_RESPONSE);
+
+        CreateTaskAssignmentResponse createTaskAssignmentResponse =
+                mHttpFederatedProtocol.createTaskAssignment(createAuthContext()).get();
+        mHttpFederatedProtocol
+                .downloadTaskAssignment(createTaskAssignmentResponse.getTaskAssignment())
+                .get();
+
+        List<OdpHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
+
+        // Verify task assignment request.
+        OdpHttpRequest actualStartTaskAssignmentRequest = actualHttpRequests.get(0);
+        checkActualTARequest(actualStartTaskAssignmentRequest, 4);
+
+        // Verify fetch resource request.
+        OdpHttpRequest actualFetchResourceRequest = actualHttpRequests.get(1);
+        ImmutableSet<String> resourceUris = ImmutableSet.of(PLAN_URI, CHECKPOINT_URI);
+        assertTrue(resourceUris.contains(actualFetchResourceRequest.getUri()));
+        HashMap<String, String> expectedHeaders = new HashMap<>();
+        if (mSupportCompression) {
+            expectedHeaders.put(ACCEPT_ENCODING_HDR, GZIP_ENCODING_HDR);
+        }
+        assertThat(actualFetchResourceRequest.getExtraHeaders()).isEqualTo(expectedHeaders);
+        verify(mTrainingEventLogger).logCheckinStarted();
+        verify(mTrainingEventLogger)
+                .logCheckinInvalidPayload(mNetworkStatsArgumentCaptor.capture());
+        NetworkStats networkStats = mNetworkStatsArgumentCaptor.getValue();
+        assertTrue(networkStats.getDataTransferDurationInMillis() > 0);
+        if (mSupportCompression) {
+            assertThat(networkStats.getTotalBytesDownloaded()).isEqualTo(213);
+            assertThat(networkStats.getTotalBytesUploaded()).isEqualTo(124);
+        } else {
+            assertThat(networkStats.getTotalBytesDownloaded()).isEqualTo(110);
             assertThat(networkStats.getTotalBytesUploaded()).isEqualTo(78);
         }
     }
@@ -300,10 +369,10 @@
         CreateTaskAssignmentResponse createTaskAssignmentResponse =
                 mHttpFederatedProtocol.createTaskAssignment(createAuthContext()).get();
 
-        List<FederatedComputeHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
+        List<OdpHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
 
         // Verify task assignment request.
-        FederatedComputeHttpRequest actualStartTaskAssignmentRequest = actualHttpRequests.get(0);
+        OdpHttpRequest actualStartTaskAssignmentRequest = actualHttpRequests.get(0);
         checkActualTARequest(actualStartTaskAssignmentRequest, 5);
         String authorizationKey =
                 actualStartTaskAssignmentRequest.getExtraHeaders().get(ODP_AUTHORIZATION_KEY);
@@ -342,10 +411,10 @@
         mHttpFederatedProtocol
                 .downloadTaskAssignment(taskAssignmentResponse.getTaskAssignment())
                 .get();
-        List<FederatedComputeHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
+        List<OdpHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
 
         // Verify task assignment request.
-        FederatedComputeHttpRequest actualStartTaskAssignmentRequest = actualHttpRequests.get(0);
+        OdpHttpRequest actualStartTaskAssignmentRequest = actualHttpRequests.get(0);
         checkActualTARequest(actualStartTaskAssignmentRequest, 5);
         String authorizationKey =
                 actualStartTaskAssignmentRequest.getExtraHeaders().get(ODP_AUTHORIZATION_KEY);
@@ -385,8 +454,7 @@
 
     @Test
     public void testCreateTaskAssignmentFailed() {
-        FederatedComputeHttpResponse httpResponse =
-                new FederatedComputeHttpResponse.Builder().setStatusCode(404).build();
+        OdpHttpResponse httpResponse = new OdpHttpResponse.Builder().setStatusCode(404).build();
         when(mMockHttpClient.performRequestAsyncWithRetry(any()))
                 .thenReturn(immediateFuture(httpResponse));
 
@@ -418,10 +486,10 @@
                 .downloadTaskAssignment(taskAssignmentResponse.getTaskAssignment())
                 .get();
 
-        List<FederatedComputeHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
+        List<OdpHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
 
         // Verify task assignment request.
-        FederatedComputeHttpRequest actualStartTaskAssignmentRequest = actualHttpRequests.get(0);
+        OdpHttpRequest actualStartTaskAssignmentRequest = actualHttpRequests.get(0);
         checkActualTARequest(actualStartTaskAssignmentRequest, 6);
         String authenticationKey =
                 actualStartTaskAssignmentRequest.getExtraHeaders().get(ODP_AUTHENTICATION_KEY);
@@ -448,7 +516,7 @@
                 .isEqualTo(OWNER_ID);
 
         // Verify fetch resource request.
-        FederatedComputeHttpRequest actualFetchResourceRequest = actualHttpRequests.get(1);
+        OdpHttpRequest actualFetchResourceRequest = actualHttpRequests.get(1);
         ImmutableSet<String> resourceUris = ImmutableSet.of(PLAN_URI, CHECKPOINT_URI);
         assertTrue(resourceUris.contains(actualFetchResourceRequest.getUri()));
         HashMap<String, String> expectedHeaders = new HashMap<>();
@@ -481,8 +549,8 @@
                 CreateTaskAssignmentResponse.newBuilder()
                         .setRejectionInfo(RejectionInfo.getDefaultInstance())
                         .build();
-        FederatedComputeHttpResponse httpResponse =
-                new FederatedComputeHttpResponse.Builder()
+        OdpHttpResponse httpResponse =
+                new OdpHttpResponse.Builder()
                         .setStatusCode(200)
                         .setPayload(createTaskAssignmentResponse.toByteArray())
                         .build();
@@ -494,7 +562,8 @@
 
         assertThat(taskAssignmentResponse.hasRejectionInfo()).isTrue();
         verify(mTrainingEventLogger).logCheckinStarted();
-        verify(mTrainingEventLogger).logCheckinRejected(mNetworkStatsArgumentCaptor.capture());
+        verify(mTrainingEventLogger)
+                .logCheckinRejected(any(), mNetworkStatsArgumentCaptor.capture());
         NetworkStats networkStats = mNetworkStatsArgumentCaptor.getValue();
         assertThat(networkStats.getTotalBytesDownloaded()).isEqualTo(2);
         assertThat(networkStats.getTotalBytesUploaded()).isEqualTo(339);
@@ -502,8 +571,7 @@
 
     @Test
     public void testTaskAssignmentSuccessPlanFetchFailed() throws Exception {
-        FederatedComputeHttpResponse planHttpResponse =
-                new FederatedComputeHttpResponse.Builder().setStatusCode(404).build();
+        OdpHttpResponse planHttpResponse = new OdpHttpResponse.Builder().setStatusCode(404).build();
         // The workflow: start task assignment success, download plan failed and download
         // checkpoint success.
         setUpHttpFederatedProtocol(
@@ -535,8 +603,8 @@
 
     @Test
     public void testTaskAssignmentSuccessCheckpointDataFetchFailed() throws Exception {
-        FederatedComputeHttpResponse checkpointHttpResponse =
-                new FederatedComputeHttpResponse.Builder().setStatusCode(404).build();
+        OdpHttpResponse checkpointHttpResponse =
+                new OdpHttpResponse.Builder().setStatusCode(404).build();
 
         // The workflow: start task assignment success, download plan success and download
         // checkpoint failed.
@@ -580,9 +648,9 @@
                 .get();
 
         // Verify ReportResult request.
-        List<FederatedComputeHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
+        List<OdpHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
         assertThat(actualHttpRequests).hasSize(4);
-        FederatedComputeHttpRequest actualReportResultRequest = actualHttpRequests.get(3);
+        OdpHttpRequest actualReportResultRequest = actualHttpRequests.get(3);
         ReportResultRequest reportResultRequest =
                 ReportResultRequest.newBuilder()
                         .setResult(Result.FAILED)
@@ -615,9 +683,9 @@
                 .get();
 
         // Verify ReportResult request.
-        List<FederatedComputeHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
+        List<OdpHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
         assertThat(actualHttpRequests).hasSize(4);
-        FederatedComputeHttpRequest actualReportResultRequest = actualHttpRequests.get(3);
+        OdpHttpRequest actualReportResultRequest = actualHttpRequests.get(3);
         ReportResultRequest reportResultRequest =
                 ReportResultRequest.newBuilder()
                         .setResult(Result.NOT_ELIGIBLE)
@@ -640,6 +708,137 @@
     }
 
     @Test
+    public void testReportTensorflowErrorTrainingResult_returnSuccess() throws Exception {
+        ComputationResult computationResult =
+                new ComputationResult(
+                        createOutputCheckpointFile(), FL_RUNNER_TENSORFLOW_ERROR_RESULT, null);
+
+        setUpHttpFederatedProtocol();
+        // Setup task id, aggregation id for report result.
+        CreateTaskAssignmentResponse taskAssignmentResponse =
+                mHttpFederatedProtocol.createTaskAssignment(createAuthContext()).get();
+        mHttpFederatedProtocol
+                .downloadTaskAssignment(taskAssignmentResponse.getTaskAssignment())
+                .get();
+
+        mHttpFederatedProtocol
+                .reportResult(computationResult, ENCRYPTION_KEY, createAuthContext())
+                .get();
+
+        // Verify ReportResult request.
+        List<OdpHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
+        assertThat(actualHttpRequests).hasSize(4);
+        OdpHttpRequest actualReportResultRequest = actualHttpRequests.get(3);
+        ReportResultRequest reportResultRequest =
+                ReportResultRequest.newBuilder()
+                        .setResult(Result.FAILED_MODEL_COMPUTATION)
+                        .setResourceCapabilities(
+                                ResourceCapabilities.newBuilder()
+                                        .addSupportedCompressionFormats(
+                                                ResourceCompressionFormat
+                                                        .RESOURCE_COMPRESSION_FORMAT_GZIP))
+                        .build();
+        checkActualReportResultRequest(actualReportResultRequest);
+        assertThat(actualReportResultRequest.getBody())
+                .isEqualTo(reportResultRequest.toByteArray());
+        verify(mTrainingEventLogger).logFailureResultUploadStarted();
+        verify(mTrainingEventLogger)
+                .logFailureResultUploadCompleted(mNetworkStatsArgumentCaptor.capture());
+        NetworkStats networkStats = mNetworkStatsArgumentCaptor.getValue();
+        assertTrue(networkStats.getDataTransferDurationInMillis() > 0);
+        assertThat(networkStats.getTotalBytesDownloaded()).isEqualTo(mSupportCompression ? 96 : 68);
+        assertThat(networkStats.getTotalBytesUploaded()).isEqualTo(231);
+    }
+
+    @Test
+    public void testReportInvalidArgTrainingResult_returnSuccess() throws Exception {
+        ComputationResult computationResult =
+                new ComputationResult(
+                        createOutputCheckpointFile(), FL_RUNNER_INVALID_ARGUMENT_RESULT, null);
+
+        setUpHttpFederatedProtocol();
+        // Setup task id, aggregation id for report result.
+        CreateTaskAssignmentResponse taskAssignmentResponse =
+                mHttpFederatedProtocol.createTaskAssignment(createAuthContext()).get();
+        mHttpFederatedProtocol
+                .downloadTaskAssignment(taskAssignmentResponse.getTaskAssignment())
+                .get();
+
+        mHttpFederatedProtocol
+                .reportResult(computationResult, ENCRYPTION_KEY, createAuthContext())
+                .get();
+
+        // Verify ReportResult request.
+        List<OdpHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
+        assertThat(actualHttpRequests).hasSize(4);
+        OdpHttpRequest actualReportResultRequest = actualHttpRequests.get(3);
+        ReportResultRequest reportResultRequest =
+                ReportResultRequest.newBuilder()
+                        .setResult(Result.FAILED_MODEL_COMPUTATION)
+                        .setResourceCapabilities(
+                                ResourceCapabilities.newBuilder()
+                                        .addSupportedCompressionFormats(
+                                                ResourceCompressionFormat
+                                                        .RESOURCE_COMPRESSION_FORMAT_GZIP))
+                        .build();
+        checkActualReportResultRequest(actualReportResultRequest);
+        assertThat(actualReportResultRequest.getBody())
+                .isEqualTo(reportResultRequest.toByteArray());
+        verify(mTrainingEventLogger).logFailureResultUploadStarted();
+        verify(mTrainingEventLogger)
+                .logFailureResultUploadCompleted(mNetworkStatsArgumentCaptor.capture());
+        NetworkStats networkStats = mNetworkStatsArgumentCaptor.getValue();
+        assertTrue(networkStats.getDataTransferDurationInMillis() > 0);
+        assertThat(networkStats.getTotalBytesDownloaded()).isEqualTo(mSupportCompression ? 96 : 68);
+        assertThat(networkStats.getTotalBytesUploaded()).isEqualTo(231);
+    }
+
+    @Test
+    public void testReportExampleIteratorErrorTrainingResult_returnSuccess() throws Exception {
+        ComputationResult computationResult =
+                new ComputationResult(
+                        createOutputCheckpointFile(),
+                        FL_RUNNER_EXAMPLE_ITEREATOR_EEROR_RESULT,
+                        null);
+
+        setUpHttpFederatedProtocol();
+        // Setup task id, aggregation id for report result.
+        CreateTaskAssignmentResponse taskAssignmentResponse =
+                mHttpFederatedProtocol.createTaskAssignment(createAuthContext()).get();
+        mHttpFederatedProtocol
+                .downloadTaskAssignment(taskAssignmentResponse.getTaskAssignment())
+                .get();
+
+        mHttpFederatedProtocol
+                .reportResult(computationResult, ENCRYPTION_KEY, createAuthContext())
+                .get();
+
+        // Verify ReportResult request.
+        List<OdpHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
+        assertThat(actualHttpRequests).hasSize(4);
+        OdpHttpRequest actualReportResultRequest = actualHttpRequests.get(3);
+        ReportResultRequest reportResultRequest =
+                ReportResultRequest.newBuilder()
+                        .setResult(Result.FAILED_EXAMPLE_GENERATION)
+                        .setResourceCapabilities(
+                                ResourceCapabilities.newBuilder()
+                                        .addSupportedCompressionFormats(
+                                                ResourceCompressionFormat
+                                                        .RESOURCE_COMPRESSION_FORMAT_GZIP))
+                        .build();
+        checkActualReportResultRequest(actualReportResultRequest);
+        assertThat(actualReportResultRequest.getBody())
+                .isEqualTo(reportResultRequest.toByteArray());
+        verify(mTrainingEventLogger).logFailureResultUploadStarted();
+        verify(mTrainingEventLogger)
+                .logFailureResultUploadCompleted(mNetworkStatsArgumentCaptor.capture());
+        NetworkStats networkStats = mNetworkStatsArgumentCaptor.getValue();
+        assertTrue(networkStats.getDataTransferDurationInMillis() > 0);
+        assertThat(networkStats.getTotalBytesDownloaded()).isEqualTo(mSupportCompression ? 96 : 68);
+        assertThat(networkStats.getTotalBytesUploaded()).isEqualTo(231);
+    }
+
+    @Test
     public void testReportAndUploadResultSuccess() throws Exception {
         ComputationResult computationResult =
                 new ComputationResult(createOutputCheckpointFile(), FL_RUNNER_SUCCESS_RESULT, null);
@@ -657,8 +856,8 @@
                 .get();
 
         // Verify ReportResult request.
-        List<FederatedComputeHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
-        FederatedComputeHttpRequest actualReportResultRequest = actualHttpRequests.get(3);
+        List<OdpHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
+        OdpHttpRequest actualReportResultRequest = actualHttpRequests.get(3);
 
         checkActualReportResultRequest(actualReportResultRequest);
         ReportResultRequest reportResultRequest =
@@ -674,9 +873,10 @@
                 .isEqualTo(reportResultRequest.toByteArray());
 
         // Verify upload data request.
-        FederatedComputeHttpRequest actualDataUploadRequest = actualHttpRequests.get(4);
+        OdpHttpRequest actualDataUploadRequest = actualHttpRequests.get(4);
         assertThat(actualDataUploadRequest.getUri()).isEqualTo(UPLOAD_LOCATION_URI);
-        assertThat(actualReportResultRequest.getHttpMethod()).isEqualTo(HttpMethod.PUT);
+        assertThat(actualReportResultRequest.getHttpMethod())
+                .isEqualTo(HttpClientUtils.HttpMethod.PUT);
         HashMap<String, String> expectedHeaders = new HashMap<>();
         expectedHeaders.put(CONTENT_TYPE_HDR, OCTET_STREAM);
         if (mSupportCompression) {
@@ -707,8 +907,8 @@
 
     @Test
     public void testReportResultFailed() throws Exception {
-        FederatedComputeHttpResponse reportResultHttpResponse =
-                new FederatedComputeHttpResponse.Builder().setStatusCode(503).build();
+        OdpHttpResponse reportResultHttpResponse =
+                new OdpHttpResponse.Builder().setStatusCode(503).build();
         ComputationResult computationResult =
                 new ComputationResult(createOutputCheckpointFile(), FL_RUNNER_SUCCESS_RESULT, null);
 
@@ -764,8 +964,8 @@
                         .get();
 
         // Verify ReportResult request.
-        List<FederatedComputeHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
-        FederatedComputeHttpRequest actualReportResultRequest = actualHttpRequests.get(3);
+        List<OdpHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
+        OdpHttpRequest actualReportResultRequest = actualHttpRequests.get(3);
         checkActualReportResultRequest(actualReportResultRequest);
         ReportResultRequest reportResultRequest =
                 ReportResultRequest.newBuilder()
@@ -808,8 +1008,8 @@
 
         assertThat(reportResultRejection).isNull();
         // Verify ReportResult request.
-        List<FederatedComputeHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
-        FederatedComputeHttpRequest actualReportResultRequest = actualHttpRequests.get(3);
+        List<OdpHttpRequest> actualHttpRequests = mHttpRequestCaptor.getAllValues();
+        OdpHttpRequest actualReportResultRequest = actualHttpRequests.get(3);
         checkActualReportResultRequest(actualReportResultRequest);
         ReportResultRequest reportResultRequest =
                 ReportResultRequest.newBuilder()
@@ -898,8 +1098,8 @@
 
     @Test
     public void testReportResultSuccessUploadFailed() throws Exception {
-        FederatedComputeHttpResponse uploadResultHttpResponse =
-                new FederatedComputeHttpResponse.Builder().setStatusCode(503).build();
+        OdpHttpResponse uploadResultHttpResponse =
+                new OdpHttpResponse.Builder().setStatusCode(503).build();
         ComputationResult computationResult =
                 new ComputationResult(createOutputCheckpointFile(), FL_RUNNER_SUCCESS_RESULT, null);
 
@@ -973,9 +1173,9 @@
         return outputCheckpointFile.getAbsolutePath();
     }
 
-    private FederatedComputeHttpResponse createPlanHttpResponse() {
+    private OdpHttpResponse createPlanHttpResponse() {
         byte[] clientOnlyPlan = TrainingTestUtil.createFederatedAnalyticClientPlan().toByteArray();
-        return new FederatedComputeHttpResponse.Builder()
+        return new OdpHttpResponse.Builder()
                 .setStatusCode(200)
                 .setHeaders(mSupportCompression ? compressionHeaderList() : new HashMap<>())
                 .setPayload(mSupportCompression ? compressWithGzip(clientOnlyPlan) : clientOnlyPlan)
@@ -991,23 +1191,37 @@
                 SUCCESS_EMPTY_HTTP_RESPONSE);
     }
 
-    private FederatedComputeHttpResponse checkpointHttpResponse() {
-        return new FederatedComputeHttpResponse.Builder()
+    private OdpHttpResponse checkpointHttpResponse() {
+        String fileName = createTempFile("input", ".ckp");
+        try (FileOutputStream fos = new FileOutputStream(fileName)) {
+            fos.write(mSupportCompression ? compressWithGzip(CHECKPOINT) : CHECKPOINT);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        return new OdpHttpResponse.Builder()
                 .setStatusCode(200)
-                .setPayload(mSupportCompression ? compressWithGzip(CHECKPOINT) : CHECKPOINT)
+                .setPayloadFileName(fileName)
+                .setHeaders(mSupportCompression ? compressionHeaderList() : new HashMap<>())
+                .build();
+    }
+
+    private OdpHttpResponse checkpointEmptyHttpResponse() {
+        return new OdpHttpResponse.Builder()
+                .setStatusCode(200)
+                .setPayloadFileName(null)
                 .setHeaders(mSupportCompression ? compressionHeaderList() : new HashMap<>())
                 .build();
     }
 
     private void setUpHttpFederatedProtocol(
-            FederatedComputeHttpResponse createTaskAssignmentResponse,
-            FederatedComputeHttpResponse planHttpResponse,
-            FederatedComputeHttpResponse checkpointHttpResponse,
-            FederatedComputeHttpResponse reportResultHttpResponse,
-            FederatedComputeHttpResponse uploadResultHttpResponse) {
+            OdpHttpResponse createTaskAssignmentResponse,
+            OdpHttpResponse planHttpResponse,
+            OdpHttpResponse checkpointHttpResponse,
+            OdpHttpResponse reportResultHttpResponse,
+            OdpHttpResponse uploadResultHttpResponse) {
         doAnswer(
                         invocation -> {
-                            FederatedComputeHttpRequest httpRequest = invocation.getArgument(0);
+                            OdpHttpRequest httpRequest = invocation.getArgument(0);
                             String uri = httpRequest.getUri();
                             // Add sleep for latency metric.
                             Thread.sleep(50);
@@ -1026,6 +1240,20 @@
                         })
                 .when(mMockHttpClient)
                 .performRequestAsyncWithRetry(mHttpRequestCaptor.capture());
+
+        doAnswer(
+                        invocation -> {
+                            OdpHttpRequest httpRequest = invocation.getArgument(0);
+                            String uri = httpRequest.getUri();
+                            // Add sleep for latency metric.
+                            Thread.sleep(50);
+                            if (uri.equals(CHECKPOINT_URI)) {
+                                return immediateFuture(checkpointHttpResponse);
+                            }
+                            return immediateFuture(SUCCESS_EMPTY_HTTP_RESPONSE);
+                        })
+                .when(mMockHttpClient)
+                .performRequestIntoFileAsyncWithRetry(mHttpRequestCaptor.capture());
     }
 
     private HashMap<String, List<String>> compressionHeaderList() {
@@ -1035,7 +1263,7 @@
         return headerList;
     }
 
-    private FederatedComputeHttpResponse createReportResultHttpResponse() {
+    private OdpHttpResponse createReportResultHttpResponse() {
         UploadInstruction.Builder uploadInstruction =
                 UploadInstruction.newBuilder().setUploadLocation(UPLOAD_LOCATION_URI);
         uploadInstruction.putExtraRequestHeaders(CONTENT_TYPE_HDR, OCTET_STREAM);
@@ -1048,13 +1276,13 @@
                 ReportResultResponse.newBuilder()
                         .setUploadInstruction(uploadInstruction.build())
                         .build();
-        return new FederatedComputeHttpResponse.Builder()
+        return new OdpHttpResponse.Builder()
                 .setStatusCode(200)
                 .setPayload(reportResultResponse.toByteArray())
                 .build();
     }
 
-    private FederatedComputeHttpResponse createStartTaskAssignmentHttpResponse() {
+    private OdpHttpResponse createStartTaskAssignmentHttpResponse() {
         CreateTaskAssignmentResponse createTaskAssignmentResponse =
                 createCreateTaskAssignmentResponse(
                         Resource.newBuilder()
@@ -1076,14 +1304,14 @@
                                                         .RESOURCE_COMPRESSION_FORMAT_UNSPECIFIED)
                                 .build());
 
-        return new FederatedComputeHttpResponse.Builder()
+        return new OdpHttpResponse.Builder()
                 .setStatusCode(200)
                 .setPayload(createTaskAssignmentResponse.toByteArray())
                 .build();
     }
 
-    private FederatedComputeHttpResponse createUnauthorizedResponse() {
-        return new FederatedComputeHttpResponse.Builder().setStatusCode(403).build();
+    private OdpHttpResponse createUnauthorizedResponse() {
+        return new OdpHttpResponse.Builder().setStatusCode(403).build();
     }
 
     private CreateTaskAssignmentResponse createCreateTaskAssignmentResponse(
@@ -1109,7 +1337,7 @@
                 .build();
     }
 
-    private FederatedComputeHttpResponse createUnauthenticatedResponse() {
+    private OdpHttpResponse createUnauthenticatedResponse() {
         CreateTaskAssignmentResponse payload =
                 CreateTaskAssignmentResponse.newBuilder()
                         .setRejectionInfo(
@@ -1118,7 +1346,7 @@
                                         .setReason(RejectionReason.Enum.UNAUTHENTICATED)
                                         .build())
                         .build();
-        return new FederatedComputeHttpResponse.Builder()
+        return new OdpHttpResponse.Builder()
                 .setStatusCode(HTTP_UNAUTHENTICATED_STATUS)
                 .setPayload(payload.toByteArray())
                 .setHeaders(new HashMap<>())
@@ -1126,9 +1354,10 @@
     }
 
     private void checkActualTARequest(
-            FederatedComputeHttpRequest actualStartTaskAssignmentRequest, int headerSize) {
+            OdpHttpRequest actualStartTaskAssignmentRequest, int headerSize) {
         assertThat(actualStartTaskAssignmentRequest.getUri()).isEqualTo(START_TASK_ASSIGNMENT_URI);
-        assertThat(actualStartTaskAssignmentRequest.getHttpMethod()).isEqualTo(HttpMethod.POST);
+        assertThat(actualStartTaskAssignmentRequest.getHttpMethod())
+                .isEqualTo(HttpClientUtils.HttpMethod.POST);
 
         // check header
         HashMap<String, String> expectedHeaders = new HashMap<>();
@@ -1149,10 +1378,10 @@
                 .containsAtLeastEntriesIn(expectedHeaders);
     }
 
-    private void checkActualReportResultRequest(
-            FederatedComputeHttpRequest actualReportResultRequest) {
+    private void checkActualReportResultRequest(OdpHttpRequest actualReportResultRequest) {
         assertThat(actualReportResultRequest.getUri()).isEqualTo(REPORT_RESULT_URI);
-        assertThat(actualReportResultRequest.getHttpMethod()).isEqualTo(HttpMethod.PUT);
+        assertThat(actualReportResultRequest.getHttpMethod())
+                .isEqualTo(HttpClientUtils.HttpMethod.PUT);
         HashMap<String, String> expectedHeaders = new HashMap<>();
         expectedHeaders.put(CONTENT_LENGTH_HDR, String.valueOf(7));
         expectedHeaders.put(CONTENT_TYPE_HDR, PROTOBUF_CONTENT_TYPE);
diff --git a/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/ProtocolRequestCreatorTest.java b/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/ProtocolRequestCreatorTest.java
index afcc97c..ce56173 100644
--- a/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/ProtocolRequestCreatorTest.java
+++ b/tests/federatedcomputetests/src/com/android/federatedcompute/services/http/ProtocolRequestCreatorTest.java
@@ -17,14 +17,15 @@
 package com.android.federatedcompute.services.http;
 
 import static com.android.federatedcompute.services.http.HttpClientUtil.CONTENT_LENGTH_HDR;
-import static com.android.federatedcompute.services.http.HttpClientUtil.CONTENT_TYPE_HDR;
-import static com.android.federatedcompute.services.http.HttpClientUtil.PROTOBUF_CONTENT_TYPE;
+import static com.android.odp.module.common.HttpClientUtils.CONTENT_TYPE_HDR;
+import static com.android.odp.module.common.HttpClientUtils.PROTOBUF_CONTENT_TYPE;
 
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
 
-import com.android.federatedcompute.services.http.HttpClientUtil.HttpMethod;
+import com.android.odp.module.common.HttpClientUtils;
+import com.android.odp.module.common.OdpHttpRequest;
 
 import com.google.internal.federatedcompute.v1.ForwardingInfo;
 
@@ -45,12 +46,12 @@
         ProtocolRequestCreator requestCreator =
                 new ProtocolRequestCreator(REQUEST_BASE_URI, new HashMap<String, String>());
 
-        FederatedComputeHttpRequest request =
+        OdpHttpRequest request =
                 requestCreator.createProtoRequest(
-                        "/v1/request", HttpMethod.POST, REQUEST_BODY, true);
+                        "/v1/request", HttpClientUtils.HttpMethod.POST, REQUEST_BODY, true);
 
         assertThat(request.getUri()).isEqualTo("https://initial.uri/v1/request");
-        assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.POST);
+        assertThat(request.getHttpMethod()).isEqualTo(HttpClientUtils.HttpMethod.POST);
         assertThat(request.getBody()).isEqualTo(REQUEST_BODY);
         HashMap<String, String> expectedHeaders = new HashMap<String, String>();
         expectedHeaders.put(CONTENT_LENGTH_HDR, String.valueOf(12));
@@ -80,7 +81,10 @@
                         IllegalArgumentException.class,
                         () ->
                                 requestCreator.createProtoRequest(
-                                        "v1/request", HttpMethod.POST, REQUEST_BODY, false));
+                                        "v1/request",
+                                        HttpClientUtils.HttpMethod.POST,
+                                        REQUEST_BODY,
+                                        false));
 
         assertThat(exception)
                 .hasMessageThat()
@@ -93,9 +97,9 @@
                 ForwardingInfo.newBuilder().setTargetUriPrefix(AGGREGATION_TARGET_URI).build();
         ProtocolRequestCreator requestCreator = ProtocolRequestCreator.create(forwardingInfo);
 
-        FederatedComputeHttpRequest request =
+        OdpHttpRequest request =
                 requestCreator.createProtoRequest(
-                        "/v1/request", HttpMethod.POST, REQUEST_BODY, false);
+                        "/v1/request", HttpClientUtils.HttpMethod.POST, REQUEST_BODY, false);
 
         assertThat(request.getUri()).isEqualTo("https://aggregation.uri/v1/request");
     }
@@ -105,12 +109,12 @@
         ProtocolRequestCreator requestCreator =
                 new ProtocolRequestCreator(REQUEST_BASE_URI, new HashMap<String, String>());
 
-        FederatedComputeHttpRequest request =
+        OdpHttpRequest request =
                 requestCreator.createProtoRequest(
-                        "/v1/request", HttpMethod.POST, REQUEST_BODY, true);
+                        "/v1/request", HttpClientUtils.HttpMethod.POST, REQUEST_BODY, true);
 
         assertThat(request.getUri()).isEqualTo("https://initial.uri/v1/request");
-        assertThat(request.getHttpMethod()).isEqualTo(HttpMethod.POST);
+        assertThat(request.getHttpMethod()).isEqualTo(HttpClientUtils.HttpMethod.POST);
         assertThat(request.getBody()).isEqualTo(REQUEST_BODY);
         HashMap<String, String> expectedHeaders = new HashMap<String, String>();
         expectedHeaders.put(CONTENT_LENGTH_HDR, String.valueOf(12));
diff --git a/tests/federatedcomputetests/src/com/android/federatedcompute/services/scheduling/JobSchedulerHelperTest.java b/tests/federatedcomputetests/src/com/android/federatedcompute/services/scheduling/JobSchedulerHelperTest.java
index b97d5c6..0b38ec7 100644
--- a/tests/federatedcomputetests/src/com/android/federatedcompute/services/scheduling/JobSchedulerHelperTest.java
+++ b/tests/federatedcomputetests/src/com/android/federatedcompute/services/scheduling/JobSchedulerHelperTest.java
@@ -83,6 +83,23 @@
                     .constraints(TRAINING_CONSTRAINTS)
                     .build();
 
+    private static final FederatedTrainingTask TRAINING_TASK_EARLY_RUN =
+            FederatedTrainingTask.builder()
+                    .appPackageName(PACKAGE_NAME)
+                    .populationName(POPULATION_NAME)
+                    .intervalOptions(INTERVAL_OPTIONS)
+                    .creationTime(CURRENT_TIME_MILLIS)
+                    .lastScheduledTime(CURRENT_TIME_MILLIS)
+                    .schedulingReason(SCHEDULING_REASON)
+                    .jobId(JOB_ID)
+                    .ownerPackageName(OWNER_PACKAGE)
+                    .ownerClassName(OWNER_CLASS)
+                    .ownerIdCertDigest(OWNER_ID_CERT_DIGEST)
+                    .serverAddress(SERVER_ADDRESS)
+                    .earliestNextRunTime(0L)
+                    .constraints(TRAINING_CONSTRAINTS)
+                    .build();
+
     private JobSchedulerHelper mJobSchedulerHelper;
     private JobScheduler mJobScheduler;
     private Context mContext;
@@ -108,6 +125,15 @@
     }
 
     @Test
+    public void scheduleTaskEarlyRun() {
+        assertThat(mJobSchedulerHelper.scheduleTask(mContext, TRAINING_TASK_EARLY_RUN)).isTrue();
+
+        JobInfo jobInfo = Iterables.getOnlyElement(mJobScheduler.getAllPendingJobs());
+
+        verifyJobInfo(jobInfo);
+    }
+
+    @Test
     public void schedule_collides_sameService_success() {
         mJobSchedulerHelper.scheduleTask(mContext, TRAINING_TASK);
         // Schedule a job with same job id.
diff --git a/tests/federatedcomputetests/src/com/android/federatedcompute/services/training/EligibilityDeciderTest.java b/tests/federatedcomputetests/src/com/android/federatedcompute/services/training/EligibilityDeciderTest.java
index af36426..eb44bf8 100644
--- a/tests/federatedcomputetests/src/com/android/federatedcompute/services/training/EligibilityDeciderTest.java
+++ b/tests/federatedcomputetests/src/com/android/federatedcompute/services/training/EligibilityDeciderTest.java
@@ -19,6 +19,10 @@
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_ELIGIBILITY_EVAL_COMPUTATION_COMPLETED;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_ELIGIBILITY_EVAL_COMPUTATION_ELIGIBLE;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_ELIGIBILITY_EVAL_COMPUTATION_STARTED;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_START;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_SUCCESS;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_START;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_SUCCESS;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -356,11 +360,16 @@
         assertTrue(result.isEligible());
 
         ArgumentCaptor<Integer> eventKindCaptor = ArgumentCaptor.forClass(Integer.class);
-        verify(mMockTrainingEventLogger, times(2)).logEventKind(eventKindCaptor.capture());
+        verify(mMockTrainingEventLogger, times(6)).logEventKind(eventKindCaptor.capture());
         assertThat(eventKindCaptor.getAllValues())
-                .containsAtLeast(
+                .containsExactly(
                         FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_ELIGIBILITY_EVAL_COMPUTATION_STARTED,
-                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_ELIGIBILITY_EVAL_COMPUTATION_ELIGIBLE);
+                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_ELIGIBILITY_EVAL_COMPUTATION_ELIGIBLE,
+                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_START,
+                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_SUCCESS,
+                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_START,
+                        FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_SUCCESS);
+
         ArgumentCaptor<ExampleStats> exampleStatsCaptor =
                 ArgumentCaptor.forClass(ExampleStats.class);
         verify(mMockTrainingEventLogger)
diff --git a/tests/federatedcomputetests/src/com/android/federatedcompute/services/training/FederatedComputeWorkerTest.java b/tests/federatedcomputetests/src/com/android/federatedcompute/services/training/FederatedComputeWorkerTest.java
index 5373db3..d151a3d 100644
--- a/tests/federatedcomputetests/src/com/android/federatedcompute/services/training/FederatedComputeWorkerTest.java
+++ b/tests/federatedcomputetests/src/com/android/federatedcompute/services/training/FederatedComputeWorkerTest.java
@@ -21,12 +21,18 @@
 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__CLIENT_PLAN_SPEC_ERROR;
 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ISOLATED_TRAINING_PROCESS_ERROR;
 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__FEDERATED_COMPUTE;
-import static com.android.federatedcompute.services.common.FileUtils.createTempFile;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_COMPUTATION_STARTED;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_ELIGIBILITY_EVAL_COMPUTATION_ELIGIBLE;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_ELIGIBILITY_EVAL_COMPUTATION_STARTED;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_START;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_SUCCESS;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_START;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_SUCCESS;
 import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_COMPLETE;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_COMPUTATION_FAILED;
+import static com.android.federatedcompute.services.stats.FederatedComputeStatsLog.FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_STARTED;
 import static com.android.federatedcompute.services.testutils.TrainingTestUtil.COLLECTION_URI;
+import static com.android.odp.module.common.FileUtils.createTempFile;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
@@ -733,13 +739,19 @@
                         anyInt(), anyString(), any(), any(), eq(ContributionResult.FAIL), eq(true));
 
         ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);
-        verify(mMockTrainingEventLogger, times(3)).logEventKind(captor.capture());
+        verify(mMockTrainingEventLogger, times(9)).logEventKind(captor.capture());
         assertThat(captor.getAllValues())
                 .containsExactlyElementsIn(
                         Arrays.asList(
                                 FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_COMPUTATION_STARTED,
                                 FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_ELIGIBILITY_EVAL_COMPUTATION_ELIGIBLE,
-                                FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_ELIGIBILITY_EVAL_COMPUTATION_STARTED));
+                                FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_ELIGIBILITY_EVAL_COMPUTATION_STARTED,
+                                FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_START,
+                                FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_BIND_SUCCESS,
+                                FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_START,
+                                FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_EXAMPLE_STORE_START_QUERY_SUCCESS,
+                                FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_FAILED_COMPUTATION_FAILED,
+                                FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED__KIND__TRAIN_RUN_STARTED));
         verify(mMockTrainingEventLogger).logComputationInvalidArgument(any());
     }
 
diff --git a/tests/federatedcomputetests/src/com/android/federatedcompute/services/training/IsolatedTrainingServiceImplTest.java b/tests/federatedcomputetests/src/com/android/federatedcompute/services/training/IsolatedTrainingServiceImplTest.java
index f609c3b..0252727 100644
--- a/tests/federatedcomputetests/src/com/android/federatedcompute/services/training/IsolatedTrainingServiceImplTest.java
+++ b/tests/federatedcomputetests/src/com/android/federatedcompute/services/training/IsolatedTrainingServiceImplTest.java
@@ -32,13 +32,13 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.federatedcompute.services.common.Constants;
 import com.android.federatedcompute.services.common.FederatedComputeExecutors;
-import com.android.federatedcompute.services.common.FileUtils;
 import com.android.federatedcompute.services.data.fbs.TrainingFlags;
 import com.android.federatedcompute.services.testutils.FakeExampleStoreIterator;
 import com.android.federatedcompute.services.testutils.TrainingTestUtil;
 import com.android.federatedcompute.services.training.aidl.ITrainingResultCallback;
 import com.android.modules.utils.testing.ExtendedMockitoRule;
 import com.android.modules.utils.testing.ExtendedMockitoRule.MockStatic;
+import com.android.odp.module.common.FileUtils;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.MoreExecutors;
diff --git a/tests/frameworktests/Android.bp b/tests/frameworktests/Android.bp
index 7ab946b..94c2dcc 100644
--- a/tests/frameworktests/Android.bp
+++ b/tests/frameworktests/Android.bp
@@ -23,7 +23,7 @@
     name: "FrameworkOnDevicePersonalizationTests",
     srcs: [
         "**/*.java",
-        "**/*.aidl"
+        "**/*.aidl",
     ],
     defaults: ["framework-ondevicepersonalization-test-defaults"],
     min_sdk_version: "Tiramisu",
@@ -35,12 +35,14 @@
         "androidx.test.rules",
         "frameworks-base-testutils",
         "guava",
+        "hamcrest-library",
         "libprotobuf-java-lite",
         "tensorflow_core_proto_java_lite",
         "mockito-target-minus-junit4",
         "ondevicepersonalization-testing-utils",
         "truth",
         "compatibility-device-util-axt",
+        "platform-compat-test-rules",
     ],
     libs: [
         "android.test.runner.stubs",
diff --git a/tests/frameworktests/AndroidManifest.xml b/tests/frameworktests/AndroidManifest.xml
index 52a1c59..9094e39 100644
--- a/tests/frameworktests/AndroidManifest.xml
+++ b/tests/frameworktests/AndroidManifest.xml
@@ -17,7 +17,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="android.ondevicepersonalization">
 
-    <application android:label="FrameworkOnDevicePersonalizationTests">
+    <application android:debuggable="true" android:label="FrameworkOnDevicePersonalizationTests">
         <uses-library android:name="android.test.runner" />
         <service
             android:name="android.adservices.ondevicepersonalization.IsolatedServiceExceptionSafetyTestImpl"
diff --git a/tests/frameworktests/src/android/adservices/ondevicepersonalization/ExecuteInIsolatedServiceRequestTest.java b/tests/frameworktests/src/android/adservices/ondevicepersonalization/ExecuteInIsolatedServiceRequestTest.java
new file mode 100644
index 0000000..8cb7f69
--- /dev/null
+++ b/tests/frameworktests/src/android/adservices/ondevicepersonalization/ExecuteInIsolatedServiceRequestTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2024 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.adservices.ondevicepersonalization;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.ComponentName;
+import android.os.PersistableBundle;
+
+import org.junit.Test;
+
+public class ExecuteInIsolatedServiceRequestTest {
+    private static final ComponentName COMPONENT_NAME =
+            ComponentName.createRelative("com.example.service", ".Example");
+
+    @Test
+    public void buildRequestWithOption_success() {
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putString("key", "ok");
+
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(COMPONENT_NAME)
+                        .setAppParams(bundle)
+                        .setOutputSpec(
+                                ExecuteInIsolatedServiceRequest.OutputSpec.buildBestValueSpec(100))
+                        .build();
+
+        ExecuteInIsolatedServiceRequest.OutputSpec options = request.getOutputSpec();
+        assertThat(options.getMaxIntValue()).isEqualTo(100);
+        assertThat(options.getOutputType())
+                .isEqualTo(ExecuteInIsolatedServiceRequest.OutputSpec.OUTPUT_TYPE_BEST_VALUE);
+        assertThat(request.getAppParams()).isEqualTo(bundle);
+        assertThat(request.getService()).isEqualTo(COMPONENT_NAME);
+    }
+
+    @Test
+    public void buildRequestWithoutOption_success() {
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putString("key", "ok");
+
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(COMPONENT_NAME)
+                        .setAppParams(bundle)
+                        .build();
+
+        ExecuteInIsolatedServiceRequest.OutputSpec options = request.getOutputSpec();
+        assertThat(options).isEqualTo(ExecuteInIsolatedServiceRequest.OutputSpec.DEFAULT);
+        assertThat(request.getAppParams()).isEqualTo(bundle);
+        assertThat(request.getService()).isEqualTo(COMPONENT_NAME);
+    }
+
+    @Test
+    public void buildRequest_noParams_success() {
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(COMPONENT_NAME).build();
+
+        assertThat(request.getService()).isEqualTo(COMPONENT_NAME);
+    }
+}
diff --git a/tests/frameworktests/src/android/adservices/ondevicepersonalization/ExecuteInIsolatedServiceResponseTest.java b/tests/frameworktests/src/android/adservices/ondevicepersonalization/ExecuteInIsolatedServiceResponseTest.java
new file mode 100644
index 0000000..5781f57
--- /dev/null
+++ b/tests/frameworktests/src/android/adservices/ondevicepersonalization/ExecuteInIsolatedServiceResponseTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2024 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.adservices.ondevicepersonalization;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class ExecuteInIsolatedServiceResponseTest {
+
+    @Test
+    public void response() {
+        ExecuteInIsolatedServiceResponse response =
+                new ExecuteInIsolatedServiceResponse(new SurfacePackageToken("aaaa"));
+
+        assertThat(response.getBestValue()).isEqualTo(-1);
+        assertThat(response.getSurfacePackageToken().getTokenString()).isEqualTo("aaaa");
+    }
+
+    @Test
+    public void responseWithBestValue() {
+        ExecuteInIsolatedServiceResponse response =
+                new ExecuteInIsolatedServiceResponse(new SurfacePackageToken("aaaa"), 20);
+
+        assertThat(response.getBestValue()).isEqualTo(20);
+        assertThat(response.getSurfacePackageToken().getTokenString()).isEqualTo("aaaa");
+    }
+
+    @Test
+    public void responseIsNull() {
+        ExecuteInIsolatedServiceResponse response = new ExecuteInIsolatedServiceResponse(null);
+
+        assertThat(response.getBestValue()).isEqualTo(-1);
+        assertThat(response.getSurfacePackageToken()).isNull();
+    }
+}
diff --git a/tests/frameworktests/src/android/adservices/ondevicepersonalization/FederatedComputeSchedulerTest.java b/tests/frameworktests/src/android/adservices/ondevicepersonalization/FederatedComputeSchedulerTest.java
index 4f65626..45d45cc 100644
--- a/tests/frameworktests/src/android/adservices/ondevicepersonalization/FederatedComputeSchedulerTest.java
+++ b/tests/frameworktests/src/android/adservices/ondevicepersonalization/FederatedComputeSchedulerTest.java
@@ -18,7 +18,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 
 import android.adservices.ondevicepersonalization.aidl.IDataAccessService;
 import android.adservices.ondevicepersonalization.aidl.IDataAccessServiceCallback;
@@ -31,16 +35,39 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.ondevicepersonalization.testing.utils.ResultReceiver;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.time.Duration;
 
-/** Unit Tests of RemoteData API. */
+/** Unit Tests for {@link FederatedComputeScheduler}. */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class FederatedComputeSchedulerTest {
-    FederatedComputeScheduler mFederatedComputeScheduler =
+
+    private static final String VALID_POPULATION_NAME = "population";
+    private static final String ERROR_POPULATION_NAME = "err";
+
+    private static final String INVALID_MANIFEST_ERROR_POPULATION_NAME = "manifest_error";
+    private static final String POPULATION_NAME_PRIVACY_NOT_ELIGIBLE = "privacy_not_eligible";
+
+    private static final TrainingInterval TEST_TRAINING_INTERVAL =
+            new TrainingInterval.Builder()
+                    .setMinimumInterval(Duration.ofHours(10))
+                    .setSchedulingMode(TrainingInterval.SCHEDULING_MODE_ONE_TIME)
+                    .build();
+
+    private static final FederatedComputeScheduler.Params TEST_SCHEDULER_PARAMS =
+            new FederatedComputeScheduler.Params(TEST_TRAINING_INTERVAL);
+
+    private static final FederatedComputeInput TEST_FC_INPUT =
+            new FederatedComputeInput.Builder().setPopulationName(VALID_POPULATION_NAME).build();
+    private static final FederatedComputeScheduleRequest TEST_SCHEDULE_INPUT =
+            new FederatedComputeScheduleRequest(TEST_SCHEDULER_PARAMS, VALID_POPULATION_NAME);
+
+    private final FederatedComputeScheduler mFederatedComputeScheduler =
             new FederatedComputeScheduler(
                     IFederatedComputeService.Stub.asInterface(new FederatedComputeService()),
                     IDataAccessService.Stub.asInterface(new TestDataService()));
@@ -48,86 +75,158 @@
     private boolean mCancelCalled = false;
     private boolean mScheduleCalled = false;
     private boolean mLogApiCalled = false;
+    private int mResponseCode = Constants.STATUS_SUCCESS;
 
     @Test
     public void testScheduleSuccess() {
-        TrainingInterval interval =
-                new TrainingInterval.Builder()
-                        .setMinimumInterval(Duration.ofHours(10))
-                        .setSchedulingMode(TrainingInterval.SCHEDULING_MODE_ONE_TIME)
-                        .build();
-        FederatedComputeScheduler.Params params = new FederatedComputeScheduler.Params(interval);
-        FederatedComputeInput input =
-                new FederatedComputeInput.Builder().setPopulationName("population").build();
-        mFederatedComputeScheduler.schedule(params, input);
+        mFederatedComputeScheduler.schedule(TEST_SCHEDULER_PARAMS, TEST_FC_INPUT);
+
         assertThat(mScheduleCalled).isTrue();
         assertThat(mLogApiCalled).isTrue();
+        assertThat(mResponseCode).isEqualTo(Constants.STATUS_SUCCESS);
+    }
+
+    @Test
+    public void testSchedule_withOutcomeReceiver_success() throws Exception {
+        var receiver = new ResultReceiver();
+
+        mFederatedComputeScheduler.schedule(TEST_SCHEDULE_INPUT, receiver);
+
+        assertNotNull(receiver.getResult());
+        assertTrue(receiver.isSuccess());
+        assertThat(mScheduleCalled).isTrue();
+        assertThat(mLogApiCalled).isTrue();
+        assertThat(mResponseCode).isEqualTo(Constants.STATUS_SUCCESS);
+    }
+
+    @Test
+    public void testSchedule_withOutcomeReceiver_error() throws Exception {
+        FederatedComputeScheduleRequest scheduleInput =
+                new FederatedComputeScheduleRequest(TEST_SCHEDULER_PARAMS, ERROR_POPULATION_NAME);
+        var receiver = new ResultReceiver();
+
+        mFederatedComputeScheduler.schedule(scheduleInput, receiver);
+
+        assertNull(receiver.getResult());
+        assertTrue(receiver.isError());
+        assertTrue(receiver.getException() instanceof OnDevicePersonalizationException);
+        assertEquals(
+                OnDevicePersonalizationException.ERROR_SCHEDULE_TRAINING_FAILED,
+                ((OnDevicePersonalizationException) receiver.getException()).getErrorCode());
+        assertThat(mScheduleCalled).isTrue();
+        assertThat(mLogApiCalled).isTrue();
+        assertThat(mResponseCode).isEqualTo(Constants.STATUS_INTERNAL_ERROR);
+    }
+
+    @Test
+    public void testSchedule_withOutcomeReceiver_manifestError() throws Exception {
+        FederatedComputeScheduleRequest scheduleInput =
+                new FederatedComputeScheduleRequest(
+                        TEST_SCHEDULER_PARAMS, INVALID_MANIFEST_ERROR_POPULATION_NAME);
+        var receiver = new ResultReceiver();
+
+        mFederatedComputeScheduler.schedule(scheduleInput, receiver);
+
+        assertNull(receiver.getResult());
+        assertTrue(receiver.isError());
+        assertTrue(receiver.getException() instanceof OnDevicePersonalizationException);
+        assertEquals(
+                OnDevicePersonalizationException.ERROR_INVALID_TRAINING_MANIFEST,
+                ((OnDevicePersonalizationException) receiver.getException()).getErrorCode());
+        assertThat(mScheduleCalled).isTrue();
+        assertThat(mLogApiCalled).isTrue();
+        assertThat(mResponseCode).isEqualTo(Constants.STATUS_FCP_MANIFEST_INVALID);
     }
 
     @Test
     public void testScheduleNull() {
         FederatedComputeScheduler fcs = new FederatedComputeScheduler(null, new TestDataService());
-        TrainingInterval interval =
-                new TrainingInterval.Builder()
-                        .setMinimumInterval(Duration.ofHours(10))
-                        .setSchedulingMode(TrainingInterval.SCHEDULING_MODE_ONE_TIME)
-                        .build();
-        FederatedComputeScheduler.Params params = new FederatedComputeScheduler.Params(interval);
-        FederatedComputeInput input =
-                new FederatedComputeInput.Builder().setPopulationName("population").build();
-        assertThrows(IllegalStateException.class, () -> fcs.schedule(params, input));
+
+        assertThrows(
+                IllegalStateException.class,
+                () -> fcs.schedule(TEST_SCHEDULER_PARAMS, TEST_FC_INPUT));
+        assertThat(mResponseCode).isEqualTo(Constants.STATUS_INTERNAL_ERROR);
     }
 
     @Test
-    public void testScheduleErr() {
-        TrainingInterval interval =
-                new TrainingInterval.Builder()
-                        .setMinimumInterval(Duration.ofHours(10))
-                        .setSchedulingMode(TrainingInterval.SCHEDULING_MODE_ONE_TIME)
-                        .build();
-        FederatedComputeScheduler.Params params = new FederatedComputeScheduler.Params(interval);
+    public void testScheduleError() {
         FederatedComputeInput input =
-                new FederatedComputeInput.Builder().setPopulationName("err").build();
-        mFederatedComputeScheduler.schedule(params, input);
+                new FederatedComputeInput.Builder()
+                        .setPopulationName(ERROR_POPULATION_NAME)
+                        .build();
+
+        mFederatedComputeScheduler.schedule(TEST_SCHEDULER_PARAMS, input);
+
         assertThat(mScheduleCalled).isTrue();
         assertThat(mLogApiCalled).isTrue();
+        assertThat(mResponseCode).isEqualTo(Constants.STATUS_INTERNAL_ERROR);
+    }
+
+    @Test
+    public void testSchedulePrivacyNotEligible() {
+        FederatedComputeInput input =
+                new FederatedComputeInput.Builder()
+                        .setPopulationName(POPULATION_NAME_PRIVACY_NOT_ELIGIBLE)
+                        .build();
+
+        mFederatedComputeScheduler.schedule(TEST_SCHEDULER_PARAMS, input);
+
+        assertThat(mScheduleCalled).isTrue();
+        assertThat(mLogApiCalled).isTrue();
+        assertThat(mResponseCode).isEqualTo(Constants.STATUS_PERSONALIZATION_DISABLED);
     }
 
     @Test
     public void testCancelSuccess() {
-        FederatedComputeInput input =
-                new FederatedComputeInput.Builder().setPopulationName("population").build();
-        mFederatedComputeScheduler.cancel(input);
+        mFederatedComputeScheduler.cancel(TEST_FC_INPUT);
+
         assertThat(mCancelCalled).isTrue();
         assertThat(mLogApiCalled).isTrue();
+        assertThat(mResponseCode).isEqualTo(Constants.STATUS_SUCCESS);
     }
 
     @Test
     public void testCancelNull() {
-        FederatedComputeInput input =
-                new FederatedComputeInput.Builder().setPopulationName("population").build();
         FederatedComputeScheduler fcs = new FederatedComputeScheduler(null, new TestDataService());
-        assertThrows(IllegalStateException.class, () -> fcs.cancel(input));
+
+        assertThrows(IllegalStateException.class, () -> fcs.cancel(TEST_FC_INPUT));
+        assertThat(mResponseCode).isEqualTo(Constants.STATUS_INTERNAL_ERROR);
     }
 
     @Test
-    public void testCancelErr() {
+    public void testCancelError() {
         FederatedComputeInput input =
-                new FederatedComputeInput.Builder().setPopulationName("err").build();
+                new FederatedComputeInput.Builder()
+                        .setPopulationName(ERROR_POPULATION_NAME)
+                        .build();
+
         mFederatedComputeScheduler.cancel(input);
+
         assertThat(mCancelCalled).isTrue();
         assertThat(mLogApiCalled).isTrue();
+        assertThat(mResponseCode).isEqualTo(Constants.STATUS_INTERNAL_ERROR);
     }
 
-    class FederatedComputeService extends IFederatedComputeService.Stub {
+    private class FederatedComputeService extends IFederatedComputeService.Stub {
         @Override
         public void schedule(
                 TrainingOptions trainingOptions,
                 IFederatedComputeCallback iFederatedComputeCallback)
                 throws RemoteException {
             mScheduleCalled = true;
-            if (trainingOptions.getPopulationName().equals("err")) {
-                iFederatedComputeCallback.onFailure(1);
+            if (trainingOptions.getPopulationName().equals(ERROR_POPULATION_NAME)) {
+                iFederatedComputeCallback.onFailure(Constants.STATUS_INTERNAL_ERROR);
+                return;
+            }
+            if (trainingOptions.getPopulationName().equals(POPULATION_NAME_PRIVACY_NOT_ELIGIBLE)) {
+                iFederatedComputeCallback.onFailure(Constants.STATUS_PERSONALIZATION_DISABLED);
+                return;
+            }
+            if (trainingOptions
+                    .getPopulationName()
+                    .equals(INVALID_MANIFEST_ERROR_POPULATION_NAME)) {
+                iFederatedComputeCallback.onFailure(Constants.STATUS_FCP_MANIFEST_INVALID);
+                return;
             }
             iFederatedComputeCallback.onSuccess();
         }
@@ -136,20 +235,23 @@
         public void cancel(String s, IFederatedComputeCallback iFederatedComputeCallback)
                 throws RemoteException {
             mCancelCalled = true;
-            if (s.equals("err")) {
+            if (s.equals(ERROR_POPULATION_NAME)) {
                 iFederatedComputeCallback.onFailure(1);
+                return;
             }
             iFederatedComputeCallback.onSuccess();
         }
     }
 
-    class TestDataService extends IDataAccessService.Stub {
+    private class TestDataService extends IDataAccessService.Stub {
+
         @Override
         public void onRequest(int operation, Bundle params, IDataAccessServiceCallback callback) {}
 
         @Override
         public void logApiCallStats(int apiName, long latencyMillis, int responseCode) {
             mLogApiCalled = true;
+            mResponseCode = responseCode;
         }
     }
 }
diff --git a/tests/frameworktests/src/android/adservices/ondevicepersonalization/IsolatedServiceExceptionSafetyTest.java b/tests/frameworktests/src/android/adservices/ondevicepersonalization/IsolatedServiceExceptionSafetyTest.java
index 441d3ac..7a53369 100644
--- a/tests/frameworktests/src/android/adservices/ondevicepersonalization/IsolatedServiceExceptionSafetyTest.java
+++ b/tests/frameworktests/src/android/adservices/ondevicepersonalization/IsolatedServiceExceptionSafetyTest.java
@@ -63,6 +63,7 @@
     private AbstractServiceBinder<IIsolatedService> mServiceBinder;
     private int mCallbackErrorCode;
     private int mIsolatedServiceErrorCode;
+    private byte[] mSerializedExceptionInfo;
     private CountDownLatch mLatch;
 
     @Parameterized.Parameter(0)
@@ -207,9 +208,13 @@
         }
 
         @Override
-        public void onError(int errorCode, int isolatedServiceErrorCode) {
+        public void onError(
+                int errorCode,
+                int isolatedServiceErrorCode,
+                byte[] serializedExceptionInfo) {
             mCallbackErrorCode = errorCode;
             mIsolatedServiceErrorCode = isolatedServiceErrorCode;
+            mSerializedExceptionInfo = serializedExceptionInfo;
             mLatch.countDown();
         }
     }
diff --git a/tests/frameworktests/src/android/adservices/ondevicepersonalization/IsolatedServiceTest.java b/tests/frameworktests/src/android/adservices/ondevicepersonalization/IsolatedServiceTest.java
index ced94f1..b44776c 100644
--- a/tests/frameworktests/src/android/adservices/ondevicepersonalization/IsolatedServiceTest.java
+++ b/tests/frameworktests/src/android/adservices/ondevicepersonalization/IsolatedServiceTest.java
@@ -70,6 +70,7 @@
     private Bundle mCallbackResult;
     private int mCallbackErrorCode;
     private int mIsolatedServiceErrorCode;
+    private byte[] mSerializedExceptionInfo;
 
     @Before
     public void setUp() {
@@ -742,9 +743,13 @@
         }
 
         @Override
-        public void onError(int errorCode, int isolatedServiceErrorCode) {
+        public void onError(
+                int errorCode,
+                int isolatedServiceErrorCode,
+                byte[] serializedExceptionInfo) {
             mCallbackErrorCode = errorCode;
             mIsolatedServiceErrorCode = isolatedServiceErrorCode;
+            mSerializedExceptionInfo = serializedExceptionInfo;
             mLatch.countDown();
         }
     }
diff --git a/tests/frameworktests/src/android/adservices/ondevicepersonalization/ModelManagerTest.java b/tests/frameworktests/src/android/adservices/ondevicepersonalization/ModelManagerTest.java
index 0a7cdb6..2ea5885 100644
--- a/tests/frameworktests/src/android/adservices/ondevicepersonalization/ModelManagerTest.java
+++ b/tests/frameworktests/src/android/adservices/ondevicepersonalization/ModelManagerTest.java
@@ -16,9 +16,10 @@
 
 package android.adservices.ondevicepersonalization;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
@@ -27,12 +28,13 @@
 import android.adservices.ondevicepersonalization.aidl.IIsolatedModelService;
 import android.adservices.ondevicepersonalization.aidl.IIsolatedModelServiceCallback;
 import android.os.Bundle;
-import android.os.OutcomeReceiver;
 import android.os.RemoteException;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import com.android.ondevicepersonalization.testing.utils.ResultReceiver;
+
 import com.google.common.util.concurrent.MoreExecutors;
 
 import org.junit.Before;
@@ -40,7 +42,6 @@
 import org.junit.runner.RunWith;
 
 import java.util.HashMap;
-import java.util.concurrent.CountDownLatch;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -76,13 +77,11 @@
                                 new InferenceOutput.Builder().setDataOutputs(outputData).build())
                         .build();
 
-        var callback = new MyTestCallback();
+        var callback = new ResultReceiver<InferenceOutput>();
         mModelManager.run(inferenceContext, MoreExecutors.directExecutor(), callback);
 
-        callback.mLatch.await();
-        assertTrue(mRunInferenceCalled);
-        assertNotNull(callback.mInferenceOutput);
-        float[] value = (float[]) callback.mInferenceOutput.getDataOutputs().get(0);
+        assertTrue(callback.isSuccess());
+        float[] value = (float[]) callback.getResult().getDataOutputs().get(0);
         assertEquals(value[0], 5.0f, 0.01f);
     }
 
@@ -100,20 +99,22 @@
                                 new InferenceOutput.Builder().setDataOutputs(outputData).build())
                         .build();
 
-        var callback = new MyTestCallback();
+        var callback = new ResultReceiver<InferenceOutput>();
         mModelManager.run(inferenceContext, MoreExecutors.directExecutor(), callback);
 
-        callback.mLatch.await();
-        assertTrue(callback.mError);
+        assertTrue(callback.isError());
+        OnDevicePersonalizationException exception =
+                (OnDevicePersonalizationException) callback.getException();
+        assertThat(exception.getErrorCode())
+                .isEqualTo(OnDevicePersonalizationException.ERROR_INFERENCE_MODEL_NOT_FOUND);
     }
 
     @Test
     public void runInference_contextNull_throw() {
+        var callback = new ResultReceiver<InferenceOutput>();
         assertThrows(
                 NullPointerException.class,
-                () ->
-                        mModelManager.run(
-                                null, MoreExecutors.directExecutor(), new MyTestCallback()));
+                () -> mModelManager.run(null, MoreExecutors.directExecutor(), callback));
     }
 
     @Test
@@ -130,29 +131,13 @@
                                 new InferenceOutput.Builder().setDataOutputs(outputData).build())
                         .build();
 
-        var callback = new MyTestCallback();
+        var callback = new ResultReceiver<InferenceOutput>();
         mModelManager.run(inferenceContext, MoreExecutors.directExecutor(), callback);
 
-        callback.mLatch.await();
-        assertTrue(callback.mError);
-    }
-
-    public class MyTestCallback implements OutcomeReceiver<InferenceOutput, Exception> {
-        public boolean mError = false;
-        public InferenceOutput mInferenceOutput = null;
-        private final CountDownLatch mLatch = new CountDownLatch(1);
-
-        @Override
-        public void onResult(InferenceOutput result) {
-            mInferenceOutput = result;
-            mLatch.countDown();
-        }
-
-        @Override
-        public void onError(Exception error) {
-            mError = true;
-            mLatch.countDown();
-        }
+        OnDevicePersonalizationException exception =
+                (OnDevicePersonalizationException) callback.getException();
+        assertThat(exception.getErrorCode())
+                .isEqualTo(OnDevicePersonalizationException.ERROR_INFERENCE_FAILED);
     }
 
     class TestIsolatedModelService extends IIsolatedModelService.Stub {
@@ -164,11 +149,11 @@
                     params.getParcelable(
                             Constants.EXTRA_INFERENCE_INPUT, InferenceInputParcel.class);
             if (inputParcel.getModelId().getKey().equals(INVALID_MODEL_KEY)) {
-                callback.onError(Constants.STATUS_INTERNAL_ERROR);
+                callback.onError(OnDevicePersonalizationException.ERROR_INFERENCE_MODEL_NOT_FOUND);
                 return;
             }
             if (inputParcel.getModelId().getKey().equals(MISSING_OUTPUT_KEY)) {
-                callback.onSuccess(new Bundle());
+                callback.onError(OnDevicePersonalizationException.ERROR_INFERENCE_FAILED);
                 return;
             }
             HashMap<Integer, Object> result = new HashMap<>();
@@ -185,6 +170,7 @@
     static class TestDataAccessService extends IDataAccessService.Stub {
         @Override
         public void onRequest(int operation, Bundle params, IDataAccessServiceCallback callback) {}
+
         @Override
         public void logApiCallStats(int apiName, long latencyMillis, int responseCode) {}
     }
diff --git a/tests/frameworktests/src/android/adservices/ondevicepersonalization/OnDevicePersonalizationConfigManagerTest.java b/tests/frameworktests/src/android/adservices/ondevicepersonalization/OnDevicePersonalizationConfigManagerTest.java
index 6832f47..dffa3d1 100644
--- a/tests/frameworktests/src/android/adservices/ondevicepersonalization/OnDevicePersonalizationConfigManagerTest.java
+++ b/tests/frameworktests/src/android/adservices/ondevicepersonalization/OnDevicePersonalizationConfigManagerTest.java
@@ -16,120 +16,28 @@
 
 package android.adservices.ondevicepersonalization;
 
-import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.isNull;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
+import static org.junit.Assert.assertTrue;
 
-import android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationConfigService;
-import android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationConfigServiceCallback;
-import android.os.OutcomeReceiver;
-import android.os.RemoteException;
+import android.content.Context;
 
-import com.android.federatedcompute.internal.util.AbstractServiceBinder;
+import androidx.test.core.app.ApplicationProvider;
 
-import org.junit.Before;
+import com.android.ondevicepersonalization.testing.utils.ResultReceiver;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.mockito.Mock;
-import org.mockito.Mockito;
+import org.junit.runners.JUnit4;
 
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.concurrent.Executor;
-
-@RunWith(Parameterized.class)
-public class OnDevicePersonalizationConfigManagerTest {
-
-    @Mock private IOnDevicePersonalizationConfigService mMockConfigService;
-
-    private OnDevicePersonalizationConfigManager mConfigManager;
-
-    @Parameterized.Parameter(0)
-    public String scenario;
-
-    @Before
-    public void setUp() {
-        mMockConfigService = Mockito.mock(IOnDevicePersonalizationConfigService.class);
-        TestServiceBinder mTestBinder = new TestServiceBinder(mMockConfigService);
-        mConfigManager = new OnDevicePersonalizationConfigManager(mTestBinder);
-    }
-
-    @Parameterized.Parameters
-    public static Collection<Object[]> data() {
-        return Arrays.asList(
-                new Object[][] {
-                    {"testSuccess"}, {"testNPE"}, {"testGenericException"},
-                });
-    }
+@RunWith(JUnit4.class)
+public final class OnDevicePersonalizationConfigManagerTest {
+    private final Context mContext = ApplicationProvider.getApplicationContext();
 
     @Test
-    public void testSetPersonalizationStatus() throws RemoteException {
-        OutcomeReceiver<Void, Exception> spyCallback = spy(new MyTestCallback());
-
-        switch (scenario) {
-            case "testSuccess":
-                doAnswer(
-                                invocation -> {
-                                    IOnDevicePersonalizationConfigServiceCallback serviceCallback =
-                                            invocation.getArgument(1);
-                                    serviceCallback.onSuccess();
-                                    return null;
-                                })
-                        .when(mMockConfigService)
-                        .setPersonalizationStatus(anyBoolean(), any());
-                mConfigManager.setPersonalizationEnabled(true, Runnable::run, spyCallback);
-                verify(spyCallback, times(1)).onResult(isNull());
-                break;
-            case "testNPE":
-                doThrow(new NullPointerException())
-                        .when(mMockConfigService)
-                        .setPersonalizationStatus(anyBoolean(), any());
-                assertThrows(
-                        NullPointerException.class,
-                        () ->
-                                mConfigManager.setPersonalizationEnabled(
-                                        true, Runnable::run, spyCallback));
-                break;
-            case "testGenericException":
-                doThrow(new RuntimeException())
-                        .when(mMockConfigService)
-                        .setPersonalizationStatus(anyBoolean(), any());
-                mConfigManager.setPersonalizationEnabled(true, Runnable::run, spyCallback);
-                verify(spyCallback, times(1)).onError(any(RuntimeException.class));
-                break;
-        }
-    }
-
-    static class TestServiceBinder
-            extends AbstractServiceBinder<IOnDevicePersonalizationConfigService> {
-        private final IOnDevicePersonalizationConfigService mService;
-
-        TestServiceBinder(IOnDevicePersonalizationConfigService service) {
-            mService = service;
-        }
-
-        @Override
-        public IOnDevicePersonalizationConfigService getService(Executor executor) {
-            return mService;
-        }
-
-        @Override
-        public void unbindFromService() {}
-    }
-
-    public static class MyTestCallback implements OutcomeReceiver<Void, Exception> {
-
-        @Override
-        public void onResult(Void result) {}
-
-        @Override
-        public void onError(Exception error) {}
+    public void testSetPersonalizationStatus() throws Exception {
+        OnDevicePersonalizationConfigManager manager =
+                new OnDevicePersonalizationConfigManager(mContext);
+        ResultReceiver<Void> receiver = new ResultReceiver<>();
+        manager.setPersonalizationEnabled(true, Runnable::run, receiver);
+        assertTrue(receiver.isCalled());
     }
 }
diff --git a/tests/frameworktests/src/android/adservices/ondevicepersonalization/OnDevicePersonalizationManagerTest.java b/tests/frameworktests/src/android/adservices/ondevicepersonalization/OnDevicePersonalizationManagerTest.java
index 7f73f71..6193c7d 100644
--- a/tests/frameworktests/src/android/adservices/ondevicepersonalization/OnDevicePersonalizationManagerTest.java
+++ b/tests/frameworktests/src/android/adservices/ondevicepersonalization/OnDevicePersonalizationManagerTest.java
@@ -13,10 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package android.adservices.ondevicepersonalization;
 
-import static org.junit.Assert.assertArrayEquals;
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -29,6 +29,7 @@
 import android.adservices.ondevicepersonalization.aidl.IRequestSurfacePackageCallback;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.PersistableBundle;
@@ -41,6 +42,8 @@
 import com.android.compatibility.common.util.ShellUtils;
 import com.android.federatedcompute.internal.util.AbstractServiceBinder;
 import com.android.ondevicepersonalization.internal.util.ByteArrayParceledSlice;
+import com.android.ondevicepersonalization.internal.util.ExceptionInfo;
+import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 import com.android.ondevicepersonalization.internal.util.PersistableBundleUtils;
 import com.android.ondevicepersonalization.testing.utils.ResultReceiver;
 
@@ -48,6 +51,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
+import org.mockito.MockitoAnnotations;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -56,32 +60,39 @@
 
 @RunWith(Parameterized.class)
 public final class OnDevicePersonalizationManagerTest {
+    private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
     private static final String TAG = "OnDevicePersonalizationManagerTest";
     private static final String KEY_OP = "op";
     private static final String KEY_STATUS_CODE = "status";
     private static final String KEY_SERVICE_ERROR_CODE = "serviceerror";
     private static final String KEY_ERROR_MESSAGE = "errormessage";
+    private static final int BEST_VALUE = 10;
+    private static final ComponentName TEST_SERVICE_COMPONENT_NAME =
+            ComponentName.createRelative("com.example.service", ".Example");
     private final Context mContext = ApplicationProvider.getApplicationContext();
-    private final TestServiceBinder mTestBinder = new TestServiceBinder(
-            IOnDevicePersonalizationManagingService.Stub.asInterface(new TestService()));
+    private final TestServiceBinder mTestBinder =
+            new TestServiceBinder(
+                    IOnDevicePersonalizationManagingService.Stub.asInterface(new TestService()));
     private final OnDevicePersonalizationManager mManager =
             new OnDevicePersonalizationManager(mContext, mTestBinder);
-    private boolean mLogApiStatsCalled = false;
+
+    private volatile boolean mLogApiStatsCalled = false;
 
     @Parameterized.Parameter(0)
     public boolean mIsSipFeatureEnabled;
 
+    @Parameterized.Parameter(1)
+    public boolean mRunExecuteInIsolatedService;
+
     @Parameterized.Parameters
     public static Collection<Object[]> data() {
         return Arrays.asList(
-                new Object[][] {
-                        {true}, {false}
-                }
-        );
+                new Object[][] {{true, true}, {true, false}, {false, true}, {false, false}});
     }
 
     @Before
-    public void setUp() {
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
         ShellUtils.runShellCommand(
                 "device_config put on_device_personalization "
                         + "shared_isolated_process_feature_enabled "
@@ -92,17 +103,47 @@
     public void testExecuteSuccess() throws Exception {
         PersistableBundle params = new PersistableBundle();
         params.putString(KEY_OP, "ok");
-        var receiver = new ResultReceiver<ExecuteResult>();
-        mManager.execute(
-                ComponentName.createRelative("com.example.service", ".Example"),
-                params,
-                Executors.newSingleThreadExecutor(),
-                receiver);
+        var receiver = new ResultReceiver();
+
+        runExecute(params, receiver);
+
         assertTrue(receiver.isSuccess());
         assertFalse(receiver.isError());
         assertNotNull(receiver.getResult());
-        assertEquals(receiver.getResult().getSurfacePackageToken().getTokenString(), "aaaa");
-        assertArrayEquals(receiver.getResult().getOutputData(), new byte[]{1, 2, 3});
+        if (mRunExecuteInIsolatedService) {
+            ExecuteInIsolatedServiceResponse response =
+                    (ExecuteInIsolatedServiceResponse) receiver.getResult();
+            assertThat(response.getSurfacePackageToken().getTokenString()).isEqualTo("aaaa");
+            assertThat(response.getBestValue()).isEqualTo(-1);
+        } else {
+            ExecuteResult response = (ExecuteResult) receiver.getResult();
+            assertThat(response.getSurfacePackageToken().getTokenString()).isEqualTo("aaaa");
+            assertThat(response.getOutputData()).isNull();
+        }
+        assertTrue(mLogApiStatsCalled);
+    }
+
+    @Test
+    public void testExecuteSuccessWithBestValueSpec() throws Exception {
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putString(KEY_OP, "best_value");
+        var receiver = new ResultReceiver<ExecuteInIsolatedServiceResponse>();
+        ExecuteInIsolatedServiceRequest request =
+                new ExecuteInIsolatedServiceRequest.Builder(TEST_SERVICE_COMPONENT_NAME)
+                        .setAppParams(bundle)
+                        .setOutputSpec(
+                                ExecuteInIsolatedServiceRequest.OutputSpec.buildBestValueSpec(100))
+                        .build();
+
+        mManager.executeInIsolatedService(request, Executors.newSingleThreadExecutor(), receiver);
+
+        assertTrue(receiver.isSuccess());
+        assertFalse(receiver.isError());
+        assertNotNull(receiver.getResult());
+
+        ExecuteInIsolatedServiceResponse response = receiver.getResult();
+        assertThat(response.getSurfacePackageToken().getTokenString()).isEqualTo("aaaa");
+        assertThat(response.getBestValue()).isEqualTo(BEST_VALUE);
         assertTrue(mLogApiStatsCalled);
     }
 
@@ -110,12 +151,10 @@
     public void testExecuteUnknownError() throws Exception {
         PersistableBundle params = new PersistableBundle();
         params.putString(KEY_OP, "error");
-        var receiver = new ResultReceiver<ExecuteResult>();
-        mManager.execute(
-                ComponentName.createRelative("com.example.service", ".Example"),
-                params,
-                Executors.newSingleThreadExecutor(),
-                receiver);
+        var receiver = new ResultReceiver();
+
+        runExecute(params, receiver);
+
         assertFalse(receiver.isSuccess());
         assertTrue(receiver.isError());
         assertTrue(receiver.getException() instanceof IllegalStateException);
@@ -127,12 +166,10 @@
         PersistableBundle params = new PersistableBundle();
         params.putString(KEY_OP, "error");
         params.putInt(KEY_STATUS_CODE, Constants.STATUS_SERVICE_FAILED);
-        var receiver = new ResultReceiver<ExecuteResult>();
-        mManager.execute(
-                ComponentName.createRelative("com.example.service", ".Example"),
-                params,
-                Executors.newSingleThreadExecutor(),
-                receiver);
+        var receiver = new ResultReceiver();
+
+        runExecute(params, receiver);
+
         assertFalse(receiver.isSuccess());
         assertTrue(receiver.isError());
         assertTrue(receiver.getException() instanceof OnDevicePersonalizationException);
@@ -141,23 +178,24 @@
 
     @Test
     public void testExecuteErrorWithCode() throws Exception {
+        int isolatedServiceErrorCode = 42;
         PersistableBundle params = new PersistableBundle();
         params.putString(KEY_OP, "error");
         params.putInt(KEY_STATUS_CODE, Constants.STATUS_SERVICE_FAILED);
-        params.putInt(KEY_SERVICE_ERROR_CODE, 42);
-        var receiver = new ResultReceiver<ExecuteResult>();
-        mManager.execute(
-                ComponentName.createRelative("com.example.service", ".Example"),
-                params,
-                Executors.newSingleThreadExecutor(),
-                receiver);
+        params.putInt(KEY_SERVICE_ERROR_CODE, isolatedServiceErrorCode);
+        var receiver = new ResultReceiver();
+
+        runExecute(params, receiver);
+
         assertFalse(receiver.isSuccess());
         assertTrue(receiver.isError());
         assertTrue(receiver.getException() instanceof OnDevicePersonalizationException);
-        assertEquals(OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_FAILED,
+        assertEquals(
+                OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_FAILED,
                 ((OnDevicePersonalizationException) receiver.getException()).getErrorCode());
         assertTrue(receiver.getException().getCause() instanceof IsolatedServiceException);
-        assertEquals(42,
+        assertEquals(
+                isolatedServiceErrorCode,
                 ((IsolatedServiceException) receiver.getException().getCause()).getErrorCode());
         assertTrue(mLogApiStatsCalled);
     }
@@ -168,28 +206,148 @@
         params.putString(KEY_OP, "error");
         params.putInt(KEY_STATUS_CODE, Constants.STATUS_SERVICE_FAILED);
         params.putString(KEY_ERROR_MESSAGE, "TestErrorMessage");
-        var receiver = new ResultReceiver<ExecuteResult>();
-        mManager.execute(
-                ComponentName.createRelative("com.example.service", ".Example"),
-                params,
-                Executors.newSingleThreadExecutor(),
-                receiver);
+        var receiver = new ResultReceiver();
+
+        runExecute(params, receiver);
+
         assertFalse(receiver.isSuccess());
         assertTrue(receiver.isError());
-        assertEquals("TestErrorMessage", receiver.getException().getMessage());
+        assertTrue(receiver.getException() instanceof OnDevicePersonalizationException);
+        assertEquals(
+                OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_FAILED,
+                ((OnDevicePersonalizationException) receiver.getException()).getErrorCode());
+        Throwable cause = receiver.getException().getCause();
+        assertNotNull(cause);
+        assertThat(cause.getMessage()).containsMatch(".*RuntimeException.*TestErrorMessage.*");
         assertTrue(mLogApiStatsCalled);
     }
 
     @Test
+    public void testExecuteManifestParsingError() throws Exception {
+        // The manifest parsing failure gets translated back to PackageManager.NameNotFound
+        // when the legacy execute API is called. The new execute API returns targeted error code.
+        PersistableBundle params = new PersistableBundle();
+        params.putString(KEY_OP, "error");
+        params.putInt(KEY_STATUS_CODE, Constants.STATUS_MANIFEST_PARSING_FAILED);
+        params.putString(KEY_ERROR_MESSAGE, "Failed parsing manifest");
+        var receiver = new ResultReceiver();
+
+        runExecute(params, receiver);
+
+        assertFalse(receiver.isSuccess());
+        assertTrue(receiver.isError());
+        Throwable cause = receiver.getException().getCause();
+        assertNotNull(cause);
+        assertThat(cause.getMessage()).containsMatch(".*RuntimeException.*parsing.*");
+        assertTrue(mLogApiStatsCalled);
+        if (mRunExecuteInIsolatedService) {
+            assertTrue(receiver.getException() instanceof OnDevicePersonalizationException);
+            assertEquals(
+                    OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_MANIFEST_PARSING_FAILED,
+                    ((OnDevicePersonalizationException) receiver.getException()).getErrorCode());
+        } else {
+            assertTrue(receiver.getException() instanceof PackageManager.NameNotFoundException);
+        }
+    }
+
+    @Test
+    public void testExecuteManifestMisconfigurationError() throws Exception {
+        // The manifest misconfigured failure gets  translated back to Class not found
+        // when the legacy execute API is used. The new execute API returns the targeted error code.
+        PersistableBundle params = new PersistableBundle();
+        params.putString(KEY_OP, "error");
+        params.putInt(KEY_STATUS_CODE, Constants.STATUS_MANIFEST_MISCONFIGURED);
+        params.putString(KEY_ERROR_MESSAGE, "Failed parsing manifest");
+        var receiver = new ResultReceiver();
+
+        runExecute(params, receiver);
+
+        assertFalse(receiver.isSuccess());
+        assertTrue(receiver.isError());
+        Throwable cause = receiver.getException().getCause();
+        assertNotNull(cause);
+        assertThat(cause.getMessage()).containsMatch(".*RuntimeException.*parsing.*");
+        assertTrue(mLogApiStatsCalled);
+        if (mRunExecuteInIsolatedService) {
+            assertTrue(receiver.getException() instanceof OnDevicePersonalizationException);
+            assertEquals(
+                    OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_MANIFEST_PARSING_FAILED,
+                    ((OnDevicePersonalizationException) receiver.getException()).getErrorCode());
+        } else {
+            assertTrue(receiver.getException() instanceof ClassNotFoundException);
+        }
+    }
+
+    @Test
+    public void testExecuteServiceTimeoutError() throws Exception {
+        // The service timeout failure gets exposed via corresponding OdpException
+        // when the new execute API is used.
+        PersistableBundle params = new PersistableBundle();
+        params.putString(KEY_OP, "error");
+        params.putInt(KEY_STATUS_CODE, Constants.STATUS_ISOLATED_SERVICE_TIMEOUT);
+        params.putString(KEY_ERROR_MESSAGE, "Service timeout");
+        var receiver = new ResultReceiver<ExecuteResult>();
+
+        runExecute(params, receiver);
+
+        assertFalse(receiver.isSuccess());
+        assertTrue(receiver.isError());
+        Throwable cause = receiver.getException().getCause();
+        assertNotNull(cause);
+        assertThat(cause.getMessage()).containsMatch(".*RuntimeException.*timeout.*");
+        assertTrue(mLogApiStatsCalled);
+        if (mRunExecuteInIsolatedService) {
+            assertTrue(receiver.getException() instanceof OnDevicePersonalizationException);
+            assertEquals(
+                    OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_TIMEOUT,
+                    ((OnDevicePersonalizationException) receiver.getException()).getErrorCode());
+        } else {
+            assertTrue(receiver.getException() instanceof OnDevicePersonalizationException);
+            assertEquals(
+                    OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_FAILED,
+                    ((OnDevicePersonalizationException) receiver.getException()).getErrorCode());
+        }
+    }
+
+    @Test
+    public void testExecuteServiceLoadingError() throws Exception {
+        // The service loading failure gets exposed via corresponding OdpException
+        // when the new execute API is used.
+        PersistableBundle params = new PersistableBundle();
+        params.putString(KEY_OP, "error");
+        params.putInt(KEY_STATUS_CODE, Constants.STATUS_ISOLATED_SERVICE_LOADING_FAILED);
+        params.putString(KEY_ERROR_MESSAGE, "Service loading failed.");
+        var receiver = new ResultReceiver<ExecuteResult>();
+
+        runExecute(params, receiver);
+
+        assertFalse(receiver.isSuccess());
+        assertTrue(receiver.isError());
+        Throwable cause = receiver.getException().getCause();
+        assertNotNull(cause);
+        assertThat(cause.getMessage()).containsMatch(".*RuntimeException.*loading.*");
+        assertTrue(mLogApiStatsCalled);
+        if (mRunExecuteInIsolatedService) {
+            assertTrue(receiver.getException() instanceof OnDevicePersonalizationException);
+            assertEquals(
+                    OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_LOADING_FAILED,
+                    ((OnDevicePersonalizationException) receiver.getException()).getErrorCode());
+        } else {
+            assertTrue(receiver.getException() instanceof OnDevicePersonalizationException);
+            assertEquals(
+                    OnDevicePersonalizationException.ERROR_ISOLATED_SERVICE_FAILED,
+                    ((OnDevicePersonalizationException) receiver.getException()).getErrorCode());
+        }
+    }
+
+    @Test
     public void testExecuteCatchesIaeFromService() throws Exception {
         PersistableBundle params = new PersistableBundle();
         params.putString(KEY_OP, "iae");
-        var receiver = new ResultReceiver<ExecuteResult>();
-        mManager.execute(
-                ComponentName.createRelative("com.example.service", ".Example"),
-                params,
-                Executors.newSingleThreadExecutor(),
-                receiver);
+        var receiver = new ResultReceiver();
+
+        runExecute(params, receiver);
+
         assertFalse(receiver.isSuccess());
         assertTrue(receiver.isError());
         assertTrue(receiver.getException() instanceof IllegalArgumentException);
@@ -200,12 +358,10 @@
     public void testExecuteCatchesNpeFromService() throws Exception {
         PersistableBundle params = new PersistableBundle();
         params.putString(KEY_OP, "npe");
-        var receiver = new ResultReceiver<ExecuteResult>();
-        mManager.execute(
-                ComponentName.createRelative("com.example.service", ".Example"),
-                params,
-                Executors.newSingleThreadExecutor(),
-                receiver);
+        var receiver = new ResultReceiver();
+
+        runExecute(params, receiver);
+
         assertFalse(receiver.isSuccess());
         assertTrue(receiver.isError());
         assertTrue(receiver.getException() instanceof NullPointerException);
@@ -216,19 +372,34 @@
     public void testExecuteCatchesOtherExceptions() throws Exception {
         PersistableBundle params = new PersistableBundle();
         params.putString(KEY_OP, "ise");
-        var receiver = new ResultReceiver<ExecuteResult>();
-        mManager.execute(
-                ComponentName.createRelative("com.example.service", ".Example"),
-                params,
-                Executors.newSingleThreadExecutor(),
-                receiver);
+        var receiver = new ResultReceiver();
+
+        runExecute(params, receiver);
+
         assertFalse(receiver.isSuccess());
         assertTrue(receiver.isError());
         assertTrue(receiver.getException() instanceof IllegalStateException);
         assertTrue(mLogApiStatsCalled);
     }
 
-    class TestService extends IOnDevicePersonalizationManagingService.Stub {
+    private void runExecute(PersistableBundle params, ResultReceiver receiver) {
+        if (mRunExecuteInIsolatedService) {
+            ExecuteInIsolatedServiceRequest request =
+                    new ExecuteInIsolatedServiceRequest.Builder(TEST_SERVICE_COMPONENT_NAME)
+                            .setAppParams(params)
+                            .build();
+            mManager.executeInIsolatedService(
+                    request, Executors.newSingleThreadExecutor(), receiver);
+        } else {
+            mManager.execute(
+                    TEST_SERVICE_COMPONENT_NAME,
+                    params,
+                    Executors.newSingleThreadExecutor(),
+                    receiver);
+        }
+    }
+
+    private class TestService extends IOnDevicePersonalizationManagingService.Stub {
         @Override
         public String getVersion() {
             return "1.0";
@@ -240,13 +411,16 @@
                 ComponentName handler,
                 Bundle wrappedParams,
                 CallerMetadata metadata,
+                ExecuteOptionsParcel options,
                 IExecuteCallback callback) {
             try {
                 PersistableBundle params;
                 String op;
                 try {
-                    ByteArrayParceledSlice paramsBuffer = wrappedParams.getParcelable(
-                            Constants.EXTRA_APP_PARAMS_SERIALIZED, ByteArrayParceledSlice.class);
+                    ByteArrayParceledSlice paramsBuffer =
+                            wrappedParams.getParcelable(
+                                    Constants.EXTRA_APP_PARAMS_SERIALIZED,
+                                    ByteArrayParceledSlice.class);
                     params = PersistableBundleUtils.fromByteArray(paramsBuffer.getByteArray());
                     op = params.getString(KEY_OP);
                 } catch (Exception e) {
@@ -256,18 +430,33 @@
                 if (op.equals("ok")) {
                     Bundle bundle = new Bundle();
                     bundle.putString(Constants.EXTRA_SURFACE_PACKAGE_TOKEN_STRING, "aaaa");
-                    bundle.putByteArray(Constants.EXTRA_OUTPUT_DATA, new byte[]{1, 2, 3});
-                    callback.onSuccess(bundle,
-                            new CalleeMetadata.Builder().setCallbackInvokeTimeMillis(
-                                    SystemClock.elapsedRealtime()).build());
+                    callback.onSuccess(
+                            bundle,
+                            new CalleeMetadata.Builder()
+                                    .setCallbackInvokeTimeMillis(SystemClock.elapsedRealtime())
+                                    .build());
+                } else if (options.getOutputType()
+                        == ExecuteInIsolatedServiceRequest.OutputSpec.OUTPUT_TYPE_BEST_VALUE) {
+                    Bundle bundle = new Bundle();
+                    bundle.putString(Constants.EXTRA_SURFACE_PACKAGE_TOKEN_STRING, "aaaa");
+                    bundle.putInt(Constants.EXTRA_OUTPUT_BEST_VALUE, BEST_VALUE);
+                    callback.onSuccess(
+                            bundle,
+                            new CalleeMetadata.Builder()
+                                    .setCallbackInvokeTimeMillis(SystemClock.elapsedRealtime())
+                                    .build());
                 } else if (op.equals("error")) {
-                    int statusCode = params.getInt(KEY_STATUS_CODE,
-                            Constants.STATUS_INTERNAL_ERROR);
+                    int statusCode =
+                            params.getInt(KEY_STATUS_CODE, Constants.STATUS_INTERNAL_ERROR);
                     int serviceErrorCode = params.getInt(KEY_SERVICE_ERROR_CODE, 0);
                     String errorMessage = params.getString(KEY_ERROR_MESSAGE);
-                    callback.onError(statusCode, serviceErrorCode, errorMessage,
-                            new CalleeMetadata.Builder().setCallbackInvokeTimeMillis(
-                                    SystemClock.elapsedRealtime()).build());
+                    callback.onError(
+                            statusCode,
+                            serviceErrorCode,
+                            ExceptionInfo.toByteArray(new RuntimeException(errorMessage), 3),
+                            new CalleeMetadata.Builder()
+                                    .setCallbackInvokeTimeMillis(SystemClock.elapsedRealtime())
+                                    .build());
                 } else if (op.equals("iae")) {
                     throw new IllegalArgumentException();
                 } else if (op.equals("npe")) {
@@ -318,9 +507,10 @@
         }
     }
 
-    class TestServiceBinder
+    private static class TestServiceBinder
             extends AbstractServiceBinder<IOnDevicePersonalizationManagingService> {
         private final IOnDevicePersonalizationManagingService mService;
+
         TestServiceBinder(IOnDevicePersonalizationManagingService service) {
             mService = service;
         }
diff --git a/tests/frameworktests/src/android/adservices/ondevicepersonalization/OnDevicePersonalizationSystemEventManagerTest.java b/tests/frameworktests/src/android/adservices/ondevicepersonalization/OnDevicePersonalizationSystemEventManagerTest.java
index 4255cf9..3698076 100644
--- a/tests/frameworktests/src/android/adservices/ondevicepersonalization/OnDevicePersonalizationSystemEventManagerTest.java
+++ b/tests/frameworktests/src/android/adservices/ondevicepersonalization/OnDevicePersonalizationSystemEventManagerTest.java
@@ -141,6 +141,7 @@
                 ComponentName handler,
                 Bundle params,
                 CallerMetadata metadata,
+                ExecuteOptionsParcel options,
                 IExecuteCallback callback) {
             throw new UnsupportedOperationException();
         }
diff --git a/tests/frameworktests/src/android/adservices/ondevicepersonalization/RemoteDataTest.java b/tests/frameworktests/src/android/adservices/ondevicepersonalization/RemoteDataTest.java
index 1a3b00d..2334f3f 100644
--- a/tests/frameworktests/src/android/adservices/ondevicepersonalization/RemoteDataTest.java
+++ b/tests/frameworktests/src/android/adservices/ondevicepersonalization/RemoteDataTest.java
@@ -16,8 +16,9 @@
 
 package android.adservices.ondevicepersonalization;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
 import android.adservices.ondevicepersonalization.aidl.IDataAccessService;
@@ -30,12 +31,14 @@
 
 import com.android.ondevicepersonalization.internal.util.ByteArrayParceledSlice;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.concurrent.CountDownLatch;
 
 /**
  * Unit Tests of RemoteData API.
@@ -43,35 +46,62 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class RemoteDataTest {
-    KeyValueStore mRemoteData = new RemoteDataImpl(
-            IDataAccessService.Stub.asInterface(
-                    new RemoteDataService()));
+
+    private KeyValueStore mRemoteData;
+    private RemoteDataService mRemoteDataService;
+
+    @Before
+    public void setup() {
+        mRemoteDataService = new RemoteDataService();
+        mRemoteData = new RemoteDataImpl(IDataAccessService.Stub.asInterface(mRemoteDataService));
+    }
 
     @Test
     public void testLookupSuccess() throws Exception {
         assertArrayEquals(new byte[] {1, 2, 3}, mRemoteData.get("a"));
         assertArrayEquals(new byte[] {7, 8, 9}, mRemoteData.get("c"));
         assertNull(mRemoteData.get("e"));
+
+        mRemoteDataService.mLatch.await();
+        assertThat(mRemoteDataService.mResponseCode).isEqualTo(Constants.STATUS_SUCCESS);
     }
 
     @Test
-    public void testLookupError() {
+    public void testLookupError() throws Exception {
         // Triggers an expected error in the mock service.
         assertNull(mRemoteData.get("z"));
+
+        mRemoteDataService.mLatch.await();
+        assertThat(mRemoteDataService.mResponseCode).isEqualTo(Constants.STATUS_INTERNAL_ERROR);
     }
 
     @Test
-    public void testKeysetSuccess() {
+    public void testLookupEmptyResult() throws Exception {
+        // Triggers an expected error in the mock service.
+        assertNull(mRemoteData.get("empty"));
+
+        mRemoteDataService.mLatch.await();
+        assertThat(mRemoteDataService.mResponseCode)
+                .isEqualTo(Constants.STATUS_SUCCESS_EMPTY_RESULT);
+    }
+
+    @Test
+    public void testKeysetSuccess() throws Exception {
         Set<String> expectedResult = new HashSet<>();
         expectedResult.add("a");
         expectedResult.add("b");
         expectedResult.add("c");
 
-        assertEquals(expectedResult, mRemoteData.keySet());
+        assertThat(expectedResult).isEqualTo(mRemoteData.keySet());
+
+        mRemoteDataService.mLatch.await();
+        assertThat(mRemoteDataService.mResponseCode).isEqualTo(Constants.STATUS_SUCCESS);
     }
 
     public static class RemoteDataService extends IDataAccessService.Stub {
         HashMap<String, byte[]> mContents = new HashMap<String, byte[]>();
+        int mResponseCode;
+        CountDownLatch mLatch = new CountDownLatch(1);
 
         public RemoteDataService() {
             mContents.put("a", new byte[] {1, 2, 3});
@@ -105,6 +135,15 @@
             if (key == null) {
                 throw new NullPointerException("key");
             }
+            if (key.equals("empty")) {
+                // Raise expected error.
+                try {
+                    callback.onSuccess(null);
+                } catch (RemoteException e) {
+                    // Ignored.
+                }
+                return;
+            }
 
             if (key.equals("z")) {
                 // Raise expected error.
@@ -129,6 +168,9 @@
         }
 
         @Override
-        public void logApiCallStats(int apiName, long latencyMillis, int responseCode) {}
+        public void logApiCallStats(int apiName, long latencyMillis, int responseCode) {
+            mLatch.countDown();
+            mResponseCode = responseCode;
+        }
     }
 }
diff --git a/tests/frameworktests/src/com/android/ondevicepersonalization/internal/util/ExceptionInfoTest.java b/tests/frameworktests/src/com/android/ondevicepersonalization/internal/util/ExceptionInfoTest.java
new file mode 100644
index 0000000..0971b49
--- /dev/null
+++ b/tests/frameworktests/src/com/android/ondevicepersonalization/internal/util/ExceptionInfoTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2024 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.ondevicepersonalization.internal.util;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.matchesPattern;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * Unit Tests of ExceptionInfo.
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ExceptionInfoTest {
+    @Test
+    public void testExceptionRoundTrip() {
+        Exception e = createException();
+        assertNotNull(e);
+        byte[] serialized = ExceptionInfo.toByteArray(e, 5);
+        Exception e2 = ExceptionInfo.fromByteArray(serialized);
+        assertNotNull(e2);
+        assertThat(e2.getMessage(), matchesPattern(".*IllegalStateException.*Exception2.*"));
+        assertNotNull(e2.getCause());
+        assertThat(e2.getCause().getMessage(),
+                matchesPattern(".*NullPointerException.*Exception1.*"));
+        String stackTrace = getStackTrace(e2);
+        assertThat(stackTrace, containsString("function2"));
+        assertThat(stackTrace, containsString("function1"));
+    }
+
+    private Exception createException() {
+        try {
+            function2();
+        } catch (Exception e) {
+            return e;
+        }
+        return null;
+    }
+
+    private static String getStackTrace(Throwable t) {
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new PrintWriter(sw);
+        t.printStackTrace(pw);
+        pw.flush();
+        return sw.toString();
+    }
+
+    private static void function1() {
+        throw new NullPointerException("Exception1");
+    }
+
+    private static void function2() {
+        try {
+            function1();
+        } catch (Exception e) {
+            throw new IllegalStateException("Exception2", e);
+        }
+    }
+}
diff --git a/tests/manualtests/Android.bp b/tests/manualtests/Android.bp
index d3ce42a..0cea200 100644
--- a/tests/manualtests/Android.bp
+++ b/tests/manualtests/Android.bp
@@ -43,6 +43,7 @@
         "androidx.test.ext.junit",
         "androidx.test.ext.truth",
         "androidx.test.rules",
+        "federated-compute-java-proto-lite",
         "kotlin-stdlib",
         "kotlin-test",
         "kotlinx-coroutines-android",
diff --git a/tests/manualtests/AndroidManifest.xml b/tests/manualtests/AndroidManifest.xml
index 43c4d6d..c05685a 100644
--- a/tests/manualtests/AndroidManifest.xml
+++ b/tests/manualtests/AndroidManifest.xml
@@ -21,7 +21,8 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
 
-    <application android:name="com.android.ondevicepersonalization.services.OnDevicePersonalizationApplication"
+    <application android:debuggable="true"
+                 android:name="com.android.ondevicepersonalization.services.OnDevicePersonalizationApplication"
                  android:label="OnDevicePersonalizationManualTests">
         <uses-library android:name="android.test.runner"/>
         <property android:name="android.ondevicepersonalization.ON_DEVICE_PERSONALIZATION_CONFIG"
diff --git a/tests/perftests/scenarios/tests/AndroidManifest.xml b/tests/perftests/scenarios/tests/AndroidManifest.xml
index 350196d..2139198 100644
--- a/tests/perftests/scenarios/tests/AndroidManifest.xml
+++ b/tests/perftests/scenarios/tests/AndroidManifest.xml
@@ -45,7 +45,7 @@
   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
   <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
 
-  <application android:label="OnDevicePersonalizationPerfScenariosTests">
+  <application android:debuggable="true" android:label="OnDevicePersonalizationPerfScenariosTests">
     <uses-library android:name="android.test.runner"/>
   </application>
 
diff --git a/tests/plugintests/AndroidManifest.xml b/tests/plugintests/AndroidManifest.xml
index d298988..e2bd0cd 100644
--- a/tests/plugintests/AndroidManifest.xml
+++ b/tests/plugintests/AndroidManifest.xml
@@ -17,7 +17,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.ondevicepersonalization.plugintests">
 
-    <application android:label="OnDevicePersonalizationPluginTests">
+    <application android:debuggable="true" android:label="OnDevicePersonalizationPluginTests">
         <uses-library android:name="android.test.runner" />
     </application>
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/servicetests/Android.bp b/tests/servicetests/Android.bp
index 07f0193..da85380 100644
--- a/tests/servicetests/Android.bp
+++ b/tests/servicetests/Android.bp
@@ -47,6 +47,7 @@
         "androidx.test.ext.junit",
         "androidx.test.ext.truth",
         "androidx.test.rules",
+        "federated-compute-java-proto-lite",
         "mockito-target-extended-minus-junit4",
         "kotlin-stdlib",
         "kotlin-test",
@@ -61,6 +62,7 @@
         "compatibility-device-util-axt",
         "tensorflowlite_java",
         "adservices-shared-spe",
+        "ondevicepersonalization-testing-utils",
     ],
     sdk_version: "module_current",
     target_sdk_version: "current",
diff --git a/tests/servicetests/AndroidManifest.xml b/tests/servicetests/AndroidManifest.xml
index 6fae2d3..014f9d9 100644
--- a/tests/servicetests/AndroidManifest.xml
+++ b/tests/servicetests/AndroidManifest.xml
@@ -25,6 +25,8 @@
     <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/>
     <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
     <uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW"/>
+    <!-- Permissions required for reading device configs -->
+    <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
 
     <!-- Required for reading and writing device configs -->
 
@@ -45,11 +47,6 @@
                 <action android:name="android.OnDevicePersonalizationService" />
             </intent-filter>
         </service>
-        <service android:name="com.android.ondevicepersonalization.services.OnDevicePersonalizationConfigServiceImpl" android:exported="true" >
-            <intent-filter>
-                <action android:name="android.OnDevicePersonalizationConfigService" />
-            </intent-filter>
-        </service>
         <service android:name="com.android.ondevicepersonalization.services.OnDevicePersonalizationDebugServiceImpl" android:exported="true" >
             <intent-filter>
                 <action android:name="android.OnDevicePersonalizationService" />
@@ -80,6 +77,10 @@
                  android:exported="false"
                  android:permission="android.permission.BIND_JOB_SERVICE">
         </service>
+        <service android:name="com.android.ondevicepersonalization.services.data.errors.AggregateErrorDataReportingService"
+                 android:exported="false"
+                 android:permission="android.permission.BIND_JOB_SERVICE">
+        </service>
         <service
             android:name="com.android.ondevicepersonalization.services.federatedcompute.OdpExampleStoreService"
             android:enabled="true"
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfigServiceTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfigServiceTest.java
deleted file mode 100644
index fa54921..0000000
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfigServiceTest.java
+++ /dev/null
@@ -1,251 +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.ondevicepersonalization.services;
-
-import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ON_DEVICE_PERSONALIZATION_ERROR;
-import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__ODP;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isA;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.adservices.ondevicepersonalization.Constants;
-import android.adservices.ondevicepersonalization.aidl.IOnDevicePersonalizationConfigServiceCallback;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.os.Build;
-import android.os.IBinder;
-
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.SdkSuppress;
-import androidx.test.rule.ServiceTestRule;
-
-import com.android.modules.utils.testing.ExtendedMockitoRule;
-import com.android.modules.utils.testing.ExtendedMockitoRule.MockStatic;
-import com.android.ondevicepersonalization.services.data.user.RawUserData;
-import com.android.ondevicepersonalization.services.data.user.UserDataCollector;
-import com.android.ondevicepersonalization.services.data.user.UserPrivacyStatus;
-import com.android.ondevicepersonalization.services.statsd.errorlogging.ClientErrorLogger;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.Mock;
-import org.mockito.quality.Strictness;
-
-import java.util.TimeZone;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-@RunWith(JUnit4.class)
-@MockStatic(ClientErrorLogger.class)
-public class OnDevicePersonalizationConfigServiceTest {
-    @Rule
-    public final ExtendedMockitoRule extendedMockitoRule =
-            new ExtendedMockitoRule.Builder(this).setStrictness(Strictness.LENIENT).build();
-
-    @Rule
-    public final ServiceTestRule serviceRule = new ServiceTestRule();
-    private Context mContext = spy(ApplicationProvider.getApplicationContext());
-    private OnDevicePersonalizationConfigServiceDelegate mBinder;
-    private UserPrivacyStatus mUserPrivacyStatus;
-    private RawUserData mUserData;
-    private UserDataCollector mUserDataCollector;
-    @Mock
-    private ClientErrorLogger mMockClientErrorLogger;
-
-    @Before
-    public void setup() throws Exception {
-
-        PhFlagsTestUtil.setUpDeviceConfigPermissions();
-        PhFlagsTestUtil.disableGlobalKillSwitch();
-        PhFlagsTestUtil.disablePersonalizationStatusOverride();
-        when(mContext.checkCallingPermission(anyString()))
-                        .thenReturn(PackageManager.PERMISSION_GRANTED);
-        mBinder = new OnDevicePersonalizationConfigServiceDelegate(mContext);
-        mUserPrivacyStatus = UserPrivacyStatus.getInstanceForTest();
-        mUserPrivacyStatus.setPersonalizationStatusEnabled(false);
-        mUserData = RawUserData.getInstance();
-        TimeZone pstTime = TimeZone.getTimeZone("GMT-08:00");
-        TimeZone.setDefault(pstTime);
-        mUserDataCollector = UserDataCollector.getInstanceForTest(mContext);
-        when(ClientErrorLogger.getInstance()).thenReturn(mMockClientErrorLogger);
-    }
-
-    @Test
-    public void testThrowIfGlobalKillSwitchEnabled() throws Exception {
-        PhFlagsTestUtil.enableGlobalKillSwitch();
-        try {
-            assertThrows(
-                    IllegalStateException.class,
-                    () ->
-                            mBinder.setPersonalizationStatus(true, null)
-            );
-        } finally {
-            PhFlagsTestUtil.disableGlobalKillSwitch();
-        }
-    }
-
-    @Test
-    public void testSetPersonalizationStatusNoCallingPermission() throws Exception {
-        when(mContext.checkCallingPermission(anyString()))
-                        .thenReturn(PackageManager.PERMISSION_DENIED);
-        assertThrows(SecurityException.class, () -> {
-            mBinder.setPersonalizationStatus(true, null);
-        });
-    }
-
-    @Test
-    public void testSetPersonalizationStatusChanged() throws Exception {
-        assertFalse(mUserPrivacyStatus.isPersonalizationStatusEnabled());
-        populateUserData();
-        assertNotEquals(0, mUserData.utcOffset);
-        assertTrue(mUserDataCollector.isInitialized());
-
-        CountDownLatch latch = new CountDownLatch(1);
-        mBinder.setPersonalizationStatus(true,
-                new IOnDevicePersonalizationConfigServiceCallback.Stub() {
-                    @Override
-                    public void onSuccess() {
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onFailure(int errorCode) {
-                        latch.countDown();
-                    }
-                });
-
-        latch.await();
-        assertTrue(mUserPrivacyStatus.isPersonalizationStatusEnabled());
-
-        assertEquals(0, mUserData.utcOffset);
-        assertFalse(mUserDataCollector.isInitialized());
-    }
-
-    @Test
-    public void testSetPersonalizationStatusIfCallbackMissing() throws Exception {
-        assertThrows(NullPointerException.class, () -> {
-            mBinder.setPersonalizationStatus(true, null);
-        });
-    }
-
-    @Test
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    public void testSetPersonalizationStatusThrowsRuntimeException() throws Exception {
-        when(mContext.getSystemService(any(Class.class))).thenThrow(RuntimeException.class);
-        CountDownLatch latch = new CountDownLatch(1);
-        TestCallback callback = new TestCallback(latch);
-
-        mBinder.setPersonalizationStatus(true, callback);
-
-        assertTrue(latch.await(10000, TimeUnit.MILLISECONDS));
-        assertEquals(Constants.STATUS_INTERNAL_ERROR, callback.getErrCode());
-        verify(mMockClientErrorLogger)
-                .logErrorWithExceptionInfo(
-                        isA(RuntimeException.class),
-                        eq(AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ON_DEVICE_PERSONALIZATION_ERROR),
-                        eq(AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__ODP));
-    }
-
-    @Test
-    public void testSetPersonalizationStatusNoOps() throws Exception {
-        mUserPrivacyStatus.setPersonalizationStatusEnabled(true);
-
-        populateUserData();
-        assertNotEquals(0, mUserData.utcOffset);
-        int utcOffset = mUserData.utcOffset;
-        assertTrue(mUserDataCollector.isInitialized());
-
-        CountDownLatch latch = new CountDownLatch(1);
-        mBinder.setPersonalizationStatus(true,
-                new IOnDevicePersonalizationConfigServiceCallback.Stub() {
-                    @Override
-                    public void onSuccess() {
-                        latch.countDown();
-                    }
-
-                    @Override
-                    public void onFailure(int errorCode) {
-                        latch.countDown();
-                    }
-                });
-
-        latch.await();
-
-        assertTrue(mUserPrivacyStatus.isPersonalizationStatusEnabled());
-        // Adult data should not be roll-back'ed
-        assertEquals(utcOffset, mUserData.utcOffset);
-        assertTrue(mUserDataCollector.isInitialized());
-    }
-
-    @Test
-    public void testWithBoundService() throws TimeoutException {
-        Intent serviceIntent = new Intent(mContext,
-                OnDevicePersonalizationConfigServiceImpl.class);
-        IBinder binder = serviceRule.bindService(serviceIntent);
-        assertTrue(binder instanceof OnDevicePersonalizationConfigServiceDelegate);
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        mUserDataCollector.clearUserData(mUserData);
-        mUserDataCollector.clearMetadata();
-    }
-
-    private void populateUserData() {
-        mUserDataCollector.updateUserData(mUserData);
-    }
-
-    class TestCallback extends IOnDevicePersonalizationConfigServiceCallback.Stub {
-
-        int mErrCode;
-        CountDownLatch mLatch;
-
-        TestCallback(CountDownLatch latch) {
-            this.mLatch = latch;
-        }
-
-        @Override
-        public void onSuccess() {
-        }
-
-        @Override
-        public void onFailure(int errorCode) {
-            mErrCode = errorCode;
-            mLatch.countDown();
-        }
-
-        public int getErrCode() {
-            return mErrCode;
-        }
-    }
-}
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationManagingServiceTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationManagingServiceTest.java
index c9d84aa..f6deec1 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationManagingServiceTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationManagingServiceTest.java
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package com.android.ondevicepersonalization.services;
 
 import static android.adservices.ondevicepersonalization.OnDevicePersonalizationPermissions.NOTIFY_MEASUREMENT_EVENT;
@@ -36,6 +35,8 @@
 import android.adservices.ondevicepersonalization.CalleeMetadata;
 import android.adservices.ondevicepersonalization.CallerMetadata;
 import android.adservices.ondevicepersonalization.Constants;
+import android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceRequest;
+import android.adservices.ondevicepersonalization.ExecuteOptionsParcel;
 import android.adservices.ondevicepersonalization.aidl.IExecuteCallback;
 import android.adservices.ondevicepersonalization.aidl.IRegisterMeasurementEventCallback;
 import android.adservices.ondevicepersonalization.aidl.IRequestSurfacePackageCallback;
@@ -60,6 +61,7 @@
 import com.android.ondevicepersonalization.services.data.user.UserDataCollectionJobService;
 import com.android.ondevicepersonalization.services.data.user.UserPrivacyStatus;
 import com.android.ondevicepersonalization.services.download.mdd.MobileDataDownloadFactory;
+import com.android.ondevicepersonalization.services.enrollment.PartnerEnrollmentChecker;
 import com.android.ondevicepersonalization.services.maintenance.OnDevicePersonalizationMaintenanceJobService;
 
 import com.google.android.libraries.mobiledatadownload.MobileDataDownload;
@@ -70,7 +72,6 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 import org.mockito.Mock;
-import org.mockito.Spy;
 import org.mockito.quality.Strictness;
 
 import java.util.concurrent.CountDownLatch;
@@ -78,40 +79,38 @@
 
 @RunWith(JUnit4.class)
 public class OnDevicePersonalizationManagingServiceTest {
-    @Rule
-    public final ServiceTestRule serviceRule = new ServiceTestRule();
+    @Rule public final ServiceTestRule serviceRule = new ServiceTestRule();
     private final Context mContext = spy(ApplicationProvider.getApplicationContext());
-    private OnDevicePersonalizationManagingServiceDelegate mService =
-            new OnDevicePersonalizationManagingServiceDelegate(mContext);
-
-    @Mock
-    private UserPrivacyStatus mUserPrivacyStatus;
+    private OnDevicePersonalizationManagingServiceDelegate mService;
+    @Mock private UserPrivacyStatus mUserPrivacyStatus;
     @Mock private MobileDataDownload mMockMdd;
-    @Spy
-    private Flags mSpyFlags = spy(FlagsFactory.getFlags());
+    @Mock private Flags mMockFlags;
+
     @Rule
-    public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
-            .mockStatic(FlagsFactory.class)
-            .spyStatic(UserPrivacyStatus.class)
-            .spyStatic(DeviceUtils.class)
-            .spyStatic(OnDevicePersonalizationMaintenanceJobService.class)
-            .spyStatic(UserDataCollectionJobService.class)
-            .spyStatic(MobileDataDownloadFactory.class)
-            .setStrictness(Strictness.LENIENT)
-            .build();
+    public final ExtendedMockitoRule mExtendedMockitoRule =
+            new ExtendedMockitoRule.Builder(this)
+                    .spyStatic(FlagsFactory.class)
+                    .spyStatic(UserPrivacyStatus.class)
+                    .spyStatic(DeviceUtils.class)
+                    .spyStatic(OnDevicePersonalizationMaintenanceJobService.class)
+                    .spyStatic(UserDataCollectionJobService.class)
+                    .spyStatic(MobileDataDownloadFactory.class)
+                    .spyStatic(PartnerEnrollmentChecker.class)
+                    .setStrictness(Strictness.LENIENT)
+                    .build();
 
     @Before
     public void setup() throws Exception {
-        ExtendedMockito.doReturn(mSpyFlags).when(FlagsFactory::getFlags);
-        when(mSpyFlags.getGlobalKillSwitch()).thenReturn(false);
-        PhFlagsTestUtil.setUpDeviceConfigPermissions();
+        ExtendedMockito.doReturn(mMockFlags).when(FlagsFactory::getFlags);
+        mService = new OnDevicePersonalizationManagingServiceDelegate(mContext);
+        when(mMockFlags.getGlobalKillSwitch()).thenReturn(false);
+        when(mMockFlags.getMaxIntValuesLimit()).thenReturn(100);
         ExtendedMockito.doReturn(true).when(() -> DeviceUtils.isOdpSupported(any()));
         ExtendedMockito.doReturn(mUserPrivacyStatus).when(UserPrivacyStatus::getInstance);
         doReturn(true).when(mUserPrivacyStatus).isMeasurementEnabled();
         doReturn(true).when(mUserPrivacyStatus).isProtectedAudienceEnabled();
         when(mContext.checkCallingPermission(NOTIFY_MEASUREMENT_EVENT))
                 .thenReturn(PackageManager.PERMISSION_GRANTED);
-
         ExtendedMockito.doReturn(SCHEDULING_RESULT_CODE_SUCCESSFUL)
                 .when(
                         () ->
@@ -120,6 +119,10 @@
         ExtendedMockito.doReturn(1).when(() -> UserDataCollectionJobService.schedule(any()));
         ExtendedMockito.doReturn(mMockMdd).when(() -> MobileDataDownloadFactory.getMdd(any()));
         doReturn(immediateVoidFuture()).when(mMockMdd).schedulePeriodicBackgroundTasks();
+        ExtendedMockito.doReturn(true)
+                .when(() -> PartnerEnrollmentChecker.isCallerAppEnrolled(any()));
+        ExtendedMockito.doReturn(true)
+                .when(() -> PartnerEnrollmentChecker.isIsolatedServiceEnrolled(any()));
     }
 
     @Test
@@ -129,7 +132,7 @@
 
     @Test
     public void testEnabledGlobalKillSwitchOnExecute() throws Exception {
-        when(mSpyFlags.getGlobalKillSwitch()).thenReturn(true);
+        when(mMockFlags.getGlobalKillSwitch()).thenReturn(true);
         var callback = new ExecuteCallback();
         assertThrows(
                 IllegalStateException.class,
@@ -141,8 +144,8 @@
                                         "com.test.TestPersonalizationHandler"),
                                 createWrappedAppParams(),
                                 new CallerMetadata.Builder().build(),
-                                callback
-                        ));
+                                ExecuteOptionsParcel.DEFAULT,
+                                callback));
     }
 
     @Test
@@ -158,8 +161,8 @@
                                         "com.test.TestPersonalizationHandler"),
                                 createWrappedAppParams(),
                                 new CallerMetadata.Builder().build(),
-                                new ExecuteCallback()
-                        ));
+                                ExecuteOptionsParcel.DEFAULT,
+                                new ExecuteCallback()));
     }
 
     @Test
@@ -167,16 +170,53 @@
         var callback = new ExecuteCallback();
         mService.execute(
                 mContext.getPackageName(),
-                new ComponentName(
-                        mContext.getPackageName(), "com.test.TestPersonalizationHandler"),
+                new ComponentName(mContext.getPackageName(), "com.test.TestPersonalizationHandler"),
                 createWrappedAppParams(),
                 new CallerMetadata.Builder().build(),
+                ExecuteOptionsParcel.DEFAULT,
                 callback);
         callback.await();
         assertTrue(callback.mWasInvoked);
     }
 
     @Test
+    public void testExecuteInvokesAppRequestFlowWithBestValue() throws Exception {
+        var callback = new ExecuteCallback();
+        ExecuteOptionsParcel options =
+                new ExecuteOptionsParcel(
+                        ExecuteInIsolatedServiceRequest.OutputSpec.OUTPUT_TYPE_BEST_VALUE, 50);
+        mService.execute(
+                mContext.getPackageName(),
+                new ComponentName(mContext.getPackageName(), "com.test.TestPersonalizationHandler"),
+                createWrappedAppParams(),
+                new CallerMetadata.Builder().build(),
+                options,
+                callback);
+        callback.await();
+        assertTrue(callback.mWasInvoked);
+    }
+
+    @Test
+    public void testExecuteInvokesAppRequestFlowWithBestValue_exceedLimit() throws Exception {
+        var callback = new ExecuteCallback();
+        ExecuteOptionsParcel options =
+                new ExecuteOptionsParcel(
+                        ExecuteInIsolatedServiceRequest.OutputSpec.OUTPUT_TYPE_BEST_VALUE, 150);
+        assertThrows(
+                IllegalArgumentException.class,
+                () ->
+                        mService.execute(
+                                mContext.getPackageName(),
+                                new ComponentName(
+                                        mContext.getPackageName(),
+                                        "com.test.TestPersonalizationHandler"),
+                                createWrappedAppParams(),
+                                new CallerMetadata.Builder().build(),
+                                options,
+                                callback));
+    }
+
+    @Test
     public void testExecuteThrowsIfAppPackageNameIncorrect() throws Exception {
         var callback = new ExecuteCallback();
         assertThrows(
@@ -189,6 +229,7 @@
                                         "com.test.TestPersonalizationHandler"),
                                 createWrappedAppParams(),
                                 new CallerMetadata.Builder().build(),
+                                ExecuteOptionsParcel.DEFAULT,
                                 callback));
     }
 
@@ -205,6 +246,7 @@
                                         "com.test.TestPersonalizationHandler"),
                                 createWrappedAppParams(),
                                 new CallerMetadata.Builder().build(),
+                                ExecuteOptionsParcel.DEFAULT,
                                 callback));
     }
 
@@ -221,6 +263,7 @@
                                         "com.test.TestPersonalizationHandler"),
                                 createWrappedAppParams(),
                                 new CallerMetadata.Builder().build(),
+                                ExecuteOptionsParcel.DEFAULT,
                                 callback));
     }
 
@@ -235,6 +278,7 @@
                                 null,
                                 createWrappedAppParams(),
                                 new CallerMetadata.Builder().build(),
+                                ExecuteOptionsParcel.DEFAULT,
                                 callback));
     }
 
@@ -249,6 +293,7 @@
                                 new ComponentName("", "ServiceClass"),
                                 createWrappedAppParams(),
                                 new CallerMetadata.Builder().build(),
+                                ExecuteOptionsParcel.DEFAULT,
                                 callback));
     }
 
@@ -263,6 +308,7 @@
                                 new ComponentName("com.test.TestPackage", ""),
                                 createWrappedAppParams(),
                                 new CallerMetadata.Builder().build(),
+                                ExecuteOptionsParcel.DEFAULT,
                                 callback));
     }
 
@@ -279,6 +325,7 @@
                                         "com.test.TestPersonalizationHandler"),
                                 createWrappedAppParams(),
                                 null,
+                                ExecuteOptionsParcel.DEFAULT,
                                 callback));
     }
 
@@ -294,57 +341,53 @@
                                         "com.test.TestPersonalizationHandler"),
                                 createWrappedAppParams(),
                                 new CallerMetadata.Builder().build(),
+                                ExecuteOptionsParcel.DEFAULT,
                                 null));
     }
 
     @Test
-    public void testExecuteThrowsIfCallerNotEnrolled() throws Exception {
+    public void testExecuteThrowsIfCallerNotEnrolled() {
         var callback = new ExecuteCallback();
-        var originalCallerAppAllowList = mSpyFlags.getCallerAppAllowList();
-        PhFlagsTestUtil.setCallerAppAllowList("");
-        try {
-            assertThrows(
-                    IllegalStateException.class,
-                    () ->
-                            mService.execute(
-                                    mContext.getPackageName(),
-                                    new ComponentName(
-                                            mContext.getPackageName(),
-                                            "com.test.TestPersonalizationHandler"),
-                                    createWrappedAppParams(),
-                                    new CallerMetadata.Builder().build(),
-                                    callback));
-        } finally {
-            PhFlagsTestUtil.setCallerAppAllowList(originalCallerAppAllowList);
-        }
+        ExtendedMockito.doReturn(false)
+                .when(() -> PartnerEnrollmentChecker.isCallerAppEnrolled(any()));
+
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        mService.execute(
+                                mContext.getPackageName(),
+                                new ComponentName(
+                                        mContext.getPackageName(),
+                                        "com.test.TestPersonalizationHandler"),
+                                createWrappedAppParams(),
+                                new CallerMetadata.Builder().build(),
+                                ExecuteOptionsParcel.DEFAULT,
+                                callback));
     }
 
     @Test
-    public void testExecuteThrowsIfIsolatedServiceNotEnrolled() throws Exception {
+    public void testExecuteThrowsIfIsolatedServiceNotEnrolled() {
         var callback = new ExecuteCallback();
-        var originalIsolatedServiceAllowList =
-                FlagsFactory.getFlags().getIsolatedServiceAllowList();
-        PhFlagsTestUtil.setIsolatedServiceAllowList("");
-        try {
-            assertThrows(
-                    IllegalStateException.class,
-                    () ->
-                            mService.execute(
-                                    mContext.getPackageName(),
-                                    new ComponentName(
-                                            mContext.getPackageName(),
-                                            "com.test.TestPersonalizationHandler"),
-                                    createWrappedAppParams(),
-                                    new CallerMetadata.Builder().build(),
-                                    callback));
-        } finally {
-            PhFlagsTestUtil.setIsolatedServiceAllowList(originalIsolatedServiceAllowList);
-        }
+        ExtendedMockito.doReturn(false)
+                .when(() -> PartnerEnrollmentChecker.isIsolatedServiceEnrolled(any()));
+
+        assertThrows(
+                IllegalStateException.class,
+                () ->
+                        mService.execute(
+                                mContext.getPackageName(),
+                                new ComponentName(
+                                        mContext.getPackageName(),
+                                        "com.test.TestPersonalizationHandler"),
+                                createWrappedAppParams(),
+                                new CallerMetadata.Builder().build(),
+                                ExecuteOptionsParcel.DEFAULT,
+                                callback));
     }
 
     @Test
     public void testEnabledGlobalKillSwitchOnRequestSurfacePackage() throws Exception {
-        when(mSpyFlags.getGlobalKillSwitch()).thenReturn(true);
+        when(mMockFlags.getGlobalKillSwitch()).thenReturn(true);
         var callback = new RequestSurfacePackageCallback();
         assertThrows(
                 IllegalStateException.class,
@@ -356,8 +399,7 @@
                                 100,
                                 50,
                                 new CallerMetadata.Builder().build(),
-                                callback
-                        ));
+                                callback));
     }
 
     @Test
@@ -374,8 +416,7 @@
                                 100,
                                 50,
                                 new CallerMetadata.Builder().build(),
-                                callback
-                        ));
+                                callback));
     }
 
     @Test
@@ -480,13 +521,7 @@
                 NullPointerException.class,
                 () ->
                         mService.requestSurfacePackage(
-                                "resultToken",
-                                new Binder(),
-                                0,
-                                100,
-                                50,
-                                null,
-                                callback));
+                                "resultToken", new Binder(), 0, 100, 50, null, callback));
     }
 
     @Test
@@ -506,7 +541,7 @@
 
     @Test
     public void testEnabledGlobalKillSwitchOnRegisterMeasurementEvent() throws Exception {
-        when(mSpyFlags.getGlobalKillSwitch()).thenReturn(true);
+        when(mMockFlags.getGlobalKillSwitch()).thenReturn(true);
         assertThrows(
                 IllegalStateException.class,
                 () ->
@@ -558,8 +593,8 @@
 
     @Test
     public void testWithBoundService() throws TimeoutException {
-        Intent serviceIntent = new Intent(mContext,
-                OnDevicePersonalizationManagingServiceImpl.class);
+        Intent serviceIntent =
+                new Intent(mContext, OnDevicePersonalizationManagingServiceImpl.class);
         IBinder binder = serviceRule.bindService(serviceIntent);
         assertTrue(binder instanceof OnDevicePersonalizationManagingServiceDelegate);
     }
@@ -581,8 +616,9 @@
 
     private Bundle createWrappedAppParams() throws Exception {
         Bundle wrappedParams = new Bundle();
-        ByteArrayParceledSlice buffer = new ByteArrayParceledSlice(
-                PersistableBundleUtils.toByteArray(PersistableBundle.EMPTY));
+        ByteArrayParceledSlice buffer =
+                new ByteArrayParceledSlice(
+                        PersistableBundleUtils.toByteArray(PersistableBundle.EMPTY));
         wrappedParams.putParcelable(Constants.EXTRA_APP_PARAMS_SERIALIZED, buffer);
         return wrappedParams;
     }
@@ -593,7 +629,7 @@
         public boolean mError = false;
         public int mErrorCode = 0;
         public int mIsolatedServiceErrorCode = 0;
-        public String mErrorMessage = null;
+        public byte[] mSerializedException = null;
         public String mToken = null;
         private final CountDownLatch mLatch = new CountDownLatch(1);
 
@@ -608,13 +644,16 @@
         }
 
         @Override
-        public void onError(int errorCode, int isolatedServiceErrorCode, String message,
+        public void onError(
+                int errorCode,
+                int isolatedServiceErrorCode,
+                byte[] serializedException,
                 CalleeMetadata calleeMetadata) {
             mWasInvoked = true;
             mError = true;
             mErrorCode = errorCode;
             mIsolatedServiceErrorCode = isolatedServiceErrorCode;
-            mErrorMessage = message;
+            mSerializedException = serializedException;
             mLatch.countDown();
         }
 
@@ -629,25 +668,28 @@
         public boolean mError = false;
         public int mErrorCode = 0;
         public int mIsolatedServiceErrorCode = 0;
-        public String mErrorMessage = null;
+        public byte[] mSerializedException = null;
         private final CountDownLatch mLatch = new CountDownLatch(1);
 
         @Override
-        public void onSuccess(SurfaceControlViewHost.SurfacePackage s,
-                CalleeMetadata calleeMetadata) {
+        public void onSuccess(
+                SurfaceControlViewHost.SurfacePackage s, CalleeMetadata calleeMetadata) {
             mWasInvoked = true;
             mSuccess = true;
             mLatch.countDown();
         }
 
         @Override
-        public void onError(int errorCode, int isolatedServiceErrorCode, String message,
+        public void onError(
+                int errorCode,
+                int isolatedServiceErrorCode,
+                byte[] serializedException,
                 CalleeMetadata calleeMetadata) {
             mWasInvoked = true;
             mError = true;
             mErrorCode = errorCode;
             mIsolatedServiceErrorCode = isolatedServiceErrorCode;
-            mErrorMessage = message;
+            mSerializedException = serializedException;
             mLatch.countDown();
         }
 
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/PhFlagsTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/PhFlagsTest.java
index 386074d..5069fe9 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/PhFlagsTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/PhFlagsTest.java
@@ -16,11 +16,15 @@
 
 package com.android.ondevicepersonalization.services;
 
-import static com.android.adservices.shared.common.flags.ModuleSharedFlags.BACKGROUND_JOB_LOGGING_ENABLED;
 import static com.android.adservices.shared.common.flags.ModuleSharedFlags.BACKGROUND_JOB_SAMPLING_LOGGING_RATE;
-import static com.android.adservices.shared.common.flags.ModuleSharedFlags.DEFAULT_JOB_SCHEDULING_LOGGING_ENABLED;
 import static com.android.adservices.shared.common.flags.ModuleSharedFlags.DEFAULT_JOB_SCHEDULING_LOGGING_SAMPLING_RATE;
 import static com.android.ondevicepersonalization.services.Flags.APP_REQUEST_FLOW_DEADLINE_SECONDS;
+import static com.android.ondevicepersonalization.services.Flags.DEFAULT_ADSERVICES_IPC_CALL_TIMEOUT_IN_MILLIS;
+import static com.android.ondevicepersonalization.services.Flags.DEFAULT_AGGREGATED_ERROR_REPORTING_ENABLED;
+import static com.android.ondevicepersonalization.services.Flags.DEFAULT_AGGREGATED_ERROR_REPORTING_INTERVAL_HOURS;
+import static com.android.ondevicepersonalization.services.Flags.DEFAULT_AGGREGATED_ERROR_REPORTING_THRESHOLD;
+import static com.android.ondevicepersonalization.services.Flags.DEFAULT_AGGREGATED_ERROR_REPORTING_URL_PATH;
+import static com.android.ondevicepersonalization.services.Flags.DEFAULT_AGGREGATED_ERROR_REPORT_TTL_DAYS;
 import static com.android.ondevicepersonalization.services.Flags.DEFAULT_APP_INSTALL_HISTORY_TTL_MILLIS;
 import static com.android.ondevicepersonalization.services.Flags.DEFAULT_CALLER_APP_ALLOW_LIST;
 import static com.android.ondevicepersonalization.services.Flags.DEFAULT_CLIENT_ERROR_LOGGING_ENABLED;
@@ -40,16 +44,21 @@
 import static com.android.ondevicepersonalization.services.Flags.WEB_TRIGGER_FLOW_DEADLINE_SECONDS;
 import static com.android.ondevicepersonalization.services.Flags.WEB_VIEW_FLOW_DEADLINE_SECONDS;
 import static com.android.ondevicepersonalization.services.PhFlags.APP_INSTALL_HISTORY_TTL;
+import static com.android.ondevicepersonalization.services.PhFlags.KEY_ADSERVICES_IPC_CALL_TIMEOUT_IN_MILLIS;
+import static com.android.ondevicepersonalization.services.PhFlags.KEY_AGGREGATED_ERROR_REPORTING_INTERVAL_HOURS;
+import static com.android.ondevicepersonalization.services.PhFlags.KEY_AGGREGATED_ERROR_REPORTING_PATH;
+import static com.android.ondevicepersonalization.services.PhFlags.KEY_AGGREGATED_ERROR_REPORTING_THRESHOLD;
+import static com.android.ondevicepersonalization.services.PhFlags.KEY_AGGREGATED_ERROR_REPORT_TTL_DAYS;
 import static com.android.ondevicepersonalization.services.PhFlags.KEY_APP_REQUEST_FLOW_DEADLINE_SECONDS;
 import static com.android.ondevicepersonalization.services.PhFlags.KEY_CALLER_APP_ALLOW_LIST;
 import static com.android.ondevicepersonalization.services.PhFlags.KEY_DOWNLOAD_FLOW_DEADLINE_SECONDS;
+import static com.android.ondevicepersonalization.services.PhFlags.KEY_ENABLE_AGGREGATED_ERROR_REPORTING;
 import static com.android.ondevicepersonalization.services.PhFlags.KEY_ENABLE_PERSONALIZATION_STATUS_OVERRIDE;
 import static com.android.ondevicepersonalization.services.PhFlags.KEY_EXAMPLE_STORE_FLOW_DEADLINE_SECONDS;
 import static com.android.ondevicepersonalization.services.PhFlags.KEY_GLOBAL_KILL_SWITCH;
 import static com.android.ondevicepersonalization.services.PhFlags.KEY_ISOLATED_SERVICE_ALLOW_LIST;
 import static com.android.ondevicepersonalization.services.PhFlags.KEY_ISOLATED_SERVICE_DEBUGGING_ENABLED;
 import static com.android.ondevicepersonalization.services.PhFlags.KEY_IS_ART_IMAGE_LOADING_OPTIMIZATION_ENABLED;
-import static com.android.ondevicepersonalization.services.PhFlags.KEY_ODP_BACKGROUND_JOBS_LOGGING_ENABLED;
 import static com.android.ondevicepersonalization.services.PhFlags.KEY_ODP_BACKGROUND_JOB_SAMPLING_LOGGING_RATE;
 import static com.android.ondevicepersonalization.services.PhFlags.KEY_ODP_ENABLE_CLIENT_ERROR_LOGGING;
 import static com.android.ondevicepersonalization.services.PhFlags.KEY_ODP_JOB_SCHEDULING_LOGGING_ENABLED;
@@ -89,19 +98,6 @@
         PhFlagsTestUtil.setUpDeviceConfigPermissions();
     }
 
-    @Test(expected = IllegalArgumentException.class)
-    public void testInvalidStableFlags() {
-        FlagsFactory.getFlags().getStableFlag("INVALID_FLAG_NAME");
-    }
-
-    @Test
-    public void testValidStableFlags() {
-        Object isSipFeatureEnabled = FlagsFactory.getFlags()
-                .getStableFlag(KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED);
-
-        assertThat(isSipFeatureEnabled).isNotNull();
-    }
-
     @Test
     public void testGetGlobalKillSwitch() {
         // Without any overriding, the value is the hard coded constant.
@@ -513,28 +509,13 @@
 
     @Test
     public void testGetBackgroundJobsLoggingEnabled() {
-        // read a stable flag value and verify it's equal to the default value.
-        boolean stableValue = FlagsFactory.getFlags().getBackgroundJobsLoggingEnabled();
-        assertThat(stableValue).isEqualTo(BACKGROUND_JOB_LOGGING_ENABLED);
-
-        // override the value in device config.
-        boolean overrideEnabled = !stableValue;
-        DeviceConfig.setProperty(
-                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
-                KEY_ODP_BACKGROUND_JOBS_LOGGING_ENABLED,
-                Boolean.toString(overrideEnabled),
-                /* makeDefault= */ false);
-
-        // the flag value remains stable
         assertThat(FlagsFactory.getFlags().getBackgroundJobsLoggingEnabled())
-                .isEqualTo(stableValue);
+                .isEqualTo(true);
     }
 
     @Test
     public void testGetBackgroundJobSamplingLoggingRate() {
         int defaultValue = BACKGROUND_JOB_SAMPLING_LOGGING_RATE;
-        assertThat(FlagsFactory.getFlags().getBackgroundJobSamplingLoggingRate())
-                .isEqualTo(defaultValue);
 
         // Override the value in device config.
         int overrideRate = defaultValue + 1;
@@ -551,7 +532,6 @@
     public void testGetJobSchedulingLoggingEnabled() {
         // read a stable flag value and verify it's equal to the default value.
         boolean stableValue = FlagsFactory.getFlags().getJobSchedulingLoggingEnabled();
-        assertThat(stableValue).isEqualTo(DEFAULT_JOB_SCHEDULING_LOGGING_ENABLED);
 
         // override the value in device config.
         boolean overrideEnabled = !stableValue;
@@ -569,8 +549,6 @@
     @Test
     public void testGetJobSchedulingLoggingSamplingRate() {
         int defaultValue = DEFAULT_JOB_SCHEDULING_LOGGING_SAMPLING_RATE;
-        assertThat(FlagsFactory.getFlags().getJobSchedulingLoggingSamplingRate())
-                .isEqualTo(defaultValue);
 
         // Override the value in device config.
         int overrideRate = defaultValue + 1;
@@ -633,4 +611,135 @@
         assertThat(FlagsFactory.getFlags().getAppInstallHistoryTtlInMillis())
                 .isEqualTo(overrideEnabled);
     }
+
+    @Test
+    public void testAggregateErrorReportingEnabled() {
+        boolean testValue = !DEFAULT_AGGREGATED_ERROR_REPORTING_ENABLED;
+
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                KEY_ENABLE_AGGREGATED_ERROR_REPORTING,
+                Boolean.toString(testValue),
+                /* makeDefault */ false);
+
+        assertThat(FlagsFactory.getFlags().getAggregatedErrorReportingEnabled())
+                .isEqualTo(testValue);
+
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                KEY_ENABLE_AGGREGATED_ERROR_REPORTING,
+                Boolean.toString(DEFAULT_AGGREGATED_ERROR_REPORTING_ENABLED),
+                /* makeDefault */ false);
+        assertThat(FlagsFactory.getFlags().getAggregatedErrorReportingEnabled())
+                .isEqualTo(DEFAULT_AGGREGATED_ERROR_REPORTING_ENABLED);
+    }
+
+    @Test
+    public void testAggregateErrorReportingTtlDays() {
+        int testValue = 4;
+
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                KEY_AGGREGATED_ERROR_REPORT_TTL_DAYS,
+                Integer.toString(testValue),
+                /* makeDefault */ false);
+
+        assertThat(FlagsFactory.getFlags().getAggregatedErrorReportingTtlInDays())
+                .isEqualTo(testValue);
+
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                KEY_AGGREGATED_ERROR_REPORT_TTL_DAYS,
+                Integer.toString(DEFAULT_AGGREGATED_ERROR_REPORT_TTL_DAYS),
+                /* makeDefault */ false);
+        assertThat(FlagsFactory.getFlags().getAggregatedErrorReportingTtlInDays())
+                .isEqualTo(DEFAULT_AGGREGATED_ERROR_REPORT_TTL_DAYS);
+    }
+
+    @Test
+    public void testAggregateErrorReportingUrlPath() {
+        String testValue = "foo/bar";
+
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                KEY_AGGREGATED_ERROR_REPORTING_PATH,
+                testValue,
+                /* makeDefault */ false);
+
+        assertThat(FlagsFactory.getFlags().getAggregatedErrorReportingServerPath())
+                .isEqualTo(testValue);
+
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                KEY_AGGREGATED_ERROR_REPORTING_PATH,
+                DEFAULT_AGGREGATED_ERROR_REPORTING_URL_PATH,
+                /* makeDefault */ false);
+        assertThat(FlagsFactory.getFlags().getAggregatedErrorReportingServerPath())
+                .isEqualTo(DEFAULT_AGGREGATED_ERROR_REPORTING_URL_PATH);
+    }
+
+    @Test
+    public void testAggregateErrorReportingThreshold() {
+        int testValue = 5;
+
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                KEY_AGGREGATED_ERROR_REPORTING_THRESHOLD,
+                Integer.toString(testValue),
+                /* makeDefault */ false);
+
+        assertThat(FlagsFactory.getFlags().getAggregatedErrorMinThreshold()).isEqualTo(testValue);
+
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                KEY_AGGREGATED_ERROR_REPORTING_THRESHOLD,
+                Integer.toString(DEFAULT_AGGREGATED_ERROR_REPORTING_THRESHOLD),
+                /* makeDefault */ false);
+        assertThat(FlagsFactory.getFlags().getAggregatedErrorMinThreshold())
+                .isEqualTo(DEFAULT_AGGREGATED_ERROR_REPORTING_THRESHOLD);
+    }
+
+    @Test
+    public void testAggregateErrorReportingInterval() {
+        int testValue = 4;
+
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                KEY_AGGREGATED_ERROR_REPORTING_INTERVAL_HOURS,
+                Integer.toString(testValue),
+                /* makeDefault */ false);
+
+        assertThat(FlagsFactory.getFlags().getAggregatedErrorReportingIntervalInHours())
+                .isEqualTo(testValue);
+
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                KEY_AGGREGATED_ERROR_REPORTING_INTERVAL_HOURS,
+                Integer.toString(DEFAULT_AGGREGATED_ERROR_REPORTING_INTERVAL_HOURS),
+                /* makeDefault */ false);
+        assertThat(FlagsFactory.getFlags().getAggregatedErrorReportingIntervalInHours())
+                .isEqualTo(DEFAULT_AGGREGATED_ERROR_REPORTING_INTERVAL_HOURS);
+    }
+
+    @Test
+    public void testGetAdservicesIpcCallTimeoutInMillis() {
+        long testTimeoutValue = 100L;
+
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                KEY_ADSERVICES_IPC_CALL_TIMEOUT_IN_MILLIS,
+                Long.toString(testTimeoutValue),
+                /* makeDefault */ false);
+
+        assertThat(FlagsFactory.getFlags().getAdservicesIpcCallTimeoutInMillis())
+                .isEqualTo(testTimeoutValue);
+
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                KEY_ADSERVICES_IPC_CALL_TIMEOUT_IN_MILLIS,
+                Long.toString(DEFAULT_ADSERVICES_IPC_CALL_TIMEOUT_IN_MILLIS),
+                /* makeDefault */ false);
+        assertThat(FlagsFactory.getFlags().getAdservicesIpcCallTimeoutInMillis())
+                .isEqualTo(DEFAULT_ADSERVICES_IPC_CALL_TIMEOUT_IN_MILLIS);
+    }
 }
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/PhFlagsTestUtil.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/PhFlagsTestUtil.java
index ef54006..c4efe55 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/PhFlagsTestUtil.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/PhFlagsTestUtil.java
@@ -26,7 +26,7 @@
 
 import android.provider.DeviceConfig;
 
-import androidx.test.InstrumentationRegistry;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 public class PhFlagsTestUtil {
     private static final String WRITE_DEVICE_CONFIG_PERMISSION =
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/StableFlagsTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/StableFlagsTest.java
new file mode 100644
index 0000000..56caab0
--- /dev/null
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/StableFlagsTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.ondevicepersonalization.services;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.quality.Strictness;
+
+
+@RunWith(JUnit4.class)
+public final class StableFlagsTest {
+    @Rule
+    public final ExtendedMockitoRule mExtendedMockitoRule =
+            new ExtendedMockitoRule.Builder(this)
+                    .spyStatic(FlagsFactory.class)
+                    .setStrictness(Strictness.LENIENT)
+                    .build();
+
+    @Before
+    public void setUp() {
+        ExtendedMockito.doReturn(new Flags() {}).when(FlagsFactory::getFlags);
+    }
+
+    @Test
+    public void testValidStableFlags() {
+        Object isSipFeatureEnabled =
+                StableFlags.get(PhFlags.KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED);
+
+        assertThat(isSipFeatureEnabled).isNotNull();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidStableFlags() {
+        StableFlags.get("INVALID_FLAG_NAME");
+    }
+}
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/data/errors/AggregateErrorDataReportingServiceTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/data/errors/AggregateErrorDataReportingServiceTest.java
new file mode 100644
index 0000000..91fc9b2
--- /dev/null
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/data/errors/AggregateErrorDataReportingServiceTest.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2024 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.ondevicepersonalization.services.data.errors;
+
+import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.AGGREGATE_ERROR_DATA_REPORTING_JOB_ID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.ondevicepersonalization.services.Flags;
+import com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig;
+
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(JUnit4.class)
+public class AggregateErrorDataReportingServiceTest {
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final JobScheduler mJobScheduler = mContext.getSystemService(JobScheduler.class);
+
+    private AggregateErrorDataReportingService mService;
+
+    @Mock private Flags mMockFlags;
+
+    @Before
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mService = spy(new AggregateErrorDataReportingService(new TestInjector()));
+        doNothing().when(mService).jobFinished(any(), anyBoolean());
+
+        // Setup tests with the global kill switch is disabled and error reporting enabled.
+        when(mMockFlags.getGlobalKillSwitch()).thenReturn(false);
+        when(mMockFlags.getAggregatedErrorReportingEnabled()).thenReturn(true);
+        if (mJobScheduler != null) {
+            // Cleanup any pending jobs
+            mJobScheduler.cancel(AGGREGATE_ERROR_DATA_REPORTING_JOB_ID);
+        }
+    }
+
+    @Test
+    public void onStartJobTestKillSwitchEnabled_jobCancelled() {
+        // Given that the aggregate error reporting job service is already scheduled and the global
+        // kill switch is enabled (that is ODP is disabled).
+        when(mMockFlags.getGlobalKillSwitch()).thenReturn(true);
+        doReturn(mJobScheduler).when(mService).getSystemService(JobScheduler.class);
+        assertEquals(
+                JobScheduler.RESULT_SUCCESS,
+                AggregateErrorDataReportingService.scheduleIfNeeded(mContext, mMockFlags));
+        assertNotNull(
+                mJobScheduler.getPendingJob(
+                        OnDevicePersonalizationConfig.AGGREGATE_ERROR_DATA_REPORTING_JOB_ID));
+
+        // When the job is started.
+        boolean result = mService.onStartJob(mock(JobParameters.class));
+
+        // Expect that the pending job is cancelled.
+        assertTrue(result);
+        verify(mService, times(1)).jobFinished(any(), eq(false));
+        assertNull(
+                mJobScheduler.getPendingJob(
+                        OnDevicePersonalizationConfig.AGGREGATE_ERROR_DATA_REPORTING_JOB_ID));
+    }
+
+    @Test
+    public void onStartJobTestAggregateReportingDisabled_jobCancelled() {
+        // Given that the aggregate error reporting job service is already scheduled and the error
+        // reporting flag has been disabled.
+        doReturn(mJobScheduler).when(mService).getSystemService(JobScheduler.class);
+        assertEquals(
+                JobScheduler.RESULT_SUCCESS,
+                AggregateErrorDataReportingService.scheduleIfNeeded(mContext, mMockFlags));
+        assertNotNull(
+                mJobScheduler.getPendingJob(
+                        OnDevicePersonalizationConfig.AGGREGATE_ERROR_DATA_REPORTING_JOB_ID));
+
+        // When the job is started with error reporting disabled.
+        when(mMockFlags.getAggregatedErrorReportingEnabled()).thenReturn(false);
+        boolean result = mService.onStartJob(mock(JobParameters.class));
+
+        // Expect that the job is cancelled and no more pending jobs.
+        assertTrue(result);
+        verify(mService, times(1)).jobFinished(any(), eq(false));
+        assertNull(
+                mJobScheduler.getPendingJob(
+                        OnDevicePersonalizationConfig.AGGREGATE_ERROR_DATA_REPORTING_JOB_ID));
+    }
+
+    @Test
+    public void onStopJobTest() {
+        assertTrue(mService.onStopJob(mock(JobParameters.class)));
+    }
+
+    @Test
+    public void scheduleIfNeeded_AggregateErrorReportingDisabled() {
+        when(mMockFlags.getAggregatedErrorReportingEnabled()).thenReturn(false);
+
+        assertEquals(
+                JobScheduler.RESULT_FAILURE,
+                AggregateErrorDataReportingService.scheduleIfNeeded(mContext, mMockFlags));
+    }
+
+    @Test
+    public void scheduleIfNeeded_AggregateErrorReportingEnabled() {
+        when(mMockFlags.getAggregatedErrorReportingEnabled()).thenReturn(true);
+
+        assertEquals(
+                JobScheduler.RESULT_SUCCESS,
+                AggregateErrorDataReportingService.scheduleIfNeeded(mContext, mMockFlags));
+    }
+
+    private class TestInjector extends AggregateErrorDataReportingService.Injector {
+        @Override
+        ListeningExecutorService getExecutor() {
+            return MoreExecutors.newDirectExecutorService();
+        }
+
+        @Override
+        Flags getFlags() {
+            return mMockFlags;
+        }
+    }
+}
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/data/errors/AggregatedErrorCodesLoggerTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/data/errors/AggregatedErrorCodesLoggerTest.java
new file mode 100644
index 0000000..b943932
--- /dev/null
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/data/errors/AggregatedErrorCodesLoggerTest.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2024 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.ondevicepersonalization.services.data.errors;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.quality.Strictness.LENIENT;
+
+import android.content.ComponentName;
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.odp.module.common.PackageUtils;
+import com.android.ondevicepersonalization.services.Flags;
+import com.android.ondevicepersonalization.services.FlagsFactory;
+import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.MockitoSession;
+
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class AggregatedErrorCodesLoggerTest {
+
+    private static final String TEST_CERT_DIGEST = "test_cert_digest";
+    private static final String TEST_PACKAGE = "test_package";
+    private static final String TEST_CLASS = "test_class";
+
+    private static final int TEST_ISOLATED_SERVICE_ERROR_CODE = 2;
+
+    private static final ComponentName TEST_COMPONENT_NAME =
+            new ComponentName(TEST_PACKAGE, TEST_CLASS);
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private MockitoSession mSession;
+
+    private int mDayIndexUtc;
+    private final OnDevicePersonalizationAggregatedErrorDataDao mErrorDataDao =
+            OnDevicePersonalizationAggregatedErrorDataDao.getInstance(
+                    mContext, TEST_COMPONENT_NAME, TEST_CERT_DIGEST);
+
+    @Before
+    public void setUp() {
+        mDayIndexUtc = DateTimeUtils.dayIndexUtc();
+
+        mSession =
+                ExtendedMockito.mockitoSession()
+                        .mockStatic(FlagsFactory.class)
+                        .mockStatic(PackageUtils.class)
+                        .spyStatic(OnDevicePersonalizationExecutors.class)
+                        .strictness(LENIENT)
+                        .startMocking();
+
+        ExtendedMockito.doReturn(MoreExecutors.newDirectExecutorService())
+                .when(OnDevicePersonalizationExecutors::getBackgroundExecutor);
+        doReturn(TEST_CERT_DIGEST).when(() -> PackageUtils.getCertDigest(any(), any()));
+        mErrorDataDao.deleteExceptionData();
+    }
+
+    @Test
+    public void logIsolatedServiceErrorCode_flagDisabled_skipsLogging() throws Exception {
+        doReturn(new TestFlags(false)).when(FlagsFactory::getFlags);
+
+        ListenableFuture<?> loggingFuture =
+                AggregatedErrorCodesLogger.logIsolatedServiceErrorCode(
+                        TEST_ISOLATED_SERVICE_ERROR_CODE, TEST_COMPONENT_NAME, mContext);
+
+        assertTrue(loggingFuture.isDone());
+        assertTrue(mErrorDataDao.getExceptionData().isEmpty());
+    }
+
+    @Test
+    public void logIsolatedServiceErrorCode_flagEnabled_logsException() {
+        doReturn(new TestFlags(true)).when(FlagsFactory::getFlags);
+
+        ListenableFuture<?> loggingFuture =
+                AggregatedErrorCodesLogger.logIsolatedServiceErrorCode(
+                        TEST_ISOLATED_SERVICE_ERROR_CODE, TEST_COMPONENT_NAME, mContext);
+
+        List<ErrorData> exceptionData = mErrorDataDao.getExceptionData();
+        assertTrue(loggingFuture.isDone());
+        assertEquals(1, exceptionData.size());
+        assertEquals(getExpectedErrorData(mDayIndexUtc), exceptionData.get(0));
+    }
+
+    @Test
+    public void cleanupAggregatedErrorData_flagDisabled_skipsCleanup() {
+        doReturn(new TestFlags(false)).when(FlagsFactory::getFlags);
+        mErrorDataDao.addExceptionCount(TEST_ISOLATED_SERVICE_ERROR_CODE, /* exceptionCount= */ 1);
+
+        ListenableFuture<?> cleanupFuture =
+                AggregatedErrorCodesLogger.cleanupAggregatedErrorData(mContext);
+
+        List<ErrorData> exceptionData = mErrorDataDao.getExceptionData();
+        assertTrue(cleanupFuture.isDone());
+        assertEquals(1, exceptionData.size());
+        assertEquals(getExpectedErrorData(mDayIndexUtc), exceptionData.get(0));
+    }
+
+    @Test
+    public void cleanupAggregatedErrorData_flagEnabled_performsCleanup() {
+        doReturn(new TestFlags(true)).when(FlagsFactory::getFlags);
+        mErrorDataDao.addExceptionCount(TEST_ISOLATED_SERVICE_ERROR_CODE, /* exceptionCount= */ 1);
+
+        ListenableFuture<?> cleanupFuture =
+                AggregatedErrorCodesLogger.cleanupAggregatedErrorData(mContext);
+
+        assertTrue(cleanupFuture.isDone());
+        assertTrue(mErrorDataDao.getExceptionData().isEmpty());
+        assertTrue(
+                OnDevicePersonalizationAggregatedErrorDataDao.getErrorDataTableNames(mContext)
+                        .isEmpty());
+    }
+
+    @After
+    public void tearDown() {
+        mSession.finishMocking();
+    }
+
+    private static ErrorData getExpectedErrorData(int dayIndexUtc) {
+        return new ErrorData.Builder(TEST_ISOLATED_SERVICE_ERROR_CODE, 1, dayIndexUtc, 0).build();
+    }
+
+    private static final class TestFlags implements Flags {
+        private final boolean mAggregateErrorReportingEnabled;
+
+        private TestFlags(boolean aggregateErrorReportingEnabled) {
+            mAggregateErrorReportingEnabled = aggregateErrorReportingEnabled;
+        }
+
+        @Override
+        public boolean getAggregatedErrorReportingEnabled() {
+            return mAggregateErrorReportingEnabled;
+        }
+    }
+}
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/data/errors/DateTimeUtilsTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/data/errors/DateTimeUtilsTest.java
new file mode 100644
index 0000000..e07e1f4
--- /dev/null
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/data/errors/DateTimeUtilsTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2024 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.ondevicepersonalization.services.data.errors;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+import com.android.odp.module.common.Clock;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.quality.Strictness;
+
+import java.util.TimeZone;
+
+@RunWith(JUnit4.class)
+public class DateTimeUtilsTest {
+    @Rule
+    public final ExtendedMockitoRule extendedMockitoRule =
+            new ExtendedMockitoRule.Builder(this).setStrictness(Strictness.LENIENT).build();
+
+    // PST: Friday, August 23, 2024 10:59:11 PM
+    private static final long DEFAULT_CURRENT_TIME_MILLIS = 1724479151000L;
+    private static final int CURRENT_DAYS_EPOCH_PST = 19958;
+    private Context mContext;
+
+    @Mock private Clock mMockClock;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mContext = ApplicationProvider.getApplicationContext();
+
+        TimeZone pstTime = TimeZone.getTimeZone("GMT-08:00");
+        TimeZone.setDefault(pstTime);
+        when(mMockClock.currentTimeMillis()).thenReturn(DEFAULT_CURRENT_TIME_MILLIS);
+    }
+
+    @Test
+    public void testDayIndexUtc() {
+        // UTC day is into the next day, Aug 24th 2024.
+        int dayEpoch = DateTimeUtils.dayIndexUtc(mMockClock);
+
+        assertEquals(CURRENT_DAYS_EPOCH_PST + 1, dayEpoch);
+    }
+
+    @Test
+    public void testDayIndexLocal() {
+        int dayEpoch = DateTimeUtils.dayIndexLocal(mMockClock);
+
+        assertEquals(CURRENT_DAYS_EPOCH_PST, dayEpoch);
+    }
+}
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/data/errors/OnDevicePersonalizationAggregatedErrorDataDaoTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/data/errors/OnDevicePersonalizationAggregatedErrorDataDaoTest.java
new file mode 100644
index 0000000..e074cd6
--- /dev/null
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/data/errors/OnDevicePersonalizationAggregatedErrorDataDaoTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2024 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.ondevicepersonalization.services.data.errors;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+
+import android.content.ComponentName;
+import android.content.Context;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+import com.android.odp.module.common.PackageUtils;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.quality.Strictness;
+
+@RunWith(JUnit4.class)
+public class OnDevicePersonalizationAggregatedErrorDataDaoTest {
+    private static final String TEST_PACKAGE = "ownerPkg";
+    private static final String OTHER_PACKAGE = "otherPkg";
+    private static final ComponentName TEST_OWNER = new ComponentName(TEST_PACKAGE, "ownerCls");
+    private static final ComponentName OTHER_OWNER = new ComponentName(OTHER_PACKAGE, "otherCls");
+    private static final String TEST_CERT_DIGEST = "certDigest1";
+    private static final String OTHER_CERT_DIGEST = "certDigest2";
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private OnDevicePersonalizationAggregatedErrorDataDao mDao;
+
+    @Rule
+    public final ExtendedMockitoRule mExtendedMockitoRule =
+            new ExtendedMockitoRule.Builder(this)
+                    .spyStatic(PackageUtils.class)
+                    .setStrictness(Strictness.LENIENT)
+                    .build();
+
+    @Before
+    public void setup() {
+        mDao =
+                OnDevicePersonalizationAggregatedErrorDataDao.getInstance(
+                        mContext, TEST_OWNER, TEST_CERT_DIGEST);
+
+        // Cleanup any existing records
+        mDao.deleteExceptionData();
+        ExtendedMockito.doReturn(TEST_CERT_DIGEST)
+                .when(() -> PackageUtils.getCertDigest(any(), eq(TEST_PACKAGE)));
+        ExtendedMockito.doReturn(OTHER_CERT_DIGEST)
+                .when(() -> PackageUtils.getCertDigest(any(), eq(OTHER_PACKAGE)));
+        OnDevicePersonalizationAggregatedErrorDataDao.cleanupErrorData(
+                mContext, /* excludedServices= */ ImmutableList.of());
+    }
+
+    @Test
+    public void testAddExceptionCount_InvalidCode_Fails() {
+        assertFalse(
+                mDao.addExceptionCount(
+                        OnDevicePersonalizationAggregatedErrorDataDao.MAX_ALLOWED_ERROR_CODE + 1,
+                        1));
+    }
+
+    @Test
+    public void testAddExceptionCount_Success() {
+        assertTrue(mDao.addExceptionCount(1, 1));
+        assertTrue(mDao.addExceptionCount(1, 1));
+        assertThat(mDao.getExceptionData()).hasSize(1);
+    }
+
+    @Test
+    public void testDeleteExceptionData_Success() {
+        // Given two records are added to the Dao
+        mDao.addExceptionCount(1, 1);
+        mDao.addExceptionCount(2, 1);
+        ImmutableList<ErrorData> originalData = mDao.getExceptionData();
+
+        // Expect that calling delete clears the table
+        assertTrue(mDao.deleteExceptionData());
+        assertThat(mDao.getExceptionData()).isEmpty();
+        assertThat(originalData).hasSize(2);
+    }
+
+    @Test
+    public void testGetInstance() {
+        OnDevicePersonalizationAggregatedErrorDataDao owner1Instance1 =
+                OnDevicePersonalizationAggregatedErrorDataDao.getInstance(
+                        mContext, TEST_OWNER, TEST_CERT_DIGEST);
+        OnDevicePersonalizationAggregatedErrorDataDao owner1Instance2 =
+                OnDevicePersonalizationAggregatedErrorDataDao.getInstance(
+                        mContext, TEST_OWNER, TEST_CERT_DIGEST);
+        OnDevicePersonalizationAggregatedErrorDataDao owner2Instance1 =
+                OnDevicePersonalizationAggregatedErrorDataDao.getInstance(
+                        mContext, OTHER_OWNER, TEST_CERT_DIGEST);
+
+        assertNotNull(owner1Instance1);
+        assertNotNull(owner2Instance1);
+        assertThat(owner1Instance1).isSameInstanceAs(owner1Instance2);
+        assertNotEquals(owner1Instance1, owner2Instance1);
+    }
+
+    @Test
+    public void testGetMatchingTables() {
+        // Given two tables with some error data
+        OnDevicePersonalizationAggregatedErrorDataDao instance1 =
+                OnDevicePersonalizationAggregatedErrorDataDao.getInstance(
+                        mContext, TEST_OWNER, TEST_CERT_DIGEST);
+        OnDevicePersonalizationAggregatedErrorDataDao instance2 =
+                OnDevicePersonalizationAggregatedErrorDataDao.getInstance(
+                        mContext, OTHER_OWNER, TEST_CERT_DIGEST);
+        instance1.addExceptionCount(1, 1);
+        instance2.addExceptionCount(2, 1);
+        int originalCount =
+                OnDevicePersonalizationAggregatedErrorDataDao.getErrorDataTableNames(mContext)
+                        .size();
+
+        // Expect that no tables exist after cleanup
+        OnDevicePersonalizationAggregatedErrorDataDao.cleanupErrorData(
+                mContext, /* excludedServices= */ ImmutableList.of());
+
+        assertEquals(2, originalCount);
+        assertThat(OnDevicePersonalizationAggregatedErrorDataDao.getErrorDataTableNames(mContext))
+                .isEmpty();
+    }
+}
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/data/user/UserDataCollectionJobServiceTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/data/user/UserDataCollectionJobServiceTest.java
index b2b213e..7eac2c5 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/data/user/UserDataCollectionJobServiceTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/data/user/UserDataCollectionJobServiceTest.java
@@ -18,6 +18,7 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -28,6 +29,7 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.app.job.JobParameters;
 import android.app.job.JobScheduler;
@@ -36,122 +38,45 @@
 import androidx.test.core.app.ApplicationProvider;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+import com.android.ondevicepersonalization.services.Flags;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig;
-import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
-import com.android.ondevicepersonalization.services.PhFlagsTestUtil;
 
+import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.MockitoSession;
+import org.mockito.Mock;
 import org.mockito.quality.Strictness;
 
 @RunWith(JUnit4.class)
 public class UserDataCollectionJobServiceTest {
+    @Rule(order = 0)
+    public final ExtendedMockitoRule extendedMockitoRule =
+            new ExtendedMockitoRule.Builder(this)
+                    .spyStatic(UserPrivacyStatus.class)
+                    .setStrictness(Strictness.LENIENT)
+                    .build();
+
     private final Context mContext = ApplicationProvider.getApplicationContext();
     private final JobScheduler mJobScheduler = mContext.getSystemService(JobScheduler.class);
     private UserDataCollector mUserDataCollector;
     private UserDataCollectionJobService mService;
     private UserPrivacyStatus mUserPrivacyStatus;
+    @Mock private Flags mMockFlags;
 
     @Before
     public void setup() throws Exception {
-        PhFlagsTestUtil.setUpDeviceConfigPermissions();
-        PhFlagsTestUtil.disableGlobalKillSwitch();
         mUserPrivacyStatus = spy(UserPrivacyStatus.getInstance());
         mUserDataCollector = UserDataCollector.getInstanceForTest(mContext);
-        mService = spy(new UserDataCollectionJobService());
-    }
-
-    @Test
-    public void testDefaultNoArgConstructor() {
-        UserDataCollectionJobService instance = new UserDataCollectionJobService();
-        assertNotNull("default no-arg constructor is required by JobService", instance);
-    }
-
-    @Test
-    public void onStartJobTest() {
-        MockitoSession session = ExtendedMockito.mockitoSession()
-                .spyStatic(UserPrivacyStatus.class)
-                .spyStatic(OnDevicePersonalizationExecutors.class)
-                .strictness(Strictness.LENIENT).startMocking();
-        try {
-            doNothing().when(mService).jobFinished(any(), anyBoolean());
-            doReturn(mContext.getPackageManager()).when(mService).getPackageManager();
-            ExtendedMockito.doReturn(MoreExecutors.newDirectExecutorService()).when(
-                    OnDevicePersonalizationExecutors::getBackgroundExecutor);
-            ExtendedMockito.doReturn(MoreExecutors.newDirectExecutorService()).when(
-                    OnDevicePersonalizationExecutors::getLightweightExecutor);
-            ExtendedMockito.doReturn(mUserPrivacyStatus).when(UserPrivacyStatus::getInstance);
-            ExtendedMockito.doReturn(true).when(mUserPrivacyStatus).isProtectedAudienceEnabled();
-            ExtendedMockito.doReturn(true).when(mUserPrivacyStatus).isMeasurementEnabled();
-
-            boolean result = mService.onStartJob(mock(JobParameters.class));
-            assertTrue(result);
-            verify(mService, times(1)).jobFinished(any(), eq(false));
-        } finally {
-            session.finishMocking();
-        }
-    }
-
-    @Test
-    public void onStartJobTestKillSwitchEnabled() {
-        PhFlagsTestUtil.enableGlobalKillSwitch();
-        MockitoSession session = ExtendedMockito.mockitoSession().startMocking();
-        try {
-            doReturn(mJobScheduler).when(mService).getSystemService(JobScheduler.class);
-            mService.schedule(mContext);
-            assertTrue(mJobScheduler.getPendingJob(
-                    OnDevicePersonalizationConfig.USER_DATA_COLLECTION_ID)
-                            != null);
-            doNothing().when(mService).jobFinished(any(), anyBoolean());
-            boolean result = mService.onStartJob(mock(JobParameters.class));
-            assertTrue(result);
-            verify(mService, times(1)).jobFinished(any(), eq(false));
-            assertTrue(mJobScheduler.getPendingJob(
-                    OnDevicePersonalizationConfig.USER_DATA_COLLECTION_ID)
-                            == null);
-        } finally {
-            session.finishMocking();
-        }
-    }
-
-    @Test
-    public void onStartJobTestUserControlRevoked() {
-        mUserDataCollector.updateUserData(RawUserData.getInstance());
-        assertTrue(mUserDataCollector.isInitialized());
-        MockitoSession session = ExtendedMockito.mockitoSession()
-                .spyStatic(UserPrivacyStatus.class)
-                .strictness(Strictness.LENIENT).startMocking();
-        try {
-            doNothing().when(mService).jobFinished(any(), anyBoolean());
-            ExtendedMockito.doReturn(mUserPrivacyStatus).when(UserPrivacyStatus::getInstance);
-            ExtendedMockito.doReturn(false)
-                    .when(mUserPrivacyStatus).isMeasurementEnabled();
-            ExtendedMockito.doReturn(false)
-                    .when(mUserPrivacyStatus).isProtectedAudienceEnabled();
-            boolean result = mService.onStartJob(mock(JobParameters.class));
-            assertTrue(result);
-            verify(mService, times(1)).jobFinished(any(), eq(false));
-            assertFalse(mUserDataCollector.isInitialized());
-        } finally {
-            session.finishMocking();
-        }
-    }
-
-    @Test
-    public void onStopJobTest() {
-        MockitoSession session = ExtendedMockito.mockitoSession().strictness(
-                Strictness.LENIENT).startMocking();
-        try {
-            assertTrue(mService.onStopJob(mock(JobParameters.class)));
-        } finally {
-            session.finishMocking();
-        }
+        mService = spy(new UserDataCollectionJobService(new TestInjector()));
+        doNothing().when(mService).jobFinished(any(), anyBoolean());
+        when(mMockFlags.getGlobalKillSwitch()).thenReturn(false);
     }
 
     @After
@@ -159,4 +84,73 @@
         mUserDataCollector.clearUserData(RawUserData.getInstance());
         mUserDataCollector.clearMetadata();
     }
+
+    @Test
+    public void testDefaultNoArgConstructor() {
+        UserDataCollectionJobService instance =
+                new UserDataCollectionJobService(new TestInjector());
+        assertNotNull("default no-arg constructor is required by JobService", instance);
+    }
+
+    @Test
+    public void onStartJobTest() throws Exception {
+        doReturn(mContext.getPackageManager()).when(mService).getPackageManager();
+        ExtendedMockito.doReturn(mUserPrivacyStatus).when(UserPrivacyStatus::getInstance);
+        ExtendedMockito.doReturn(false).when(mUserPrivacyStatus)
+                .isProtectedAudienceAndMeasurementBothDisabled();
+
+        boolean result = mService.onStartJob(mock(JobParameters.class));
+        assertTrue(result);
+        Thread.sleep(2000);
+        verify(mService, times(1)).jobFinished(any(), eq(false));
+    }
+
+    @Test
+    public void onStartJobTestKillSwitchEnabled() {
+        when(mMockFlags.getGlobalKillSwitch()).thenReturn(true);
+        doReturn(mJobScheduler).when(mService).getSystemService(JobScheduler.class);
+        mService.schedule(mContext);
+        assertNotNull(
+                mJobScheduler.getPendingJob(OnDevicePersonalizationConfig.USER_DATA_COLLECTION_ID));
+
+        boolean result = mService.onStartJob(mock(JobParameters.class));
+
+        assertTrue(result);
+        verify(mService, times(1)).jobFinished(any(), eq(false));
+        assertNull(
+                mJobScheduler.getPendingJob(OnDevicePersonalizationConfig.USER_DATA_COLLECTION_ID));
+    }
+
+    @Test
+    public void onStartJobTestUserControlRevoked() throws Exception {
+        mUserDataCollector.updateUserData(RawUserData.getInstance());
+        assertTrue(mUserDataCollector.isInitialized());
+        ExtendedMockito.doReturn(mUserPrivacyStatus).when(UserPrivacyStatus::getInstance);
+        ExtendedMockito.doReturn(true).when(mUserPrivacyStatus)
+                .isProtectedAudienceAndMeasurementBothDisabled();
+
+        boolean result = mService.onStartJob(mock(JobParameters.class));
+
+        assertTrue(result);
+        Thread.sleep(2000);
+        verify(mService, times(1)).jobFinished(any(), eq(false));
+        assertFalse(mUserDataCollector.isInitialized());
+    }
+
+    @Test
+    public void onStopJobTest() {
+        assertTrue(mService.onStopJob(mock(JobParameters.class)));
+    }
+
+    private class TestInjector extends UserDataCollectionJobService.Injector {
+        @Override
+        ListeningExecutorService getExecutor() {
+            return MoreExecutors.newDirectExecutorService();
+        }
+
+        @Override
+        Flags getFlags() {
+            return mMockFlags;
+        }
+    }
 }
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/data/user/UserDataCollectorTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/data/user/UserDataCollectorTest.java
index 0e558a9..ee69287 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/data/user/UserDataCollectorTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/data/user/UserDataCollectorTest.java
@@ -21,8 +21,8 @@
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.when;
@@ -112,12 +112,10 @@
         assertTrue(mUserData.availableStorageBytes >= 0);
         assertTrue(mUserData.batteryPercentage >= 0);
         assertTrue(mUserData.batteryPercentage <= 100);
-        assertNotNull(mUserData.networkCapabilities);
-
         assertTrue(UserDataCollector.ALLOWED_NETWORK_TYPE.contains(mUserData.dataNetworkType));
 
         mCollector.updateUserData(mUserData);
-        assertTrue(mUserData.installedApps.size() > 0);
+        assertFalse(mUserData.installedApps.isEmpty());
     }
 
     @Test
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/data/user/UserPrivacyStatusTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/data/user/UserPrivacyStatusTest.java
index 9e46098..fa2491f 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/data/user/UserPrivacyStatusTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/data/user/UserPrivacyStatusTest.java
@@ -16,23 +16,42 @@
 
 package com.android.ondevicepersonalization.services.data.user;
 
+import static android.adservices.ondevicepersonalization.Constants.STATUS_CALLER_NOT_ALLOWED;
+import static android.adservices.ondevicepersonalization.Constants.STATUS_INTERNAL_ERROR;
+import static android.adservices.ondevicepersonalization.Constants.STATUS_METHOD_NOT_FOUND;
+import static android.adservices.ondevicepersonalization.Constants.STATUS_REMOTE_EXCEPTION;
+import static android.adservices.ondevicepersonalization.Constants.STATUS_TIMEOUT;
 import static android.app.job.JobScheduler.RESULT_SUCCESS;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+import static com.android.ondevicepersonalization.services.PhFlags.KEY_ENABLE_PERSONALIZATION_STATUS_OVERRIDE;
+import static com.android.ondevicepersonalization.services.PhFlags.KEY_PERSONALIZATION_STATUS_OVERRIDE_VALUE;
+import static com.android.ondevicepersonalization.services.PhFlags.KEY_USER_CONTROL_CACHE_IN_MILLIS;
+
+import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.spy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.when;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.modules.utils.testing.ExtendedMockitoRule;
+import com.android.odp.module.common.Clock;
 import com.android.ondevicepersonalization.services.Flags;
 import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.PhFlagsTestUtil;
+import com.android.ondevicepersonalization.services.StableFlags;
 import com.android.ondevicepersonalization.services.reset.ResetDataJobService;
+import com.android.ondevicepersonalization.services.util.DebugUtils;
+import com.android.ondevicepersonalization.services.util.StatsUtils;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 
 import org.junit.After;
 import org.junit.Before;
@@ -40,21 +59,53 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.Spy;
 import org.mockito.quality.Strictness;
 
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
 @RunWith(JUnit4.class)
 public final class UserPrivacyStatusTest {
     private UserPrivacyStatus mUserPrivacyStatus;
     private static final int CONTROL_RESET_STATUS_CODE = 5;
+    private static final long CACHE_TIMEOUT_MILLIS = 10000;
+    private long mClockTime = 1000L;
+    private boolean mCommonStatesWrapperCalled = false;
+    private AdServicesCommonStatesWrapper.CommonStatesResult mCommonStatesResult =
+            new AdServicesCommonStatesWrapper.CommonStatesResult(
+                    UserPrivacyStatus.CONTROL_GIVEN_STATUS_CODE,
+                    UserPrivacyStatus.CONTROL_GIVEN_STATUS_CODE);
 
-    @Spy
-    private Flags mSpyFlags = spy(FlagsFactory.getFlags());
+    private Flags mSpyFlags = new Flags() {
+        @Override public boolean getGlobalKillSwitch() {
+            return false;
+        }
+    };
+
+    private Clock mTestClock = new Clock() {
+        @Override public long elapsedRealtime() {
+            return mClockTime;
+        }
+        @Override public long currentTimeMillis() {
+            return mClockTime;
+        }
+    };
+
+    private AdServicesCommonStatesWrapper mCommonStatesWrapper =
+            new AdServicesCommonStatesWrapper() {
+                @Override public ListenableFuture<CommonStatesResult> getCommonStates() {
+                    mCommonStatesWrapperCalled = true;
+                    return Futures.immediateFuture(mCommonStatesResult);
+                }
+            };
 
     @Rule
     public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
+            .mockStatic(DebugUtils.class)
             .mockStatic(FlagsFactory.class)
+            .mockStatic(StatsUtils.class)
             .spyStatic(ResetDataJobService.class)
+            .spyStatic(StableFlags.class)
             .setStrictness(Strictness.LENIENT)
             .build();
 
@@ -62,9 +113,15 @@
     public void setup() throws Exception {
         PhFlagsTestUtil.setUpDeviceConfigPermissions();
         ExtendedMockito.doReturn(mSpyFlags).when(FlagsFactory::getFlags);
-        when(mSpyFlags.getGlobalKillSwitch()).thenReturn(false);
-        when(mSpyFlags.getPersonalizationStatusOverrideValue()).thenReturn(false);
-        mUserPrivacyStatus = UserPrivacyStatus.getInstance();
+        ExtendedMockito.doNothing().when(() -> StatsUtils.writeServiceRequestMetrics(
+                anyInt(), anyString(), any(), any(), anyInt(), anyLong()));
+        ExtendedMockito.doReturn(false).when(
+                () -> StableFlags.get(KEY_ENABLE_PERSONALIZATION_STATUS_OVERRIDE));
+        ExtendedMockito.doReturn(false).when(
+                () -> StableFlags.get(KEY_PERSONALIZATION_STATUS_OVERRIDE_VALUE));
+        ExtendedMockito.doReturn(CACHE_TIMEOUT_MILLIS).when(
+                () -> StableFlags.get(KEY_USER_CONTROL_CACHE_IN_MILLIS));
+        mUserPrivacyStatus = new UserPrivacyStatus(mCommonStatesWrapper, mTestClock);
         doReturn(RESULT_SUCCESS).when(ResetDataJobService::schedule);
     }
 
@@ -111,6 +168,82 @@
         assertFalse(mUserPrivacyStatus.isUserControlCacheValid());
     }
 
+    @Test
+    public void testFetchesFromAdServicesOnCacheTimeout() {
+        mUserPrivacyStatus.invalidateUserControlCacheForTesting();
+        assertFalse(mUserPrivacyStatus.isUserControlCacheValid());
+        var unused = mUserPrivacyStatus.isMeasurementEnabled();
+        assertTrue(mCommonStatesWrapperCalled);
+        mCommonStatesWrapperCalled = false;
+        var unused2 = mUserPrivacyStatus.isMeasurementEnabled();
+        assertFalse(mCommonStatesWrapperCalled);
+        mClockTime += 2 * CACHE_TIMEOUT_MILLIS;
+        var unused3 = mUserPrivacyStatus.isMeasurementEnabled();
+        assertTrue(mCommonStatesWrapperCalled);
+    }
+
+    @Test
+    public void testOverrideEnabledOnDeveloperModeOverrideTrue() {
+        mUserPrivacyStatus.updateUserControlCache(
+                UserPrivacyStatus.CONTROL_REVOKED_STATUS_CODE,
+                UserPrivacyStatus.CONTROL_REVOKED_STATUS_CODE);
+        ExtendedMockito.doReturn(true).when(
+                () -> StableFlags.get(KEY_ENABLE_PERSONALIZATION_STATUS_OVERRIDE));
+        ExtendedMockito.doReturn(true).when(
+                () -> StableFlags.get(KEY_PERSONALIZATION_STATUS_OVERRIDE_VALUE));
+        doReturn(true).when(() -> DebugUtils.isDeveloperModeEnabled(any()));
+
+        assertFalse(mUserPrivacyStatus.isProtectedAudienceAndMeasurementBothDisabled());
+        assertTrue(mUserPrivacyStatus.isMeasurementEnabled());
+        assertTrue(mUserPrivacyStatus.isProtectedAudienceEnabled());
+    }
+
+    @Test
+    public void testOverrideEnabledOnDeveloperModeOverrideFalse() {
+        mUserPrivacyStatus.updateUserControlCache(
+                UserPrivacyStatus.CONTROL_GIVEN_STATUS_CODE,
+                UserPrivacyStatus.CONTROL_GIVEN_STATUS_CODE);
+        ExtendedMockito.doReturn(true).when(
+                () -> StableFlags.get(KEY_ENABLE_PERSONALIZATION_STATUS_OVERRIDE));
+        ExtendedMockito.doReturn(false).when(
+                () -> StableFlags.get(KEY_PERSONALIZATION_STATUS_OVERRIDE_VALUE));
+        doReturn(true).when(() -> DebugUtils.isDeveloperModeEnabled(any()));
+
+        assertTrue(mUserPrivacyStatus.isProtectedAudienceAndMeasurementBothDisabled());
+        assertFalse(mUserPrivacyStatus.isMeasurementEnabled());
+        assertFalse(mUserPrivacyStatus.isProtectedAudienceEnabled());
+    }
+
+    @Test
+    public void testOverrideNotAllowedOnNonDeveloperMode() {
+        mUserPrivacyStatus.updateUserControlCache(
+                UserPrivacyStatus.CONTROL_REVOKED_STATUS_CODE,
+                UserPrivacyStatus.CONTROL_REVOKED_STATUS_CODE);
+        ExtendedMockito.doReturn(true).when(
+                () -> StableFlags.get(KEY_ENABLE_PERSONALIZATION_STATUS_OVERRIDE));
+        ExtendedMockito.doReturn(true).when(
+                () -> StableFlags.get(KEY_PERSONALIZATION_STATUS_OVERRIDE_VALUE));
+        doReturn(false).when(() -> DebugUtils.isDeveloperModeEnabled(any()));
+        assertTrue(mUserPrivacyStatus.isProtectedAudienceAndMeasurementBothDisabled());
+        assertFalse(mUserPrivacyStatus.isMeasurementEnabled());
+        assertFalse(mUserPrivacyStatus.isProtectedAudienceEnabled());
+    }
+
+    @Test
+    public void testGetStatusCode() {
+        assertThat(mUserPrivacyStatus.getExceptionStatus(
+                new ExecutionException("timeout testing", new TimeoutException())))
+                .isEqualTo(STATUS_TIMEOUT);
+        assertThat(mUserPrivacyStatus.getExceptionStatus(new NoSuchMethodException()))
+                .isEqualTo(STATUS_METHOD_NOT_FOUND);
+        assertThat(mUserPrivacyStatus.getExceptionStatus(new SecurityException()))
+                .isEqualTo(STATUS_CALLER_NOT_ALLOWED);
+        assertThat(mUserPrivacyStatus.getExceptionStatus(new IllegalArgumentException()))
+                .isEqualTo(STATUS_INTERNAL_ERROR);
+        assertThat(mUserPrivacyStatus.getExceptionStatus(new Exception()))
+                .isEqualTo(STATUS_REMOTE_EXCEPTION);
+    }
+
     @After
     public void tearDown() {
         mUserPrivacyStatus.resetUserControlForTesting();
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/data/vendor/OnDevicePersonalizationVendorDataDaoTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/data/vendor/OnDevicePersonalizationVendorDataDaoTest.java
index 0671667..a736a74 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/data/vendor/OnDevicePersonalizationVendorDataDaoTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/data/vendor/OnDevicePersonalizationVendorDataDaoTest.java
@@ -215,59 +215,15 @@
         OnDevicePersonalizationVendorDataDao instance2Owner1 =
                 OnDevicePersonalizationVendorDataDao.getInstance(mContext, owner1,
                         TEST_CERT_DIGEST);
-        assertEquals(instance1Owner1, instance2Owner1);
         ComponentName owner2 = new ComponentName("owner2", "cls2");
         OnDevicePersonalizationVendorDataDao instance1Owner2 =
                 OnDevicePersonalizationVendorDataDao.getInstance(mContext, owner2,
                         TEST_CERT_DIGEST);
+
+        com.google.common.truth.Truth.assertThat(instance1Owner1).isSameInstanceAs(instance2Owner1);
         assertNotEquals(instance1Owner1, instance1Owner2);
     }
 
-    @After
-    public void cleanup() {
-        OnDevicePersonalizationDbHelper dbHelper =
-                OnDevicePersonalizationDbHelper.getInstanceForTest(mContext);
-        dbHelper.getWritableDatabase().close();
-        dbHelper.getReadableDatabase().close();
-        dbHelper.close();
-
-        File vendorDir = new File(mContext.getFilesDir(), "VendorData");
-        File localDir = new File(mContext.getFilesDir(), "LocalData");
-        FileUtils.deleteDirectory(vendorDir);
-        FileUtils.deleteDirectory(localDir);
-    }
-
-    private void addTestData(long timestamp) {
-        addTestData(timestamp, mDao);
-    }
-
-    private void addTestData(long timestamp, OnDevicePersonalizationVendorDataDao dao) {
-        List<VendorData> dataList = new ArrayList<>();
-        dataList.add(new VendorData.Builder().setKey("key").setData(new byte[10]).build());
-        dataList.add(new VendorData.Builder().setKey("key2").setData(new byte[10]).build());
-        dataList.add(new VendorData.Builder().setKey("large").setData(new byte[111111]).build());
-        dataList.add(new VendorData.Builder().setKey("large2").setData(new byte[111111]).build());
-        dataList.add(new VendorData.Builder().setKey("xlarge").setData(new byte[5555555]).build());
-
-        List<String> retainedKeys = new ArrayList<>();
-        retainedKeys.add("key");
-        retainedKeys.add("key2");
-        retainedKeys.add("large");
-        retainedKeys.add("large2");
-        retainedKeys.add("xlarge");
-        assertTrue(dao.batchUpdateOrInsertVendorDataTransaction(dataList, retainedKeys,
-                timestamp));
-    }
-
-    private void addEventState(ComponentName service) {
-        EventState eventState = new EventState.Builder()
-                .setTaskIdentifier(TASK_IDENTIFIER)
-                .setService(service)
-                .setToken(new byte[]{1})
-                .build();
-        mEventsDao.updateOrInsertEventState(eventState);
-    }
-
     @Test
     public void testDeleteVendorTables() throws Exception {
         ExtendedMockito.doReturn(TEST_CERT_DIGEST)
@@ -319,4 +275,49 @@
         assertFalse(dir.exists());
         assertNull(mEventsDao.getEventState(TASK_IDENTIFIER, TEST_OWNER));
     }
+
+    @After
+    public void cleanup() {
+        OnDevicePersonalizationDbHelper dbHelper =
+                OnDevicePersonalizationDbHelper.getInstanceForTest(mContext);
+        dbHelper.getWritableDatabase().close();
+        dbHelper.getReadableDatabase().close();
+        dbHelper.close();
+
+        File vendorDir = new File(mContext.getFilesDir(), "VendorData");
+        File localDir = new File(mContext.getFilesDir(), "LocalData");
+        FileUtils.deleteDirectory(vendorDir);
+        FileUtils.deleteDirectory(localDir);
+    }
+
+    private void addTestData(long timestamp) {
+        addTestData(timestamp, mDao);
+    }
+
+    private static void addTestData(long timestamp, OnDevicePersonalizationVendorDataDao dao) {
+        List<VendorData> dataList = new ArrayList<>();
+        dataList.add(new VendorData.Builder().setKey("key").setData(new byte[10]).build());
+        dataList.add(new VendorData.Builder().setKey("key2").setData(new byte[10]).build());
+        dataList.add(new VendorData.Builder().setKey("large").setData(new byte[111111]).build());
+        dataList.add(new VendorData.Builder().setKey("large2").setData(new byte[111111]).build());
+        dataList.add(new VendorData.Builder().setKey("xlarge").setData(new byte[5555555]).build());
+
+        List<String> retainedKeys = new ArrayList<>();
+        retainedKeys.add("key");
+        retainedKeys.add("key2");
+        retainedKeys.add("large");
+        retainedKeys.add("large2");
+        retainedKeys.add("xlarge");
+        assertTrue(dao.batchUpdateOrInsertVendorDataTransaction(dataList, retainedKeys, timestamp));
+    }
+
+    private void addEventState(ComponentName service) {
+        EventState eventState =
+                new EventState.Builder()
+                        .setTaskIdentifier(TASK_IDENTIFIER)
+                        .setService(service)
+                        .setToken(new byte[] {1})
+                        .build();
+        mEventsDao.updateOrInsertEventState(eventState);
+    }
 }
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/display/OdpWebViewClientTests.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/display/OdpWebViewClientTests.java
index 52539a0..6460c9a 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/display/OdpWebViewClientTests.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/display/OdpWebViewClientTests.java
@@ -16,6 +16,8 @@
 
 package com.android.ondevicepersonalization.services.display;
 
+import static com.android.ondevicepersonalization.services.PhFlags.KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -23,10 +25,8 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 import android.adservices.ondevicepersonalization.EventOutputParcel;
 import android.adservices.ondevicepersonalization.RequestLogRecord;
@@ -53,6 +53,7 @@
 import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
 import com.android.ondevicepersonalization.services.PhFlagsTestUtil;
+import com.android.ondevicepersonalization.services.StableFlags;
 import com.android.ondevicepersonalization.services.data.OnDevicePersonalizationDbHelper;
 import com.android.ondevicepersonalization.services.data.events.EventUrlHelper;
 import com.android.ondevicepersonalization.services.data.events.EventUrlPayload;
@@ -71,7 +72,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
-import org.mockito.Spy;
 import org.mockito.quality.Strictness;
 
 import java.net.HttpURLConnection;
@@ -120,27 +120,32 @@
         );
     }
 
-    @Spy
-    private Flags mSpyFlags = spy(FlagsFactory.getFlags());
+    private Flags mSpyFlags = new Flags() {
+        int mIsolatedServiceDeadlineSeconds = 30;
+        @Override public int getIsolatedServiceDeadlineSeconds() {
+            return mIsolatedServiceDeadlineSeconds;
+        }
+    };
 
     @Rule
     public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
             .mockStatic(FlagsFactory.class)
+            .spyStatic(StableFlags.class)
             .setStrictness(Strictness.LENIENT)
             .build();
 
     @Before
     public void setup() throws Exception {
         PhFlagsTestUtil.setUpDeviceConfigPermissions();
+        ExtendedMockito.doReturn(mSpyFlags).when(FlagsFactory::getFlags);
+        ExtendedMockito.doReturn(SdkLevel.isAtLeastU() && mIsSipFeatureEnabled).when(
+                () -> StableFlags.get(KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED));
         mDbHelper = OnDevicePersonalizationDbHelper.getInstanceForTest(mContext);
         mDao = EventsDao.getInstanceForTest(mContext);
         // Insert query for FK constraint
         mDao.insertQuery(mTestQuery);
         mLatch = new CountDownLatch(1);
 
-        ExtendedMockito.doReturn(mSpyFlags).when(FlagsFactory::getFlags);
-        when(mSpyFlags.isSharedIsolatedProcessFeatureEnabled())
-                .thenReturn(SdkLevel.isAtLeastU() && mIsSipFeatureEnabled);
         ShellUtils.runShellCommand("settings put global hidden_api_policy 1");
 
         CountDownLatch latch = new CountDownLatch(1);
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDataProcessingAsyncCallableTests.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDataProcessingAsyncCallableTests.java
index b1f43ef..5edf262 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDataProcessingAsyncCallableTests.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDataProcessingAsyncCallableTests.java
@@ -16,12 +16,12 @@
 
 package com.android.ondevicepersonalization.services.download;
 
+import static com.android.ondevicepersonalization.services.PhFlags.KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
 
 import android.adservices.ondevicepersonalization.DownloadCompletedOutputParcel;
 import android.content.ComponentName;
@@ -38,6 +38,7 @@
 import com.android.ondevicepersonalization.services.Flags;
 import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.PhFlagsTestUtil;
+import com.android.ondevicepersonalization.services.StableFlags;
 import com.android.ondevicepersonalization.services.data.OnDevicePersonalizationDbHelper;
 import com.android.ondevicepersonalization.services.data.vendor.OnDevicePersonalizationVendorDataDao;
 import com.android.ondevicepersonalization.services.data.vendor.VendorData;
@@ -60,7 +61,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
-import org.mockito.Spy;
 import org.mockito.quality.Strictness;
 
 import java.util.ArrayList;
@@ -109,12 +109,17 @@
         );
     }
 
-    @Spy
-    private Flags mSpyFlags = spy(FlagsFactory.getFlags());
+    private Flags mSpyFlags = new Flags() {
+        int mIsolatedServiceDeadlineSeconds = 30;
+        @Override public int getIsolatedServiceDeadlineSeconds() {
+            return mIsolatedServiceDeadlineSeconds;
+        }
+    };
 
     @Rule
     public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
             .mockStatic(FlagsFactory.class)
+            .spyStatic(StableFlags.class)
             .setStrictness(Strictness.LENIENT)
             .build();
 
@@ -138,8 +143,8 @@
 
         PhFlagsTestUtil.setUpDeviceConfigPermissions();
         ExtendedMockito.doReturn(mSpyFlags).when(FlagsFactory::getFlags);
-        when(mSpyFlags.isSharedIsolatedProcessFeatureEnabled())
-                .thenReturn(SdkLevel.isAtLeastU() && mIsSipFeatureEnabled);
+        ExtendedMockito.doReturn(SdkLevel.isAtLeastU() && mIsSipFeatureEnabled).when(
+                () -> StableFlags.get(KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED));
         ShellUtils.runShellCommand("settings put global hidden_api_policy 1");
 
         mLatch = new CountDownLatch(1);
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDownloadProcessingJobServiceTests.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDownloadProcessingJobServiceTests.java
index ca25a4b..47edfcb 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDownloadProcessingJobServiceTests.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDownloadProcessingJobServiceTests.java
@@ -32,7 +32,6 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 import android.app.job.JobParameters;
 import android.app.job.JobScheduler;
@@ -57,7 +56,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.Spy;
 import org.mockito.quality.Strictness;
 
 import java.util.concurrent.CountDownLatch;
@@ -66,8 +64,14 @@
 public class OnDevicePersonalizationDownloadProcessingJobServiceTests {
     private final Context mContext = ApplicationProvider.getApplicationContext();
 
-    @Spy
-    private Flags mSpyFlags = spy(FlagsFactory.getFlags());
+    static class TestFlags implements Flags {
+        boolean mGlobalKillSwitch = false;
+        @Override public boolean getGlobalKillSwitch() {
+            return mGlobalKillSwitch;
+        }
+    }
+
+    private TestFlags mSpyFlags = new TestFlags();
 
     @Rule
     public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
@@ -82,7 +86,6 @@
     public void setup() throws Exception {
         PhFlagsTestUtil.setUpDeviceConfigPermissions();
         ExtendedMockito.doReturn(mSpyFlags).when(FlagsFactory::getFlags);
-        when(mSpyFlags.getGlobalKillSwitch()).thenReturn(false);
         // Use direct executor to keep all work sequential for the tests
         ListeningExecutorService executorService = MoreExecutors.newDirectExecutorService();
         MobileDataDownloadFactory.getMdd(mContext, executorService, executorService);
@@ -125,7 +128,7 @@
 
     @Test
     public void onStartJobTestKillSwitchEnabled() {
-        when(mSpyFlags.getGlobalKillSwitch()).thenReturn(true);
+        mSpyFlags.mGlobalKillSwitch = true;
         doNothing().when(mSpyService).jobFinished(any(), anyBoolean());
         boolean result = mSpyService.onStartJob(mock(JobParameters.class));
         assertTrue(result);
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/download/mdd/MddJobServiceTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/download/mdd/MddJobServiceTest.java
index b447841..fcd2201 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/download/mdd/MddJobServiceTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/download/mdd/MddJobServiceTest.java
@@ -48,9 +48,7 @@
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
 import com.android.modules.utils.testing.ExtendedMockitoRule;
 import com.android.ondevicepersonalization.services.Flags;
-import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
-import com.android.ondevicepersonalization.services.PhFlagsTestUtil;
 import com.android.ondevicepersonalization.services.data.user.UserPrivacyStatus;
 
 import com.google.common.util.concurrent.ListeningExecutorService;
@@ -61,7 +59,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
-import org.mockito.Spy;
+import org.mockito.Mock;
 import org.mockito.quality.Strictness;
 
 @RunWith(JUnit4.class)
@@ -72,27 +70,36 @@
     private MddJobService mSpyService;
     private UserPrivacyStatus mUserPrivacyStatus;
 
-    @Spy
-    private Flags mSpyFlags = spy(FlagsFactory.getFlags());
+    @Mock
+    private Flags mMockFlags;
 
     @Rule
     public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
-            .mockStatic(FlagsFactory.class)
             .spyStatic(UserPrivacyStatus.class)
             .spyStatic(OnDevicePersonalizationExecutors.class)
             .setStrictness(Strictness.LENIENT)
             .build();
 
+    private class TestInjector extends MddJobService.Injector {
+        @Override
+        ListeningExecutorService getBackgroundExecutor() {
+            return MoreExecutors.newDirectExecutorService();
+        }
+
+        @Override
+        Flags getFlags() {
+            return mMockFlags;
+        }
+    }
+
     @Before
     public void setup() throws Exception {
-        PhFlagsTestUtil.setUpDeviceConfigPermissions();
-        ExtendedMockito.doReturn(mSpyFlags).when(FlagsFactory::getFlags);
-        when(mSpyFlags.getGlobalKillSwitch()).thenReturn(false);
+        when(mMockFlags.getGlobalKillSwitch()).thenReturn(false);
         mUserPrivacyStatus = spy(UserPrivacyStatus.getInstance());
         ListeningExecutorService executorService = MoreExecutors.newDirectExecutorService();
         MobileDataDownloadFactory.getMdd(mContext, executorService, executorService);
 
-        mSpyService = spy(new MddJobService());
+        mSpyService = spy(new MddJobService(new TestInjector()));
         mMockJobScheduler = mock(JobScheduler.class);
         doNothing().when(mSpyService).jobFinished(any(), anyBoolean());
         doReturn(mMockJobScheduler).when(mSpyService).getSystemService(JobScheduler.class);
@@ -108,14 +115,12 @@
     }
 
     @Test
-    public void onStartJobTest() {
+    public void onStartJobTest() throws Exception {
         ExtendedMockito.doReturn(MoreExecutors.newDirectExecutorService()).when(
                 OnDevicePersonalizationExecutors::getBackgroundExecutor);
-        ExtendedMockito.doReturn(MoreExecutors.newDirectExecutorService()).when(
-                OnDevicePersonalizationExecutors::getLightweightExecutor);
         ExtendedMockito.doReturn(mUserPrivacyStatus).when(UserPrivacyStatus::getInstance);
-        ExtendedMockito.doReturn(true).when(mUserPrivacyStatus).isProtectedAudienceEnabled();
-        ExtendedMockito.doReturn(true).when(mUserPrivacyStatus).isMeasurementEnabled();
+        ExtendedMockito.doReturn(false).when(mUserPrivacyStatus)
+                .isProtectedAudienceAndMeasurementBothDisabled();
 
         JobParameters jobParameters = mock(JobParameters.class);
         PersistableBundle extras = new PersistableBundle();
@@ -124,13 +129,14 @@
 
         boolean result = mSpyService.onStartJob(jobParameters);
         assertTrue(result);
+        Thread.sleep(5000);
         verify(mSpyService, times(1)).jobFinished(any(), eq(false));
         verify(mMockJobScheduler, times(1)).schedule(any());
     }
 
     @Test
     public void onStartJobTestKillSwitchEnabled() {
-        when(mSpyFlags.getGlobalKillSwitch()).thenReturn(true);
+        when(mMockFlags.getGlobalKillSwitch()).thenReturn(true);
         JobScheduler mJobScheduler = mContext.getSystemService(JobScheduler.class);
         PersistableBundle extras = new PersistableBundle();
         extras.putString(MDD_TASK_TAG_KEY, WIFI_CHARGING_PERIODIC_TASK);
@@ -159,10 +165,10 @@
     }
 
     @Test
-    public void onStartJobTestUserControlRevoked() {
+    public void onStartJobTestUserControlRevoked() throws Exception {
         ExtendedMockito.doReturn(mUserPrivacyStatus).when(UserPrivacyStatus::getInstance);
-        ExtendedMockito.doReturn(false).when(mUserPrivacyStatus).isProtectedAudienceEnabled();
-        ExtendedMockito.doReturn(false).when(mUserPrivacyStatus).isMeasurementEnabled();
+        ExtendedMockito.doReturn(true).when(mUserPrivacyStatus)
+                .isProtectedAudienceAndMeasurementBothDisabled();
         JobScheduler mJobScheduler = mContext.getSystemService(JobScheduler.class);
         PersistableBundle extras = new PersistableBundle();
         extras.putString(MDD_TASK_TAG_KEY, WIFI_CHARGING_PERIODIC_TASK);
@@ -185,16 +191,13 @@
         doReturn(extras).when(jobParameters).getExtras();
         boolean result = mSpyService.onStartJob(jobParameters);
         assertTrue(result);
+        Thread.sleep(2000);
         verify(mSpyService, times(1)).jobFinished(any(), eq(false));
         verify(mMockJobScheduler, times(0)).schedule(any());
     }
 
     @Test
     public void onStartJobNoTaskTagTest() {
-        ExtendedMockito.doReturn(MoreExecutors.newDirectExecutorService()).when(
-                OnDevicePersonalizationExecutors::getBackgroundExecutor);
-        ExtendedMockito.doReturn(MoreExecutors.newDirectExecutorService()).when(
-                OnDevicePersonalizationExecutors::getLightweightExecutor);
 
         assertThrows(IllegalArgumentException.class,
                 () -> mSpyService.onStartJob(mock(JobParameters.class)));
@@ -203,14 +206,10 @@
     }
 
     @Test
-    public void onStartJobFailHandleTaskTest() {
-        ExtendedMockito.doReturn(MoreExecutors.newDirectExecutorService()).when(
-                OnDevicePersonalizationExecutors::getBackgroundExecutor);
-        ExtendedMockito.doReturn(MoreExecutors.newDirectExecutorService()).when(
-                OnDevicePersonalizationExecutors::getLightweightExecutor);
+    public void onStartJobFailHandleTaskTest() throws Exception {
         ExtendedMockito.doReturn(mUserPrivacyStatus).when(UserPrivacyStatus::getInstance);
-        ExtendedMockito.doReturn(true).when(mUserPrivacyStatus).isProtectedAudienceEnabled();
-        ExtendedMockito.doReturn(true).when(mUserPrivacyStatus).isMeasurementEnabled();
+        ExtendedMockito.doReturn(false).when(mUserPrivacyStatus)
+                .isProtectedAudienceAndMeasurementBothDisabled();
 
         JobParameters jobParameters = mock(JobParameters.class);
         PersistableBundle extras = new PersistableBundle();
@@ -219,6 +218,7 @@
 
         boolean result = mSpyService.onStartJob(jobParameters);
         assertTrue(result);
+        Thread.sleep(2000);
         verify(mSpyService, times(1)).jobFinished(any(), eq(false));
         verify(mMockJobScheduler, times(0)).schedule(any());
     }
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/federatedcompute/FederatedComputeServiceImplTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/federatedcompute/FederatedComputeServiceImplTest.java
index 4f46648..682892c 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/federatedcompute/FederatedComputeServiceImplTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/federatedcompute/FederatedComputeServiceImplTest.java
@@ -35,7 +35,6 @@
 import android.federatedcompute.common.TrainingInterval;
 import android.federatedcompute.common.TrainingOptions;
 import android.os.OutcomeReceiver;
-import android.provider.DeviceConfig;
 
 import androidx.test.core.app.ApplicationProvider;
 
@@ -70,12 +69,24 @@
 @RunWith(JUnit4.class)
 public class FederatedComputeServiceImplTest {
     private static final String FC_SERVER_URL = "https://google.com";
+    private static final String TEST_POPULATION_NAME = "population";
+    private static final TrainingInterval TEST_INTERVAL =
+            new TrainingInterval.Builder()
+                    .setMinimumIntervalMillis(100)
+                    .setSchedulingMode(1)
+                    .build();
+    private static final TrainingOptions TEST_OPTIONS =
+            new TrainingOptions.Builder()
+                    .setPopulationName(TEST_POPULATION_NAME)
+                    .setTrainingInterval(TEST_INTERVAL)
+                    .build();
+
     private static final String SERVICE_CLASS = "com.test.TestPersonalizationService";
     private final Context mApplicationContext = ApplicationProvider.getApplicationContext();
     ArgumentCaptor<OutcomeReceiver<Object, Exception>> mCallbackCapture;
     ArgumentCaptor<ScheduleFederatedComputeRequest> mRequestCapture;
-    private TestInjector mInjector = new TestInjector();
-    private CountDownLatch mLatch = new CountDownLatch(1);
+    private final TestInjector mInjector = new TestInjector();
+    private final CountDownLatch mLatch = new CountDownLatch(1);
     private int mErrorCode = 0;
     private boolean mOnSuccessCalled = false;
     private boolean mOnErrorCalled = false;
@@ -96,7 +107,7 @@
     @Before
     public void setup() throws Exception {
         mIsolatedService = new ComponentName(mApplicationContext.getPackageName(), SERVICE_CLASS);
-        mInjector = new TestInjector();
+
         mMockManager = Mockito.mock(FederatedComputeManager.class);
         mCallbackCapture = ArgumentCaptor.forClass(OutcomeReceiver.class);
         mRequestCapture = ArgumentCaptor.forClass(ScheduleFederatedComputeRequest.class);
@@ -123,22 +134,13 @@
 
     @Test
     public void testSchedule() throws Exception {
-        TrainingInterval interval =
-                new TrainingInterval.Builder()
-                        .setMinimumIntervalMillis(100)
-                        .setSchedulingMode(1)
-                        .build();
-        TrainingOptions options =
-                new TrainingOptions.Builder()
-                        .setPopulationName("population")
-                        .setTrainingInterval(interval)
-                        .build();
-        mServiceProxy.schedule(options, new TestCallback());
+        mServiceProxy.schedule(TEST_OPTIONS, new TestCallback());
         mCallbackCapture.getValue().onResult(null);
         var request = mRequestCapture.getValue();
         mLatch.await(1000, TimeUnit.MILLISECONDS);
+
         assertEquals(FC_SERVER_URL, request.getTrainingOptions().getServerAddress());
-        assertEquals("population", request.getTrainingOptions().getPopulationName());
+        assertEquals(TEST_POPULATION_NAME, request.getTrainingOptions().getPopulationName());
         assertTrue(mOnSuccessCalled);
     }
 
@@ -147,18 +149,10 @@
         ExtendedMockito.doReturn(mUserPrivacyStatus).when(UserPrivacyStatus::getInstance);
         ExtendedMockito.doReturn(false)
                 .when(mUserPrivacyStatus).isMeasurementEnabled();
-        TrainingInterval interval =
-                new TrainingInterval.Builder()
-                        .setMinimumIntervalMillis(100)
-                        .setSchedulingMode(1)
-                        .build();
-        TrainingOptions options =
-                new TrainingOptions.Builder()
-                        .setPopulationName("population")
-                        .setTrainingInterval(interval)
-                        .build();
-        mServiceProxy.schedule(options, new TestCallback());
+
+        mServiceProxy.schedule(TEST_OPTIONS, new TestCallback());
         mLatch.await(1000, TimeUnit.MILLISECONDS);
+
         assertFalse(mOnSuccessCalled);
     }
 
@@ -170,70 +164,23 @@
         String overrideUrl = "https://android.com";
         ShellUtils.runShellCommand(
                 "setprop debug.ondevicepersonalization.override_fc_server_url " + overrideUrl);
-        TrainingInterval interval =
-                new TrainingInterval.Builder()
-                        .setMinimumIntervalMillis(100)
-                        .setSchedulingMode(1)
-                        .build();
-        TrainingOptions options =
-                new TrainingOptions.Builder()
-                        .setPopulationName("population")
-                        .setTrainingInterval(interval)
-                        .build();
-        mServiceProxy.schedule(options, new TestCallback());
-        mCallbackCapture.getValue().onResult(null);
-        var request = mRequestCapture.getValue();
-        mLatch.await(1000, TimeUnit.MILLISECONDS);
-        assertEquals(overrideUrl, request.getTrainingOptions().getServerAddress());
-        assertEquals("population", request.getTrainingOptions().getPopulationName());
-        assertTrue(mOnSuccessCalled);
-    }
 
-    @Test
-    public void testScheduleUrlDeviceConfigOverride() throws Exception {
-        ShellUtils.runShellCommand(
-                "setprop debug.ondevicepersonalization.override_fc_server_url_package "
-                        + mApplicationContext.getPackageName());
-        String overrideUrl = "https://cs.android.com";
-        DeviceConfig.setProperty(
-                DeviceConfig.NAMESPACE_ON_DEVICE_PERSONALIZATION,
-                "debug.ondevicepersonalization.override_fc_server_url",
-                overrideUrl,
-                /* makeDefault= */ false);
-        TrainingInterval interval =
-                new TrainingInterval.Builder()
-                        .setMinimumIntervalMillis(100)
-                        .setSchedulingMode(1)
-                        .build();
-        TrainingOptions options =
-                new TrainingOptions.Builder()
-                        .setPopulationName("population")
-                        .setTrainingInterval(interval)
-                        .build();
-        mServiceProxy.schedule(options, new TestCallback());
+        mServiceProxy.schedule(TEST_OPTIONS, new TestCallback());
         mCallbackCapture.getValue().onResult(null);
         var request = mRequestCapture.getValue();
         mLatch.await(1000, TimeUnit.MILLISECONDS);
+
         assertEquals(overrideUrl, request.getTrainingOptions().getServerAddress());
-        assertEquals("population", request.getTrainingOptions().getPopulationName());
+        assertEquals(TEST_POPULATION_NAME, request.getTrainingOptions().getPopulationName());
         assertTrue(mOnSuccessCalled);
     }
 
     @Test
     public void testScheduleErr() throws Exception {
-        TrainingInterval interval =
-                new TrainingInterval.Builder()
-                        .setMinimumIntervalMillis(100)
-                        .setSchedulingMode(1)
-                        .build();
-        TrainingOptions options =
-                new TrainingOptions.Builder()
-                        .setPopulationName("population")
-                        .setTrainingInterval(interval)
-                        .build();
-        mServiceProxy.schedule(options, new TestCallback());
+        mServiceProxy.schedule(TEST_OPTIONS, new TestCallback());
         mCallbackCapture.getValue().onError(new Exception());
         mLatch.await(1000, TimeUnit.MILLISECONDS);
+
         assertTrue(mOnErrorCalled);
         assertEquals(ClientConstants.STATUS_INTERNAL_ERROR, mErrorCode);
     }
@@ -244,19 +191,22 @@
                 .updateOrInsertEventState(
                         new EventState.Builder()
                                 .setService(mIsolatedService)
-                                .setTaskIdentifier("population")
+                                .setTaskIdentifier(TEST_POPULATION_NAME)
                                 .setToken(new byte[] {})
                                 .build());
-        mServiceProxy.cancel("population", new TestCallback());
+
+        mServiceProxy.cancel(TEST_POPULATION_NAME, new TestCallback());
         mCallbackCapture.getValue().onResult(null);
         mLatch.await(1000, TimeUnit.MILLISECONDS);
+
         assertTrue(mOnSuccessCalled);
     }
 
     @Test
     public void testCancelNoPopulation() throws Exception {
-        mServiceProxy.cancel("population", new TestCallback());
+        mServiceProxy.cancel(TEST_POPULATION_NAME, new TestCallback());
         mLatch.await(1000, TimeUnit.MILLISECONDS);
+
         verify(mMockManager, times(0)).cancel(any(), any(), any(), any());
         assertTrue(mOnSuccessCalled);
     }
@@ -267,12 +217,14 @@
                 .updateOrInsertEventState(
                         new EventState.Builder()
                                 .setService(mIsolatedService)
-                                .setTaskIdentifier("population")
+                                .setTaskIdentifier(TEST_POPULATION_NAME)
                                 .setToken(new byte[] {})
                                 .build());
-        mServiceProxy.cancel("population", new TestCallback());
+
+        mServiceProxy.cancel(TEST_POPULATION_NAME, new TestCallback());
         mCallbackCapture.getValue().onError(new Exception());
         mLatch.await(1000, TimeUnit.MILLISECONDS);
+
         assertTrue(mOnErrorCalled);
         assertEquals(ClientConstants.STATUS_INTERNAL_ERROR, mErrorCode);
     }
@@ -283,6 +235,7 @@
                 "setprop debug.ondevicepersonalization.override_fc_server_url_package \"\"");
         ShellUtils.runShellCommand(
                 "setprop debug.ondevicepersonalization.override_fc_server_url \"\"");
+
         OnDevicePersonalizationDbHelper dbHelper =
                 OnDevicePersonalizationDbHelper.getInstanceForTest(mApplicationContext);
         dbHelper.getWritableDatabase().close();
@@ -290,7 +243,7 @@
         dbHelper.close();
     }
 
-    class TestCallback extends IFederatedComputeCallback.Stub {
+    private class TestCallback extends IFederatedComputeCallback.Stub {
         @Override
         public void onSuccess() {
             mOnSuccessCalled = true;
@@ -305,7 +258,7 @@
         }
     }
 
-    class TestInjector extends FederatedComputeServiceImpl.Injector {
+    private class TestInjector extends FederatedComputeServiceImpl.Injector {
 
         ListeningExecutorService getExecutor() {
             return MoreExecutors.newDirectExecutorService();
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/federatedcompute/OdpExampleStoreServiceTests.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/federatedcompute/OdpExampleStoreServiceTests.java
index 5bdcd2e..086d560 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/federatedcompute/OdpExampleStoreServiceTests.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/federatedcompute/OdpExampleStoreServiceTests.java
@@ -25,6 +25,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.when;
@@ -54,10 +55,10 @@
 import com.android.ondevicepersonalization.services.data.events.EventState;
 import com.android.ondevicepersonalization.services.data.events.EventsDao;
 import com.android.ondevicepersonalization.services.data.user.UserPrivacyStatus;
+import com.android.ondevicepersonalization.testing.utils.DeviceSupportHelper;
 
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -108,6 +109,7 @@
 
     @Before
     public void setUp() throws Exception {
+        assumeTrue(DeviceSupportHelper.isDeviceSupported());
         initMocks(this);
         when(mMockContext.getApplicationContext()).thenReturn(mContext);
         ExtendedMockito.doReturn(mUserPrivacyStatus).when(UserPrivacyStatus::getInstance);
@@ -191,7 +193,6 @@
     }
 
     @Test
-    @Ignore("TODO: b/342475912 - temporary disable failing tests.")
     public void testWithStartQuery() throws Exception {
         mEventsDao.updateOrInsertEventState(
                 new EventState.Builder()
@@ -335,6 +336,40 @@
         assertFalse(mQueryCallbackOnFailureCalled);
     }
 
+    @Test
+    public void testStartQuery_isolatedServiceThrowsException() throws Exception {
+        mEventsDao.updateOrInsertEventState(
+                new EventState.Builder()
+                        .setTaskIdentifier("throw_exception")
+                        .setService(mIsolatedService)
+                        .setToken()
+                        .build());
+        mService.onCreate();
+        Intent intent = new Intent();
+        intent.setAction(EXAMPLE_STORE_ACTION).setPackage(mContext.getPackageName());
+        IExampleStoreService binder =
+                IExampleStoreService.Stub.asInterface(mService.onBind(intent));
+        assertNotNull(binder);
+        TestQueryCallback callback = new TestQueryCallback();
+        Bundle input = new Bundle();
+        ContextData contextData =
+                new ContextData(mIsolatedService.getPackageName(), mIsolatedService.getClassName());
+        input.putByteArray(
+                ClientConstants.EXTRA_CONTEXT_DATA, ContextData.toByteArray(contextData));
+        input.putString(ClientConstants.EXTRA_POPULATION_NAME, "throw_exception");
+        input.putString(ClientConstants.EXTRA_TASK_ID, "TaskName");
+        input.putString(ClientConstants.EXTRA_COLLECTION_URI, "CollectionUri");
+        input.putInt(ClientConstants.EXTRA_ELIGIBILITY_MIN_EXAMPLE, 4);
+
+        binder.startQuery(input, callback);
+        assertTrue(
+                "timeout reached while waiting for countdownlatch!",
+                mLatch.await(5000, TimeUnit.MILLISECONDS));
+
+        assertFalse(mQueryCallbackOnSuccessCalled);
+        assertTrue(mQueryCallbackOnFailureCalled);
+    }
+
     public class TestIteratorCallback implements IExampleStoreIteratorCallback {
         byte[] mExpectedExample;
         byte[] mExpectedResumptionToken;
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/inference/IsolatedModelServiceImplTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/inference/IsolatedModelServiceImplTest.java
index 377c090..9868331 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/inference/IsolatedModelServiceImplTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/inference/IsolatedModelServiceImplTest.java
@@ -30,6 +30,7 @@
 import android.adservices.ondevicepersonalization.InferenceOutput;
 import android.adservices.ondevicepersonalization.InferenceOutputParcel;
 import android.adservices.ondevicepersonalization.ModelId;
+import android.adservices.ondevicepersonalization.OnDevicePersonalizationException;
 import android.adservices.ondevicepersonalization.RemoteDataImpl;
 import android.adservices.ondevicepersonalization.aidl.IDataAccessService;
 import android.adservices.ondevicepersonalization.aidl.IDataAccessServiceCallback;
@@ -53,6 +54,7 @@
 import java.util.Map;
 import java.util.Random;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 public class IsolatedModelServiceImplTest {
     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
@@ -89,11 +91,8 @@
         IsolatedModelServiceImpl modelService = new IsolatedModelServiceImpl();
         var callback = new TestServiceCallback();
         modelService.runInference(bundle, callback);
-        callback.mLatch.await();
 
-        assertFalse(callback.mError);
-        InferenceOutputParcel result =
-                mCallbackResult.getParcelable(Constants.EXTRA_RESULT, InferenceOutputParcel.class);
+        InferenceOutputParcel result = verifyAndGetCallbackResult(callback);
         Map<Integer, Object> outputs = result.getData();
         float[] output1 = (float[]) outputs.get(0);
         assertThat(output1.length).isEqualTo(1);
@@ -119,11 +118,8 @@
         IsolatedModelServiceImpl modelService = new IsolatedModelServiceImpl();
         var callback = new TestServiceCallback();
         modelService.runInference(bundle, callback);
-        callback.mLatch.await();
 
-        assertFalse(callback.mError);
-        InferenceOutputParcel result =
-                mCallbackResult.getParcelable(Constants.EXTRA_RESULT, InferenceOutputParcel.class);
+        InferenceOutputParcel result = verifyAndGetCallbackResult(callback);
         Map<Integer, Object> outputs = result.getData();
         float[] output1 = (float[]) outputs.get(0);
         assertThat(output1.length).isEqualTo(numExample);
@@ -148,11 +144,8 @@
         IsolatedModelServiceImpl modelService = new IsolatedModelServiceImpl();
         var callback = new TestServiceCallback();
         modelService.runInference(bundle, callback);
-        callback.mLatch.await();
 
-        assertFalse(callback.mError);
-        InferenceOutputParcel result =
-                mCallbackResult.getParcelable(Constants.EXTRA_RESULT, InferenceOutputParcel.class);
+        InferenceOutputParcel result = verifyAndGetCallbackResult(callback);
         Map<Integer, Object> outputs = result.getData();
         float[] output1 = (float[]) outputs.get(0);
         assertThat(output1.length).isEqualTo(numExample);
@@ -176,11 +169,8 @@
         IsolatedModelServiceImpl modelService = new IsolatedModelServiceImpl();
         var callback = new TestServiceCallback();
         modelService.runInference(bundle, callback);
-        callback.mLatch.await();
 
-        assertFalse(callback.mError);
-        InferenceOutputParcel result =
-                mCallbackResult.getParcelable(Constants.EXTRA_RESULT, InferenceOutputParcel.class);
+        InferenceOutputParcel result = verifyAndGetCallbackResult(callback);
         Map<Integer, Object> outputs = result.getData();
         float[] output1 = (float[]) outputs.get(0);
         assertThat(output1.length).isEqualTo(numExample);
@@ -205,10 +195,8 @@
         IsolatedModelServiceImpl modelService = new IsolatedModelServiceImpl();
         var callback = new TestServiceCallback();
         modelService.runInference(bundle, callback);
-        callback.mLatch.await();
 
-        assertTrue(callback.mError);
-        assertThat(callback.mErrorCode).isEqualTo(Constants.STATUS_INTERNAL_ERROR);
+        verifyCallBackError(callback, OnDevicePersonalizationException.ERROR_INFERENCE_FAILED);
     }
 
     @Test
@@ -262,9 +250,30 @@
         var callback = new TestServiceCallback();
         modelService.runInference(bundle, callback);
 
-        callback.mLatch.await();
-        assertTrue(callback.mError);
-        assertThat(callback.mErrorCode).isEqualTo(Constants.STATUS_INTERNAL_ERROR);
+        verifyCallBackError(callback, OnDevicePersonalizationException.ERROR_INFERENCE_FAILED);
+    }
+
+    @Test
+    public void runModelInference_modelNotExist() throws Exception {
+        InferenceInput.Params params =
+                new InferenceInput.Params.Builder(mRemoteData, "nonexist").build();
+        InferenceInput inferenceInput =
+                // Not set output structure in InferenceOutput.
+                new InferenceInput.Builder(
+                                params, generateInferenceInput(1), generateInferenceOutput(1))
+                        .build();
+
+        Bundle bundle = new Bundle();
+        bundle.putBinder(Constants.EXTRA_DATA_ACCESS_SERVICE_BINDER, new TestDataAccessService());
+        bundle.putParcelable(
+                Constants.EXTRA_INFERENCE_INPUT, new InferenceInputParcel(inferenceInput));
+
+        IsolatedModelServiceImpl modelService = new IsolatedModelServiceImpl();
+        var callback = new TestServiceCallback();
+        modelService.runInference(bundle, callback);
+
+        verifyCallBackError(
+                callback, OnDevicePersonalizationException.ERROR_INFERENCE_MODEL_NOT_FOUND);
     }
 
     @Test
@@ -285,6 +294,23 @@
                 () -> modelService.runInference(bundle, new TestServiceCallback()));
     }
 
+    private void verifyCallBackError(TestServiceCallback callback, int errorCode) throws Exception {
+        assertTrue(
+                "Timeout when run ModelService.runInference complete",
+                callback.mLatch.await(5000, TimeUnit.SECONDS));
+        assertTrue(callback.mError);
+        assertThat(callback.mErrorCode).isEqualTo(errorCode);
+    }
+
+    private InferenceOutputParcel verifyAndGetCallbackResult(TestServiceCallback callback)
+            throws Exception {
+        assertTrue(
+                "Timeout when run ModelService.runInference complete",
+                callback.mLatch.await(5000, TimeUnit.SECONDS));
+        assertFalse(callback.mError);
+        return mCallbackResult.getParcelable(Constants.EXTRA_RESULT, InferenceOutputParcel.class);
+    }
+
     private Object[] generateInferenceInput(int numExample) {
         float[][] input0 = new float[numExample][100];
         for (int i = 0; i < numExample; i++) {
@@ -329,6 +355,10 @@
                             TAG
                                     + " TestDataAccessService onRequest model id %s"
                                     + modelId.getKey());
+                    if (modelId.getKey().equals("nonexist")) {
+                        callback.onSuccess(Bundle.EMPTY);
+                        return;
+                    }
                     assertThat(modelId.getKey()).isEqualTo(MODEL_KEY);
                     assertThat(modelId.getTableId()).isEqualTo(ModelId.TABLE_ID_REMOTE_DATA);
                     Context context = ApplicationProvider.getApplicationContext();
@@ -351,6 +381,7 @@
                 }
             }
         }
+
         @Override
         public void logApiCallStats(int apiName, long latencyMillis, int responseCode) {}
     }
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/maintenance/OnDevicePersonalizationMaintenanceJobServiceTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/maintenance/OnDevicePersonalizationMaintenanceJobServiceTest.java
index 1a4865f..86717e6 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/maintenance/OnDevicePersonalizationMaintenanceJobServiceTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/maintenance/OnDevicePersonalizationMaintenanceJobServiceTest.java
@@ -33,7 +33,6 @@
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
 
 import android.app.job.JobParameters;
 import android.app.job.JobScheduler;
@@ -53,6 +52,7 @@
 import com.android.ondevicepersonalization.services.PhFlagsTestUtil;
 import com.android.ondevicepersonalization.services.data.DbUtils;
 import com.android.ondevicepersonalization.services.data.OnDevicePersonalizationDbHelper;
+import com.android.ondevicepersonalization.services.data.errors.AggregatedErrorCodesLogger;
 import com.android.ondevicepersonalization.services.data.events.Event;
 import com.android.ondevicepersonalization.services.data.events.EventState;
 import com.android.ondevicepersonalization.services.data.events.EventsDao;
@@ -64,13 +64,13 @@
 import com.android.ondevicepersonalization.services.data.vendor.VendorData;
 import com.android.ondevicepersonalization.services.sharedlibrary.spe.OdpJobScheduler;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
-import org.mockito.Spy;
 import org.mockito.quality.Strictness;
 
 import java.io.File;
@@ -94,8 +94,29 @@
     private EventsDao mEventsDao;
     private OnDevicePersonalizationMaintenanceJobService mSpyService;
 
-    @Spy
-    private Flags mSpyFlags = spy(FlagsFactory.getFlags());
+    class TestFlags implements Flags {
+        boolean mGetGlobalKillSwitch = false;
+        boolean mSpePilotJobEnabled = false;
+        boolean mGetAggregatedErrorReportingEnabled = false;
+        String mIsolatedServiceAllowList = "*";
+
+        @Override public boolean getGlobalKillSwitch() {
+            return mGetGlobalKillSwitch;
+        }
+        @Override public boolean getSpePilotJobEnabled() {
+            return mSpePilotJobEnabled;
+        }
+        @Override public String getIsolatedServiceAllowList() {
+            return mIsolatedServiceAllowList;
+        }
+
+        @Override
+        public boolean getAggregatedErrorReportingEnabled() {
+            return mGetAggregatedErrorReportingEnabled;
+        }
+    }
+
+    private TestFlags mSpyFlags = new TestFlags();
 
     @Rule
     public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
@@ -104,59 +125,10 @@
             .setStrictness(Strictness.LENIENT)
             .build();
 
-    private static void addTestData(long timestamp, OnDevicePersonalizationVendorDataDao dao) {
-        // Add vendor data
-        List<VendorData> dataList = new ArrayList<>();
-        dataList.add(new VendorData.Builder().setKey("key").setData(new byte[10]).build());
-        dataList.add(new VendorData.Builder().setKey("key2").setData(new byte[10]).build());
-        dataList.add(new VendorData.Builder().setKey("large").setData(new byte[111111]).build());
-        dataList.add(new VendorData.Builder().setKey("large2").setData(new byte[111111]).build());
-        List<String> retainedKeys = new ArrayList<>();
-        retainedKeys.add("key");
-        retainedKeys.add("key2");
-        retainedKeys.add("large");
-        retainedKeys.add("large2");
-        assertTrue(dao.batchUpdateOrInsertVendorDataTransaction(dataList, retainedKeys,
-                timestamp));
-    }
-
-    private void addEventData(ComponentName service, long timestamp) {
-        Query query = new Query.Builder(
-                timestamp,
-                "com.app",
-                service,
-                TEST_CERT_DIGEST,
-                "query".getBytes(StandardCharsets.UTF_8))
-                .build();
-        long queryId = mEventsDao.insertQuery(query);
-
-        Event event = new Event.Builder()
-                .setType(1)
-                .setEventData("event".getBytes(StandardCharsets.UTF_8))
-                .setService(service)
-                .setQueryId(queryId)
-                .setTimeMillis(timestamp)
-                .setRowIndex(0)
-                .build();
-        mEventsDao.insertEvent(event);
-
-        EventState eventState = new EventState.Builder()
-                .setTaskIdentifier(TASK_IDENTIFIER)
-                .setService(service)
-                .setToken(new byte[]{1})
-                .build();
-        mEventsDao.updateOrInsertEventState(eventState);
-    }
-
     @Before
     public void setup() throws Exception {
         PhFlagsTestUtil.setUpDeviceConfigPermissions();
         ExtendedMockito.doReturn(mSpyFlags).when(FlagsFactory::getFlags);
-        when(mSpyFlags.getGlobalKillSwitch()).thenReturn(false);
-        when(mSpyFlags.getPersonalizationStatusOverrideValue()).thenReturn(false);
-
-        // By default, disable SPE.
-        when(mSpyFlags.getSpePilotJobEnabled()).thenReturn(false);
 
         // Clean data up directories
         File vendorDir = new File(mContext.getFilesDir(), "VendorData");
@@ -200,7 +172,7 @@
 
     @Test
     public void onStartJobTestKillSwitchEnabled() {
-        when(mSpyFlags.getGlobalKillSwitch()).thenReturn(true);
+        mSpyFlags.mGetGlobalKillSwitch = true;
         doReturn(mJobScheduler).when(mSpyService).getSystemService(JobScheduler.class);
         mSpyService.schedule(mContext, /* forceSchedule= */ false);
         assertTrue(
@@ -219,7 +191,7 @@
     @MockStatic(OdpJobScheduler.class)
     public void onStartJobSpeEnabled() {
         // Enable SPE.
-        when(mSpyFlags.getSpePilotJobEnabled()).thenReturn(true);
+        mSpyFlags.mSpePilotJobEnabled = true;
         // Mock OdpJobScheduler to not actually schedule the job.
         OdpJobScheduler mockedScheduler = mock(OdpJobScheduler.class);
         doReturn(mockedScheduler).when(() -> OdpJobScheduler.getInstance(any()));
@@ -236,7 +208,28 @@
     }
 
     @Test
+    public void testVendorDataCleanup_clearAggregatedErrorData() throws Exception {
+        // when(mSpyFlags.getAggregatedErrorReportingEnabled()).thenReturn(true);
+        mSpyFlags.mGetAggregatedErrorReportingEnabled = true;
+        doReturn(MoreExecutors.newDirectExecutorService())
+                .when(OnDevicePersonalizationExecutors::getBackgroundExecutor);
+        var originalIsolatedServiceAllowList =
+                FlagsFactory.getFlags().getIsolatedServiceAllowList();
+        mSpyFlags.mIsolatedServiceAllowList = mContext.getPackageName();
+        addErrorCodeData(mService, mContext);
+
+        // Mark all the services un-enrolled
+        mSpyFlags.mIsolatedServiceAllowList = "";
+        OnDevicePersonalizationMaintenanceJobService.cleanupVendorData(mContext);
+
+        assertEquals(0, AggregatedErrorCodesLogger.getErrorDataTableCount(mContext));
+        // Reset original allow list and test cleanup successful
+        mSpyFlags.mIsolatedServiceAllowList = originalIsolatedServiceAllowList;
+    }
+
+    @Test
     public void testVendorDataCleanup() throws Exception {
+        // Add data
         long timestamp = System.currentTimeMillis();
         addTestData(timestamp, mTestDao);
         addTestData(timestamp, mDao);
@@ -244,11 +237,11 @@
         addEventData(mService, 100L);
         addEventData(TEST_OWNER, timestamp);
 
-        var originalIsolatedServiceAllowList =
-                FlagsFactory.getFlags().getIsolatedServiceAllowList();
-        when(mSpyFlags.getIsolatedServiceAllowList()).thenReturn(mContext.getPackageName());
+        // Save original allow list and enable aggregate error reporting flag
+        var originalIsolatedServiceAllowList = mSpyFlags.getIsolatedServiceAllowList();
+        mSpyFlags.mIsolatedServiceAllowList = mContext.getPackageName();
         OnDevicePersonalizationMaintenanceJobService.cleanupVendorData(mContext);
-        when(mSpyFlags.getIsolatedServiceAllowList()).thenReturn(originalIsolatedServiceAllowList);
+        mSpyFlags.mIsolatedServiceAllowList = originalIsolatedServiceAllowList;
         File dir = new File(OnDevicePersonalizationVendorDataDao.getFileDir(
                 OnDevicePersonalizationVendorDataDao.getTableName(
                         mService,
@@ -275,11 +268,11 @@
         assertEquals(6, dir.listFiles().length);
 
         originalIsolatedServiceAllowList =
-                FlagsFactory.getFlags().getIsolatedServiceAllowList();
-        when(mSpyFlags.getIsolatedServiceAllowList()).thenReturn(
-                "com.android.ondevicepersonalization.servicetests");
+                mSpyFlags.getIsolatedServiceAllowList();
+        mSpyFlags.mIsolatedServiceAllowList =
+                "com.android.ondevicepersonalization.servicetests";
         OnDevicePersonalizationMaintenanceJobService.cleanupVendorData(mContext);
-        when(mSpyFlags.getIsolatedServiceAllowList()).thenReturn(originalIsolatedServiceAllowList);
+        mSpyFlags.mIsolatedServiceAllowList = originalIsolatedServiceAllowList;
         assertEquals(2, dir.listFiles().length);
         assertTrue(new File(dir, "large_" + (timestamp + 20)).exists());
         assertTrue(new File(dir, "large2_" + (timestamp + 20)).exists());
@@ -333,11 +326,11 @@
         assertEquals(3, localDir.listFiles().length);
 
         var originalIsolatedServiceAllowList =
-                FlagsFactory.getFlags().getIsolatedServiceAllowList();
-        when(mSpyFlags.getIsolatedServiceAllowList()).thenReturn(
-                "com.android.ondevicepersonalization.servicetests");
+                mSpyFlags.getIsolatedServiceAllowList();
+        mSpyFlags.mIsolatedServiceAllowList =
+                "com.android.ondevicepersonalization.servicetests";
         OnDevicePersonalizationMaintenanceJobService.cleanupVendorData(mContext);
-        when(mSpyFlags.getIsolatedServiceAllowList()).thenReturn(originalIsolatedServiceAllowList);
+        mSpyFlags.mIsolatedServiceAllowList = originalIsolatedServiceAllowList;
         assertEquals(1, vendorDir.listFiles().length);
         assertEquals(1, localDir.listFiles().length);
     }
@@ -355,4 +348,58 @@
         FileUtils.deleteDirectory(vendorDir);
         FileUtils.deleteDirectory(localDir);
     }
+
+    private void addEventData(ComponentName service, long timestamp) {
+        Query query =
+                new Query.Builder(
+                                timestamp,
+                                "com.app",
+                                service,
+                                TEST_CERT_DIGEST,
+                                "query".getBytes(StandardCharsets.UTF_8))
+                        .build();
+        long queryId = mEventsDao.insertQuery(query);
+
+        Event event =
+                new Event.Builder()
+                        .setType(1)
+                        .setEventData("event".getBytes(StandardCharsets.UTF_8))
+                        .setService(service)
+                        .setQueryId(queryId)
+                        .setTimeMillis(timestamp)
+                        .setRowIndex(0)
+                        .build();
+        mEventsDao.insertEvent(event);
+
+        EventState eventState =
+                new EventState.Builder()
+                        .setTaskIdentifier(TASK_IDENTIFIER)
+                        .setService(service)
+                        .setToken(new byte[] {1})
+                        .build();
+        mEventsDao.updateOrInsertEventState(eventState);
+    }
+
+    private static void addTestData(long timestamp, OnDevicePersonalizationVendorDataDao dao) {
+        // Add vendor data
+        List<VendorData> dataList = new ArrayList<>();
+        dataList.add(new VendorData.Builder().setKey("key").setData(new byte[10]).build());
+        dataList.add(new VendorData.Builder().setKey("key2").setData(new byte[10]).build());
+        dataList.add(new VendorData.Builder().setKey("large").setData(new byte[111111]).build());
+        dataList.add(new VendorData.Builder().setKey("large2").setData(new byte[111111]).build());
+        List<String> retainedKeys = new ArrayList<>();
+        retainedKeys.add("key");
+        retainedKeys.add("key2");
+        retainedKeys.add("large");
+        retainedKeys.add("large2");
+        assertTrue(dao.batchUpdateOrInsertVendorDataTransaction(dataList, retainedKeys, timestamp));
+    }
+
+    private static void addErrorCodeData(ComponentName service, Context context) {
+        // Add a single error code entry
+        ListenableFuture<?> loggingFuture =
+                AggregatedErrorCodesLogger.logIsolatedServiceErrorCode(1, service, context);
+        assertTrue(loggingFuture.isDone());
+        assertEquals(1, AggregatedErrorCodesLogger.getErrorDataTableCount(context));
+    }
 }
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/process/SharedIsolatedProcessRunnerTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/process/SharedIsolatedProcessRunnerTest.java
index 7134cf1..0621f75 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/process/SharedIsolatedProcessRunnerTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/process/SharedIsolatedProcessRunnerTest.java
@@ -23,15 +23,37 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+
+import android.adservices.ondevicepersonalization.Constants;
+import android.adservices.ondevicepersonalization.IsolatedServiceException;
+import android.adservices.ondevicepersonalization.aidl.IIsolatedService;
+import android.adservices.ondevicepersonalization.aidl.IIsolatedServiceCallback;
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.test.core.app.ApplicationProvider;
 
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.federatedcompute.internal.util.AbstractServiceBinder;
 import com.android.modules.utils.testing.ExtendedMockitoRule;
 import com.android.ondevicepersonalization.services.Flags;
 import com.android.ondevicepersonalization.services.FlagsFactory;
+import com.android.ondevicepersonalization.services.OdpServiceException;
 import com.android.ondevicepersonalization.services.PhFlagsTestUtil;
+import com.android.ondevicepersonalization.services.StableFlags;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -39,6 +61,11 @@
 import org.mockito.Mock;
 import org.mockito.quality.Strictness;
 
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
 @RunWith(JUnit4.class)
 public class SharedIsolatedProcessRunnerTest {
 
@@ -46,44 +73,204 @@
             SharedIsolatedProcessRunner.getInstance();
 
     private static final String TRUSTED_APP_NAME = "trusted_app_name";
+    private static final int CALLBACK_TIMEOUT_SECONDS = 60;
     @Mock
     private Flags mFlags;
 
+    @Mock private IsolatedServiceInfo mIsolatedServiceInfo = null;
+    @Mock private AbstractServiceBinder mAbstractServiceBinder = null;
+
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+
     @Rule
     public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
             .spyStatic(FlagsFactory.class)
+            .spyStatic(StableFlags.class)
             .setStrictness(Strictness.LENIENT)
             .build();
 
+    private final SharedIsolatedProcessRunner.Injector mTestInjector =
+            new SharedIsolatedProcessRunner.Injector();
+
+    private SharedIsolatedProcessRunner mInstanceUnderTest;
+    private final CountDownLatch mCountDownLatch = new CountDownLatch(1);
+    private final FutureCallback<Object> mTestCallback =
+            new FutureCallback<Object>() {
+                @Override
+                public void onSuccess(Object result) {
+                    mCountDownLatch.countDown();
+                }
+
+                @Override
+                public void onFailure(Throwable t) {
+                    mCountDownLatch.countDown();
+                }
+            };
+
     @Before
     public void setup() throws Exception {
         PhFlagsTestUtil.setUpDeviceConfigPermissions();
 
         ExtendedMockito.doReturn(mFlags).when(FlagsFactory::getFlags);
-        doReturn(TRUSTED_APP_NAME).when(mFlags).getStableFlag(KEY_TRUSTED_PARTNER_APPS_LIST);
+        ExtendedMockito.doReturn(TRUSTED_APP_NAME).when(
+                () -> StableFlags.get(KEY_TRUSTED_PARTNER_APPS_LIST));
+
+        mInstanceUnderTest =
+                new SharedIsolatedProcessRunner(
+                        ApplicationProvider.getApplicationContext(), mTestInjector);
     }
 
     @Test
     public void testGetSipInstanceName_artImageLoadingOptimizationEnabled() {
-        doReturn(true).when(mFlags)
-                .getStableFlag(KEY_IS_ART_IMAGE_LOADING_OPTIMIZATION_ENABLED);
+        ExtendedMockito.doReturn(true).when(
+                () -> StableFlags.get(KEY_IS_ART_IMAGE_LOADING_OPTIMIZATION_ENABLED));
         assertThat(sSipRunner.getSipInstanceName(TRUSTED_APP_NAME))
                 .isEqualTo(TRUSTED_PARTNER_APPS_SIP + "_disable_art_image_");
     }
 
     @Test
     public void testGetSipInstanceName_trustedApp() {
-        doReturn(false).when(mFlags)
-                .getStableFlag(KEY_IS_ART_IMAGE_LOADING_OPTIMIZATION_ENABLED);
+        ExtendedMockito.doReturn(false).when(
+                () -> StableFlags.get(KEY_IS_ART_IMAGE_LOADING_OPTIMIZATION_ENABLED));
         assertThat(sSipRunner.getSipInstanceName(TRUSTED_APP_NAME))
                 .isEqualTo(TRUSTED_PARTNER_APPS_SIP);
     }
 
     @Test
     public void testGetSipInstanceName_unknownApp() {
-        doReturn(false).when(mFlags)
-                .getStableFlag(KEY_IS_ART_IMAGE_LOADING_OPTIMIZATION_ENABLED);
+        ExtendedMockito.doReturn(false).when(
+                () -> StableFlags.get(KEY_IS_ART_IMAGE_LOADING_OPTIMIZATION_ENABLED));
         assertThat(sSipRunner.getSipInstanceName("unknown_app_name"))
                 .isEqualTo(UNKNOWN_APPS_SIP);
     }
+
+    @Test
+    @Ignore("TODO: b/342672147 - temporary disable failing tests.")
+    public void testLoadIsolatedService_packageManagerNameNotFoundException_failedFuture()
+            throws Exception {
+        // When the package is not found during loading IsolatedService, returned future fails
+        // with appropriate OdpServiceException.
+        ListenableFuture<IsolatedServiceInfo> resultFuture =
+                mInstanceUnderTest.loadIsolatedService(
+                        "AppRequestTask",
+                        new ComponentName(mContext.getPackageName(), "nonExistService"));
+        Futures.addCallback(resultFuture, mTestCallback, mTestInjector.getExecutor());
+
+        mCountDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        assertThat(resultFuture.isDone()).isTrue();
+        ExecutionException outException = assertThrows(ExecutionException.class, resultFuture::get);
+        assertThat(outException.getCause()).isInstanceOf(OdpServiceException.class);
+        assertThat(((OdpServiceException) outException.getCause()).getErrorCode())
+                .isEqualTo(Constants.STATUS_ISOLATED_SERVICE_LOADING_FAILED);
+    }
+
+    @Test
+    public void testRunIsolatedService_serviceBinderException_failedFutureOdpServiceException()
+            throws Exception {
+        // When the getting the IsolatedServiceBinder throws an exception the returned future fails
+        // with the loading service failed error code.
+        doThrow(new RuntimeException("Unexpected exception in binder!"))
+                .when(mIsolatedServiceInfo)
+                .getIsolatedServiceBinder();
+
+        ListenableFuture<Bundle> resultFuture =
+                mInstanceUnderTest.runIsolatedService(
+                        mIsolatedServiceInfo, Constants.API_NAME_SERVICE_ON_EXECUTE, new Bundle());
+        Futures.addCallback(resultFuture, mTestCallback, mTestInjector.getExecutor());
+
+        mCountDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        assertThat(resultFuture.isDone()).isTrue();
+        ExecutionException outException = assertThrows(ExecutionException.class, resultFuture::get);
+        assertThat(outException.getCause()).isInstanceOf(OdpServiceException.class);
+        assertThat(((OdpServiceException) outException.getCause()).getErrorCode())
+                .isEqualTo(Constants.STATUS_ISOLATED_SERVICE_LOADING_FAILED);
+    }
+
+    @Test
+    public void testRunIsolatedService_serviceBinderError_failedFutureOdpServiceException()
+            throws Exception {
+        // When the service binder returns an isolatedServiceError code the returned future
+        // fails with appropriate IsolatedServiceException
+        int isolatedServiceErrorCode = 6;
+        doReturn(mAbstractServiceBinder).when(mIsolatedServiceInfo).getIsolatedServiceBinder();
+        doReturn(new TestServiceBinder(Constants.STATUS_SERVICE_FAILED, isolatedServiceErrorCode))
+                .when(mAbstractServiceBinder)
+                .getService(any());
+
+        ListenableFuture<Bundle> resultFuture =
+                mInstanceUnderTest.runIsolatedService(
+                        mIsolatedServiceInfo, Constants.API_NAME_SERVICE_ON_EXECUTE, new Bundle());
+        Futures.addCallback(resultFuture, mTestCallback, mTestInjector.getExecutor());
+
+        mCountDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        assertThat(resultFuture.isDone()).isTrue();
+        ExecutionException outException = assertThrows(ExecutionException.class, resultFuture::get);
+        assertThat(outException.getCause()).isInstanceOf(OdpServiceException.class);
+        OdpServiceException odpServiceException = (OdpServiceException) outException.getCause();
+        assertThat(odpServiceException.getErrorCode()).isEqualTo(Constants.STATUS_SERVICE_FAILED);
+        assertThat(odpServiceException.getCause()).isInstanceOf(IsolatedServiceException.class);
+        assertThat(((IsolatedServiceException) odpServiceException.getCause()).getErrorCode())
+                .isEqualTo(isolatedServiceErrorCode);
+    }
+
+    @Test
+    public void testRunIsolatedService_serviceBinderTimeout_failedFutureTimeoutException()
+            throws Exception {
+        // When the service binder times out without responding the future fails with a timeout
+        // exception.
+        doReturn(mAbstractServiceBinder).when(mIsolatedServiceInfo).getIsolatedServiceBinder();
+        doReturn(new FakeTimeoutServiceBinder()).when(mAbstractServiceBinder).getService(any());
+
+        ListenableFuture<Bundle> resultFuture =
+                mInstanceUnderTest.runIsolatedService(
+                        mIsolatedServiceInfo, Constants.API_NAME_SERVICE_ON_EXECUTE, new Bundle());
+        Futures.addCallback(resultFuture, mTestCallback, mTestInjector.getExecutor());
+        // For a GC to cause the callbackToFutureAdapter to throw FutureGarbageCollectedException
+        forceGc();
+
+        mCountDownLatch.await(CALLBACK_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        assertThat(resultFuture.isDone()).isTrue();
+        ExecutionException outException = assertThrows(ExecutionException.class, resultFuture::get);
+        assertThat(outException.getCause()).isInstanceOf(TimeoutException.class);
+    }
+
+    private static void forceGc() {
+        System.gc();
+        System.runFinalization();
+        System.gc();
+    }
+
+    private static final class TestServiceBinder extends IIsolatedService.Stub {
+        private final int mErrorCode;
+        private final int mIsolatedServiceErrorCode;
+
+        private TestServiceBinder(int errorCode, int isolatedServiceErrorCode) {
+            mErrorCode = errorCode;
+            mIsolatedServiceErrorCode = isolatedServiceErrorCode;
+        }
+
+        @Override
+        public void onRequest(
+                int operationCode,
+                @NonNull Bundle params,
+                @NonNull IIsolatedServiceCallback resultCallback) {
+            try {
+                resultCallback.onError(mErrorCode, mIsolatedServiceErrorCode, null);
+            } catch (Exception e) {
+
+            }
+        }
+    }
+
+    private static final class FakeTimeoutServiceBinder extends IIsolatedService.Stub {
+        private FakeTimeoutServiceBinder() {}
+
+        @Override
+        public void onRequest(
+                int operationCode,
+                @NonNull Bundle params,
+                @NonNull IIsolatedServiceCallback resultCallback) {
+            // Does nothing no-op
+        }
+    }
 }
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/AppRequestFlowTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/AppRequestFlowTest.java
index aa6b7ba..20463f2 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/AppRequestFlowTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/AppRequestFlowTest.java
@@ -16,18 +16,23 @@
 
 package com.android.ondevicepersonalization.services.serviceflow;
 
+import static com.android.ondevicepersonalization.services.PhFlags.KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED;
 
-import static org.junit.Assert.assertArrayEquals;
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
 import android.adservices.ondevicepersonalization.CalleeMetadata;
 import android.adservices.ondevicepersonalization.Constants;
+import android.adservices.ondevicepersonalization.ExecuteInIsolatedServiceRequest;
+import android.adservices.ondevicepersonalization.ExecuteOptionsParcel;
 import android.adservices.ondevicepersonalization.aidl.IExecuteCallback;
 import android.content.ComponentName;
 import android.content.ContentValues;
@@ -39,13 +44,14 @@
 
 import com.android.compatibility.common.util.ShellUtils;
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.modules.utils.testing.ExtendedMockitoRule;
 import com.android.odp.module.common.PackageUtils;
 import com.android.ondevicepersonalization.internal.util.ByteArrayParceledSlice;
+import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 import com.android.ondevicepersonalization.internal.util.PersistableBundleUtils;
 import com.android.ondevicepersonalization.services.Flags;
-import com.android.ondevicepersonalization.services.FlagsFactory;
-import com.android.ondevicepersonalization.services.PhFlagsTestUtil;
+import com.android.ondevicepersonalization.services.StableFlags;
 import com.android.ondevicepersonalization.services.data.DbUtils;
 import com.android.ondevicepersonalization.services.data.OnDevicePersonalizationDbHelper;
 import com.android.ondevicepersonalization.services.data.events.EventsContract;
@@ -55,61 +61,106 @@
 import com.android.ondevicepersonalization.services.data.user.UserPrivacyStatus;
 import com.android.ondevicepersonalization.services.data.vendor.OnDevicePersonalizationVendorDataDao;
 import com.android.ondevicepersonalization.services.data.vendor.VendorData;
+import com.android.ondevicepersonalization.services.request.AppRequestFlow;
+import com.android.ondevicepersonalization.services.util.NoiseUtil;
 import com.android.ondevicepersonalization.services.util.OnDevicePersonalizationFlatbufferUtils;
 
+import com.test.TestPersonalizationHandler;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
 import org.mockito.Mock;
-import org.mockito.Spy;
 import org.mockito.quality.Strictness;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
-@RunWith(JUnit4.class)
+@RunWith(Parameterized.class)
 public class AppRequestFlowTest {
+    private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
+    private static final String TAG = AppRequestFlowTest.class.getSimpleName();
 
+    private static final String TEST_SERVICE_CLASS = "com.test.TestPersonalizationService";
+    private static final int TEST_TIMEOUT_SECONDS = 10;
     private final Context mContext = spy(ApplicationProvider.getApplicationContext());
+    private final ComponentName mTestServiceComponentName =
+            new ComponentName(mContext.getPackageName(), TEST_SERVICE_CLASS);
     private final CountDownLatch mLatch = new CountDownLatch(1);
     private final OnDevicePersonalizationDbHelper mDbHelper =
             OnDevicePersonalizationDbHelper.getInstanceForTest(mContext);
 
-    private boolean mCallbackSuccess;
-    private boolean mCallbackError;
-    private int mCallbackErrorCode;
+    private volatile boolean mCallbackSuccess;
+    private volatile boolean mCallbackError;
+    private volatile int mCallbackErrorCode;
     private int mIsolatedServiceErrorCode;
-    private String mErrorMessage;
-    private Bundle mExecuteCallback;
+    private byte[] mSerializedException;
+    private volatile Bundle mExecuteCallback;
     private ServiceFlowOrchestrator mSfo;
 
     @Mock
     UserPrivacyStatus mUserPrivacyStatus;
+    @Mock private NoiseUtil mMockNoiseUtil;
+    @Parameterized.Parameter(0)
+    public boolean mIsSipFeatureEnabled;
 
-    @Spy
-    private Flags mSpyFlags = spy(FlagsFactory.getFlags());
+    @Parameterized.Parameters
+    public static Collection<Object[]> data() {
+        return Arrays.asList(
+                new Object[][] {
+                        {true}, {false}
+                }
+        );
+    }
+
+    class TestFlags implements Flags {
+        int mIsolatedServiceDeadlineSeconds = 30;
+        String mOutputDataAllowList = "*;*";
+        String mPlatformDataAllowList = "";
+
+        @Override public boolean getGlobalKillSwitch() {
+            return false;
+        }
+        @Override public int getIsolatedServiceDeadlineSeconds() {
+            return mIsolatedServiceDeadlineSeconds;
+        }
+        @Override public String getOutputDataAllowList() {
+            return mOutputDataAllowList;
+        }
+
+        @Override
+        public String getDefaultPlatformDataForExecuteAllowlist() {
+            return mPlatformDataAllowList;
+        }
+    }
+
+    private TestFlags mSpyFlags = new TestFlags();
 
     @Rule
-    public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
-            .mockStatic(FlagsFactory.class)
-            .spyStatic(UserPrivacyStatus.class)
-            .setStrictness(Strictness.LENIENT)
-            .build();
+    public final ExtendedMockitoRule mExtendedMockitoRule =
+            new ExtendedMockitoRule.Builder(this)
+                    .spyStatic(StableFlags.class)
+                    .spyStatic(UserPrivacyStatus.class)
+                    .setStrictness(Strictness.LENIENT)
+                    .build();
 
     @Before
     public void setup() throws Exception {
-        PhFlagsTestUtil.setUpDeviceConfigPermissions();
-        ExtendedMockito.doReturn(mSpyFlags).when(FlagsFactory::getFlags);
-        when(mSpyFlags.getGlobalKillSwitch()).thenReturn(false);
         ShellUtils.runShellCommand("settings put global hidden_api_policy 1");
 
         ExtendedMockito.doReturn(mUserPrivacyStatus).when(UserPrivacyStatus::getInstance);
+        ExtendedMockito.doReturn(SdkLevel.isAtLeastU() && mIsSipFeatureEnabled).when(
+                () -> StableFlags.get(KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED));
         doReturn(true).when(mUserPrivacyStatus).isMeasurementEnabled();
         doReturn(true).when(mUserPrivacyStatus).isProtectedAudienceEnabled();
+        when(mMockNoiseUtil.applyNoiseToBestValue(anyInt(), anyInt(), any())).thenReturn(3);
 
         setUpTestData();
 
@@ -124,15 +175,65 @@
     }
 
     @Test
+    public void testAppRequestFlow_InvalidService_ErrorManifestMisconfigured()
+            throws InterruptedException {
+        mSfo.scheduleForTest(
+                ServiceFlowType.APP_REQUEST_FLOW,
+                mContext.getPackageName(),
+                new ComponentName(mContext.getPackageName(), "com.test.BadService"),
+                createWrappedAppParams(),
+                new TestExecuteCallback(),
+                mContext,
+                100L,
+                110L,
+                ExecuteOptionsParcel.DEFAULT,
+                new AppTestInjector());
+
+        mLatch.await();
+
+        assertTrue(mCallbackError);
+        assertEquals(Constants.STATUS_MANIFEST_MISCONFIGURED, mCallbackErrorCode);
+    }
+
+    @Test
+    public void testAppRequestFlow_InvalidPackage_ErrorParsingFailed() throws InterruptedException {
+        mSfo.scheduleForTest(
+                ServiceFlowType.APP_REQUEST_FLOW,
+                mContext.getPackageName(),
+                new ComponentName("badPackageName", TEST_SERVICE_CLASS),
+                createWrappedAppParams(),
+                new TestExecuteCallback(),
+                mContext,
+                100L,
+                110L,
+                ExecuteOptionsParcel.DEFAULT,
+                new AppTestInjector());
+
+        mLatch.await();
+
+        assertTrue(mCallbackError);
+        assertEquals(Constants.STATUS_MANIFEST_PARSING_FAILED, mCallbackErrorCode);
+    }
+
+    @Test
     public void testAppRequestFlow_MeasurementControlRevoked() throws InterruptedException {
         int originalQueriesCount = getDbTableSize(QueriesContract.QueriesEntry.TABLE_NAME);
         int originalEventsCount = getDbTableSize(EventsContract.EventsEntry.TABLE_NAME);
         doReturn(false).when(mUserPrivacyStatus).isMeasurementEnabled();
 
-        mSfo.schedule(ServiceFlowType.APP_REQUEST_FLOW, mContext.getPackageName(),
-                new ComponentName(mContext.getPackageName(), "com.test.TestPersonalizationService"),
-                createWrappedAppParams(), new TestExecuteCallback(), mContext, 100L, 110L);
+        mSfo.scheduleForTest(
+                ServiceFlowType.APP_REQUEST_FLOW,
+                mContext.getPackageName(),
+                mTestServiceComponentName,
+                createWrappedAppParams(),
+                new TestExecuteCallback(),
+                mContext,
+                100L,
+                110L,
+                ExecuteOptionsParcel.DEFAULT,
+                new AppTestInjector());
         mLatch.await();
+
         assertTrue(mCallbackSuccess);
         assertFalse(mExecuteCallback.isEmpty());
         // make sure no request or event records are written to the database
@@ -144,48 +245,169 @@
     public void testAppRequestFlow_TargetingControlRevoked() throws InterruptedException {
         doReturn(false).when(mUserPrivacyStatus).isProtectedAudienceEnabled();
 
-        mSfo.schedule(ServiceFlowType.APP_REQUEST_FLOW, mContext.getPackageName(),
-                new ComponentName(mContext.getPackageName(), "com.test.TestPersonalizationService"),
-                createWrappedAppParams(), new TestExecuteCallback(), mContext, 100L, 110L);
+        mSfo.scheduleForTest(
+                ServiceFlowType.APP_REQUEST_FLOW,
+                mContext.getPackageName(),
+                mTestServiceComponentName,
+                createWrappedAppParams(),
+                new TestExecuteCallback(),
+                mContext,
+                100L,
+                110L,
+                ExecuteOptionsParcel.DEFAULT,
+                new AppTestInjector());
         mLatch.await();
+
         assertTrue(mCallbackSuccess);
         assertTrue(mExecuteCallback.isEmpty());
     }
 
     @Test
-    public void testAppRequestFlow_OutputDataBlocked() throws InterruptedException {
-        when(mSpyFlags.getOutputDataAllowList()).thenReturn("");
+    public void testAppRequestFlow_notInOutputDataAllowlist_blocked() throws InterruptedException {
+        mSpyFlags.mOutputDataAllowList = "";
+        ExecuteOptionsParcel options =
+                new ExecuteOptionsParcel(
+                        ExecuteInIsolatedServiceRequest.OutputSpec.OUTPUT_TYPE_BEST_VALUE, 10);
 
-        mSfo.schedule(ServiceFlowType.APP_REQUEST_FLOW, mContext.getPackageName(),
-                new ComponentName(mContext.getPackageName(), "com.test.TestPersonalizationService"),
-                createWrappedAppParams(), new TestExecuteCallback(), mContext, 100L, 110L);
+        mSfo.scheduleForTest(
+                ServiceFlowType.APP_REQUEST_FLOW,
+                mContext.getPackageName(),
+                mTestServiceComponentName,
+                createWrappedAppParams(),
+                new TestExecuteCallback(),
+                mContext,
+                100L,
+                110L,
+                options,
+                new AppTestInjector());
         mLatch.await();
 
         assertTrue(mCallbackSuccess);
-        assertNull(mExecuteCallback.getByteArray(Constants.EXTRA_OUTPUT_DATA));
+        assertThat(mExecuteCallback.getInt(Constants.EXTRA_OUTPUT_BEST_VALUE)).isEqualTo(-1);
         assertEquals(2, getDbTableSize(QueriesContract.QueriesEntry.TABLE_NAME));
         assertEquals(1, getDbTableSize(EventsContract.EventsEntry.TABLE_NAME));
     }
 
     @Test
-    public void testAppRequestFlow_OutputDataAllowed() throws InterruptedException {
+    public void testAppRequestFlow_notInPlatformDataAllowlist_blocked()
+            throws InterruptedException {
         String contextPackageName = mContext.getPackageName();
-        when(mSpyFlags.getOutputDataAllowList()).thenReturn(
-                contextPackageName + ";" + contextPackageName);
+        mSpyFlags.mOutputDataAllowList = contextPackageName + ";" + contextPackageName;
+        mSpyFlags.mPlatformDataAllowList = "";
+        ExecuteOptionsParcel options =
+                new ExecuteOptionsParcel(
+                        ExecuteInIsolatedServiceRequest.OutputSpec.OUTPUT_TYPE_BEST_VALUE, 10);
 
-        mSfo.schedule(ServiceFlowType.APP_REQUEST_FLOW, mContext.getPackageName(),
-                new ComponentName(mContext.getPackageName(), "com.test.TestPersonalizationService"),
-                createWrappedAppParams(), new TestExecuteCallback(), mContext, 100L, 110L);
+        mSfo.scheduleForTest(
+                ServiceFlowType.APP_REQUEST_FLOW,
+                mContext.getPackageName(),
+                mTestServiceComponentName,
+                createWrappedAppParams(),
+                new TestExecuteCallback(),
+                mContext,
+                100L,
+                110L,
+                options,
+                new AppTestInjector());
         mLatch.await();
 
         assertTrue(mCallbackSuccess);
-        assertArrayEquals(
-                mExecuteCallback.getByteArray(Constants.EXTRA_OUTPUT_DATA),
-                new byte[] {1, 2, 3});
+        assertThat(mExecuteCallback.getInt(Constants.EXTRA_OUTPUT_BEST_VALUE)).isEqualTo(-1);
         assertEquals(2, getDbTableSize(QueriesContract.QueriesEntry.TABLE_NAME));
         assertEquals(1, getDbTableSize(EventsContract.EventsEntry.TABLE_NAME));
     }
 
+    @Test
+    public void testAppRequestFlow_getBestValue() throws Exception {
+        String contextPackageName = mContext.getPackageName();
+        mSpyFlags.mOutputDataAllowList =
+                contextPackageName + ";" + contextPackageName;
+        mSpyFlags.mPlatformDataAllowList =
+                contextPackageName
+                        + ":"
+                        + PackageUtils.getCertDigest(mContext, mContext.getPackageName());
+        ExecuteOptionsParcel options =
+                new ExecuteOptionsParcel(
+                        ExecuteInIsolatedServiceRequest.OutputSpec.OUTPUT_TYPE_BEST_VALUE, 10);
+
+        mSfo.scheduleForTest(
+                ServiceFlowType.APP_REQUEST_FLOW,
+                mContext.getPackageName(),
+                mTestServiceComponentName,
+                createWrappedAppParams(),
+                new TestExecuteCallback(),
+                mContext,
+                100L,
+                110L,
+                options,
+                new AppTestInjector());
+        mLatch.await();
+
+        assertTrue(mCallbackSuccess);
+        assertThat(mExecuteCallback.getInt(Constants.EXTRA_OUTPUT_BEST_VALUE)).isEqualTo(3);
+        assertEquals(2, getDbTableSize(QueriesContract.QueriesEntry.TABLE_NAME));
+        assertEquals(1, getDbTableSize(EventsContract.EventsEntry.TABLE_NAME));
+    }
+
+    @Test
+    public void testAppRequestFlow_getServiceFlowFuture_timeoutExceptionReturned()
+            throws InterruptedException {
+        // When the request fails due to the test service timing out, the callback should fail
+        // with the service timeout error code.
+        String contextPackageName = mContext.getPackageName();
+        mSpyFlags.mOutputDataAllowList =
+                contextPackageName + ";" + contextPackageName;
+        mSpyFlags.mIsolatedServiceDeadlineSeconds = TEST_TIMEOUT_SECONDS;
+
+        mSfo.scheduleForTest(
+                ServiceFlowType.APP_REQUEST_FLOW,
+                mContext.getPackageName(),
+                mTestServiceComponentName,
+                createWrappedAppParams(/* timeout= */ true),
+                new TestExecuteCallback(),
+                mContext,
+                100L,
+                110L,
+                ExecuteOptionsParcel.DEFAULT,
+                new AppTestInjector());
+        boolean countedDown = mLatch.await(3 * TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        assertTrue(countedDown);
+        assertFalse(mCallbackSuccess);
+        assertTrue(mCallbackError);
+        assertEquals(Constants.STATUS_ISOLATED_SERVICE_TIMEOUT, mCallbackErrorCode);
+    }
+
+    @Test
+    public void testAppRequestFlow_getServiceFlowFuture_outputValidationExceptionReturned()
+            throws Exception {
+        // When the request fails due to output validation check failing, the callback should fail
+        // with the service failed error code. Clear vendor data to cause output
+        // validation check to fail.
+        String contextPackageName = mContext.getPackageName();
+        mSpyFlags.mOutputDataAllowList =
+                contextPackageName + ";" + contextPackageName;
+        clearVendorDataDao();
+
+        mSfo.scheduleForTest(
+                ServiceFlowType.APP_REQUEST_FLOW,
+                mContext.getPackageName(),
+                mTestServiceComponentName,
+                createWrappedAppParams(),
+                new TestExecuteCallback(),
+                mContext,
+                100L,
+                110L,
+                ExecuteOptionsParcel.DEFAULT,
+                new AppTestInjector());
+        boolean countedDown = mLatch.await(TEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+
+        assertTrue(countedDown);
+        assertFalse(mCallbackSuccess);
+        assertTrue(mCallbackError);
+        assertEquals(Constants.STATUS_SERVICE_FAILED, mCallbackErrorCode);
+    }
+
     private int getDbTableSize(String tableName) {
         return mDbHelper.getReadableDatabase().query(tableName, null,
                 null, null, null, null, null).getCount();
@@ -199,8 +421,7 @@
         ContentValues row2 = new ContentValues();
         row2.put("b", 2);
         rows.add(row2);
-        ComponentName service = new ComponentName(
-                mContext.getPackageName(), "com.test.TestPersonalizationService");
+        ComponentName service = new ComponentName(mContext.getPackageName(), TEST_SERVICE_CLASS);
         byte[] queryDataBytes = OnDevicePersonalizationFlatbufferUtils.createQueryData(
                 DbUtils.toTableValue(service), "AABBCCDD", rows);
         EventsDao.getInstanceForTest(mContext).insertQuery(
@@ -213,10 +434,10 @@
                         .build());
         EventsDao.getInstanceForTest(mContext);
 
-        OnDevicePersonalizationVendorDataDao testVendorDao = OnDevicePersonalizationVendorDataDao
-                .getInstanceForTest(mContext,
-                        new ComponentName(mContext.getPackageName(),
-                                "com.test.TestPersonalizationService"),
+        OnDevicePersonalizationVendorDataDao testVendorDao =
+                OnDevicePersonalizationVendorDataDao.getInstanceForTest(
+                        mContext,
+                        new ComponentName(mContext.getPackageName(), TEST_SERVICE_CLASS),
                         PackageUtils.getCertDigest(mContext, mContext.getPackageName()));
         VendorData vendorData = new VendorData.Builder().setData(new byte[5]).setKey(
                 "bid1").build();
@@ -227,11 +448,36 @@
         );
     }
 
-    private Bundle createWrappedAppParams() {
+    private void clearVendorDataDao() throws Exception {
+        OnDevicePersonalizationVendorDataDao testVendorDao =
+                OnDevicePersonalizationVendorDataDao.getInstanceForTest(
+                        mContext,
+                        new ComponentName(mContext.getPackageName(), TEST_SERVICE_CLASS),
+                        PackageUtils.getCertDigest(mContext, mContext.getPackageName()));
+        testVendorDao.deleteVendorData(
+                mContext,
+                new ComponentName(mContext.getPackageName(), TEST_SERVICE_CLASS),
+                PackageUtils.getCertDigest(mContext, mContext.getPackageName()));
+    }
+
+    private static Bundle createWrappedAppParams() {
+        return createWrappedAppParams(/* timeout= */ false);
+    }
+
+    /**
+     * Creates Bundle with app params for the test, including optional boolean for mimicking timeout
+     * in the {@code TestPersonalizationService}.
+     */
+    private static Bundle createWrappedAppParams(boolean timeout) {
         try {
             Bundle wrappedParams = new Bundle();
-            ByteArrayParceledSlice buffer = new ByteArrayParceledSlice(
-                    PersistableBundleUtils.toByteArray(PersistableBundle.EMPTY));
+            PersistableBundle handlerBundle = PersistableBundle.EMPTY;
+            if (timeout) {
+                handlerBundle = new PersistableBundle();
+                handlerBundle.putBoolean(TestPersonalizationHandler.TIMEOUT_KEY, timeout);
+            }
+            ByteArrayParceledSlice buffer =
+                    new ByteArrayParceledSlice(PersistableBundleUtils.toByteArray(handlerBundle));
             wrappedParams.putParcelable(Constants.EXTRA_APP_PARAMS_SERIALIZED, buffer);
             return wrappedParams;
         } catch (Exception e) {
@@ -239,21 +485,41 @@
         }
     }
 
+    class AppTestInjector extends AppRequestFlow.Injector {
+        @Override
+        public Flags getFlags() {
+            return mSpyFlags;
+        }
+
+        @Override
+        public boolean shouldValidateExecuteOutput() {
+            return true;
+        }
+
+        @Override
+        public NoiseUtil getNoiseUtil() {
+            return mMockNoiseUtil;
+        }
+    }
+
     class TestExecuteCallback extends IExecuteCallback.Stub {
         @Override
         public void onSuccess(Bundle bundle, CalleeMetadata calleeMetadata) {
+            sLogger.d(TAG + " : onSuccess callback.");
             mCallbackSuccess = true;
             mExecuteCallback = bundle;
             mLatch.countDown();
         }
 
         @Override
-        public void onError(int errorCode, int isolatedServiceErrorCode, String message,
+        public void onError(int errorCode, int isolatedServiceErrorCode,
+                byte[] serializedException,
                 CalleeMetadata calleeMetadata) {
+            sLogger.d(TAG + " : onError callback.");
             mCallbackError = true;
             mCallbackErrorCode = errorCode;
             mIsolatedServiceErrorCode = isolatedServiceErrorCode;
-            mErrorMessage = message;
+            mSerializedException = serializedException;
             mLatch.countDown();
         }
     }
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/RenderFlowTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/RenderFlowTest.java
index 02ed35f..69bb408 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/RenderFlowTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/RenderFlowTest.java
@@ -16,11 +16,11 @@
 
 package com.android.ondevicepersonalization.services.serviceflow;
 
+import static com.android.ondevicepersonalization.services.PhFlags.KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
 
 import android.adservices.ondevicepersonalization.CalleeMetadata;
 import android.adservices.ondevicepersonalization.Constants;
@@ -38,10 +38,12 @@
 
 import com.android.compatibility.common.util.ShellUtils;
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.modules.utils.testing.ExtendedMockitoRule;
 import com.android.ondevicepersonalization.services.Flags;
 import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.PhFlagsTestUtil;
+import com.android.ondevicepersonalization.services.StableFlags;
 import com.android.ondevicepersonalization.services.data.DbUtils;
 import com.android.ondevicepersonalization.services.data.OnDevicePersonalizationDbHelper;
 import com.android.ondevicepersonalization.services.data.events.EventsDao;
@@ -58,7 +60,6 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.mockito.Mock;
-import org.mockito.Spy;
 import org.mockito.quality.Strictness;
 
 import java.util.ArrayList;
@@ -79,7 +80,7 @@
     private boolean mCallbackError;
     private int mCallbackErrorCode;
     private int mIsolatedServiceErrorCode;
-    private String mErrorMessage;
+    private byte[] mSerializedException;
     private Bundle mCallbackResult;
     private ServiceFlowOrchestrator mSfo;
 
@@ -98,12 +99,17 @@
         );
     }
 
-    @Spy
-    private Flags mSpyFlags = spy(FlagsFactory.getFlags());
+    private Flags mSpyFlags = new Flags() {
+        int mIsolatedServiceDeadlineSeconds = 30;
+        @Override public int getIsolatedServiceDeadlineSeconds() {
+            return mIsolatedServiceDeadlineSeconds;
+        }
+    };
 
     @Rule
     public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
             .mockStatic(FlagsFactory.class)
+            .spyStatic(StableFlags.class)
             .spyStatic(UserPrivacyStatus.class)
             .spyStatic(CryptUtils.class)
             .setStrictness(Strictness.LENIENT)
@@ -114,7 +120,8 @@
         PhFlagsTestUtil.setUpDeviceConfigPermissions();
         ShellUtils.runShellCommand("settings put global hidden_api_policy 1");
         ExtendedMockito.doReturn(mSpyFlags).when(FlagsFactory::getFlags);
-        when(mSpyFlags.isSharedIsolatedProcessFeatureEnabled()).thenReturn(mIsSipFeatureEnabled);
+        ExtendedMockito.doReturn(SdkLevel.isAtLeastU() && mIsSipFeatureEnabled).when(
+                () -> StableFlags.get(KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED));
 
         setUpTestDate();
 
@@ -198,12 +205,13 @@
             mLatch.countDown();
         }
         @Override public void onError(
-                int errorCode, int isolatedServiceErrorCode, String message,
+                int errorCode, int isolatedServiceErrorCode,
+                byte[] serializedException,
                 CalleeMetadata calleeMetadata) {
             mCallbackError = true;
             mCallbackErrorCode = errorCode;
             mIsolatedServiceErrorCode = isolatedServiceErrorCode;
-            mErrorMessage = message;
+            mSerializedException = serializedException;
             mLatch.countDown();
         }
     }
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowFactoryTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowFactoryTest.java
index b6597dc..b8178ae 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowFactoryTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowFactoryTest.java
@@ -20,6 +20,7 @@
 
 import android.adservices.ondevicepersonalization.CalleeMetadata;
 import android.adservices.ondevicepersonalization.Constants;
+import android.adservices.ondevicepersonalization.ExecuteOptionsParcel;
 import android.adservices.ondevicepersonalization.MeasurementWebTriggerEventParamsParcel;
 import android.adservices.ondevicepersonalization.aidl.IExecuteCallback;
 import android.adservices.ondevicepersonalization.aidl.IRegisterMeasurementEventCallback;
@@ -55,10 +56,17 @@
 
     @Test
     public void testCreateAppRequestFlowInstance() throws Exception {
-        ServiceFlow serviceFlow = ServiceFlowFactory.createInstance(
-                ServiceFlowType.APP_REQUEST_FLOW, "testCallingPackage",
-                new ComponentName("testPackage", "testClass"), new Bundle(),
-                new TestExecuteCallback(), mContext, 0L, 100L);
+        ServiceFlow serviceFlow =
+                ServiceFlowFactory.createInstance(
+                        ServiceFlowType.APP_REQUEST_FLOW,
+                        "testCallingPackage",
+                        new ComponentName("testPackage", "testClass"),
+                        new Bundle(),
+                        new TestExecuteCallback(),
+                        mContext,
+                        0L,
+                        100L,
+                        ExecuteOptionsParcel.DEFAULT);
 
         assertThat(serviceFlow).isNotNull();
         assertThat(serviceFlow).isInstanceOf(AppRequestFlow.class);
@@ -66,9 +74,19 @@
 
     @Test
     public void testCreateRenderFlowInstance() throws Exception {
-        ServiceFlow serviceFlow = ServiceFlowFactory.createInstance(
-                ServiceFlowType.RENDER_FLOW, "testToken", new Binder(), 0,
-                100, 50, new TestRenderFlowCallback(), mContext, 0L, 100L);
+        ServiceFlow serviceFlow =
+                ServiceFlowFactory.createInstance(
+                        ServiceFlowType.RENDER_FLOW,
+                        "testToken",
+                        new Binder(),
+                        0,
+                        100,
+                        50,
+                        new TestRenderFlowCallback(),
+                        mContext,
+                        0L,
+                        100L,
+                        ExecuteOptionsParcel.DEFAULT);
 
         assertThat(serviceFlow).isNotNull();
         assertThat(serviceFlow).isInstanceOf(RenderFlow.class);
@@ -76,9 +94,18 @@
 
     @Test(expected = ClassCastException.class)
     public void testCreateAppRequestFlowInstance_IllegalInputClass() throws Exception {
-        ServiceFlow serviceFlow = ServiceFlowFactory.createInstance(
-                ServiceFlowType.APP_REQUEST_FLOW, "testToken", new Binder(), 0,
-                100, 50, new TestRenderFlowCallback(), mContext, 0L);
+        ServiceFlow serviceFlow =
+                ServiceFlowFactory.createInstance(
+                        ServiceFlowType.APP_REQUEST_FLOW,
+                        "testToken",
+                        new Binder(),
+                        0,
+                        100,
+                        50,
+                        new TestRenderFlowCallback(),
+                        mContext,
+                        0L,
+                        ExecuteOptionsParcel.DEFAULT);
     }
 
     @Test(expected = ArrayIndexOutOfBoundsException.class)
@@ -89,9 +116,15 @@
 
     @Test
     public void testCreateWebTriggerFlowInstance() throws Exception {
-        ServiceFlow serviceFlow = ServiceFlowFactory.createInstance(
-                ServiceFlowType.WEB_TRIGGER_FLOW, getWebTriggerParams(), mContext,
-                new TestWebCallback(), 0L, 100L);
+        ServiceFlow serviceFlow =
+                ServiceFlowFactory.createInstance(
+                        ServiceFlowType.WEB_TRIGGER_FLOW,
+                        getWebTriggerParams(),
+                        mContext,
+                        new TestWebCallback(),
+                        0L,
+                        100L,
+                        ExecuteOptionsParcel.DEFAULT);
 
         assertThat(serviceFlow).isNotNull();
         assertThat(serviceFlow).isInstanceOf(WebTriggerFlow.class);
@@ -101,15 +134,15 @@
         @Override
         public void onSuccess(Bundle bundle, CalleeMetadata calleeMetadata) {}
         @Override
-        public void onError(int errorCode, int isolatedServiceErrorCode, String message,
-                CalleeMetadata calleeMetadata) {}
+        public void onError(int errorCode, int isolatedServiceErrorCode,
+                byte[] serializedException, CalleeMetadata calleeMetadata) {}
     }
 
     class TestRenderFlowCallback extends IRequestSurfacePackageCallback.Stub {
         @Override public void onSuccess(SurfaceControlViewHost.SurfacePackage surfacePackage,
                 CalleeMetadata calleeMetadata) {}
-        @Override public void onError(int errorCode, int isolatedServiceErrorCode, String message,
-                CalleeMetadata calleeMetadata) {}
+        @Override public void onError(int errorCode, int isolatedServiceErrorCode,
+        byte[] serializedException, CalleeMetadata calleeMetadata) {}
     }
 
     class TestWebCallback extends IRegisterMeasurementEventCallback.Stub {
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowTypeTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowTypeTest.java
index 4541f6d..b5e9001 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowTypeTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/ServiceFlowTypeTest.java
@@ -25,7 +25,6 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.PhFlagsTestUtil;
 
 import org.junit.Before;
@@ -52,7 +51,7 @@
         assertThat(ServiceFlowType.RENDER_FLOW.getTaskName()).isEqualTo("Render");
         assertThat(ServiceFlowType.WEB_TRIGGER_FLOW.getTaskName()).isEqualTo("WebTrigger");
         assertThat(ServiceFlowType.WEB_VIEW_FLOW.getTaskName())
-                .isEqualTo("ComputeEventMetrics");
+                .isEqualTo("WebView");
         assertThat(ServiceFlowType.EXAMPLE_STORE_FLOW.getTaskName())
                 .isEqualTo("ExampleStore");
         assertThat(ServiceFlowType.DOWNLOAD_FLOW.getTaskName())
@@ -86,18 +85,4 @@
         assertThat(ServiceFlowType.DOWNLOAD_FLOW.getPriority())
                 .isEqualTo(ServiceFlowType.Priority.LOW);
     }
-
-    @Test
-    public void executionTimeoutTest() {
-        assertThat(ServiceFlowType.APP_REQUEST_FLOW.getExecutionTimeout())
-                .isEqualTo(FlagsFactory.getFlags().getAppRequestFlowDeadlineSeconds());
-        assertThat(ServiceFlowType.RENDER_FLOW.getExecutionTimeout())
-                .isEqualTo(FlagsFactory.getFlags().getRenderFlowDeadlineSeconds());
-        assertThat(ServiceFlowType.WEB_TRIGGER_FLOW.getExecutionTimeout())
-                .isEqualTo(FlagsFactory.getFlags().getWebTriggerFlowDeadlineSeconds());
-        assertThat(ServiceFlowType.EXAMPLE_STORE_FLOW.getExecutionTimeout())
-                .isEqualTo(FlagsFactory.getFlags().getExampleStoreFlowDeadlineSeconds());
-        assertThat(ServiceFlowType.DOWNLOAD_FLOW.getExecutionTimeout())
-                .isEqualTo(FlagsFactory.getFlags().getDownloadFlowDeadlineSeconds());
-    }
 }
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/WebTriggerFlowTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/WebTriggerFlowTest.java
index 5ff81af..408ea6b 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/WebTriggerFlowTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/WebTriggerFlowTest.java
@@ -16,11 +16,11 @@
 
 package com.android.ondevicepersonalization.services.serviceflow;
 
+import static com.android.ondevicepersonalization.services.PhFlags.KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.when;
 
 import android.adservices.ondevicepersonalization.CalleeMetadata;
 import android.adservices.ondevicepersonalization.Constants;
@@ -36,10 +36,12 @@
 
 import com.android.compatibility.common.util.ShellUtils;
 import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.modules.utils.build.SdkLevel;
 import com.android.modules.utils.testing.ExtendedMockitoRule;
 import com.android.ondevicepersonalization.services.Flags;
 import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.PhFlagsTestUtil;
+import com.android.ondevicepersonalization.services.StableFlags;
 import com.android.ondevicepersonalization.services.data.DbUtils;
 import com.android.ondevicepersonalization.services.data.OnDevicePersonalizationDbHelper;
 import com.android.ondevicepersonalization.services.data.events.EventsDao;
@@ -52,15 +54,16 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
 import org.mockito.Mock;
-import org.mockito.Spy;
 import org.mockito.quality.Strictness;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.concurrent.CountDownLatch;
 
-@RunWith(JUnit4.class)
+@RunWith(Parameterized.class)
 public class WebTriggerFlowTest {
 
     private final Context mContext = ApplicationProvider.getApplicationContext();
@@ -75,12 +78,36 @@
 
     @Mock UserPrivacyStatus mUserPrivacyStatus;
 
-    @Spy
-    private Flags mSpyFlags = spy(FlagsFactory.getFlags());
+    @Parameterized.Parameter(0)
+    public boolean mIsSipFeatureEnabled;
+
+    @Parameterized.Parameters
+    public static Collection<Object[]> data() {
+        return Arrays.asList(
+                new Object[][] {
+                        {true}, {false}
+                }
+        );
+    }
+
+    static class TestFlags implements Flags {
+        int mIsolatedServiceDeadlineSeconds = 30;
+        boolean mGlobalKillSwitch = false;
+        @Override
+        public boolean getGlobalKillSwitch() {
+            return mGlobalKillSwitch;
+        }
+        @Override public int getIsolatedServiceDeadlineSeconds() {
+            return mIsolatedServiceDeadlineSeconds;
+        }
+    };
+
+    private TestFlags mSpyFlags = new TestFlags();
 
     @Rule
     public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
             .mockStatic(FlagsFactory.class)
+            .spyStatic(StableFlags.class)
             .spyStatic(UserPrivacyStatus.class)
             .setStrictness(Strictness.LENIENT)
             .build();
@@ -90,7 +117,8 @@
         PhFlagsTestUtil.setUpDeviceConfigPermissions();
         ShellUtils.runShellCommand("settings put global hidden_api_policy 1");
         ExtendedMockito.doReturn(mSpyFlags).when(FlagsFactory::getFlags);
-        when(mSpyFlags.getGlobalKillSwitch()).thenReturn(false);
+        ExtendedMockito.doReturn(SdkLevel.isAtLeastU() && mIsSipFeatureEnabled).when(
+                () -> StableFlags.get(KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED));
 
         ExtendedMockito.doReturn(mUserPrivacyStatus).when(UserPrivacyStatus::getInstance);
         doReturn(true).when(mUserPrivacyStatus).isMeasurementEnabled();
@@ -109,7 +137,7 @@
 
     @Test
     public void testWebTriggerFlow_GlobalKillswitchOn() throws Exception {
-        when(mSpyFlags.getGlobalKillSwitch()).thenReturn(true);
+        mSpyFlags.mGlobalKillSwitch = true;
 
         mSfo.schedule(ServiceFlowType.WEB_TRIGGER_FLOW, getWebTriggerParams(), mContext,
                 new TestWebCallback(), 100L, 110L);
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/WebViewFlowTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/WebViewFlowTest.java
index 663eef2..7804bb2 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/WebViewFlowTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/serviceflow/WebViewFlowTest.java
@@ -16,6 +16,8 @@
 
 package com.android.ondevicepersonalization.services.serviceflow;
 
+import static com.android.ondevicepersonalization.services.PhFlags.KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.spy;
@@ -32,8 +34,14 @@
 import androidx.test.core.app.ApplicationProvider;
 
 import com.android.compatibility.common.util.ShellUtils;
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+import com.android.ondevicepersonalization.services.Flags;
+import com.android.ondevicepersonalization.services.FlagsFactory;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
 import com.android.ondevicepersonalization.services.PhFlagsTestUtil;
+import com.android.ondevicepersonalization.services.StableFlags;
 import com.android.ondevicepersonalization.services.data.OnDevicePersonalizationDbHelper;
 import com.android.ondevicepersonalization.services.data.events.EventUrlPayload;
 import com.android.ondevicepersonalization.services.data.events.EventsContract;
@@ -45,15 +53,18 @@
 import org.jetbrains.annotations.NotNull;
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
+import org.mockito.quality.Strictness;
 
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.concurrent.CountDownLatch;
 
-@RunWith(JUnit4.class)
+@RunWith(Parameterized.class)
 public class WebViewFlowTest {
 
     private static final String SERVICE_CLASS = "com.test.TestPersonalizationService";
@@ -66,10 +77,38 @@
     private FlowCallback mCallback;
     private static final ServiceFlowOrchestrator sSfo = ServiceFlowOrchestrator.getInstance();
 
+    @Parameterized.Parameter(0)
+    public boolean mIsSipFeatureEnabled;
+
+    @Parameterized.Parameters
+    public static Collection<Object[]> data() {
+        return Arrays.asList(
+                new Object[][] {
+                        {true}, {false}
+                }
+        );
+    }
+
+    private Flags mSpyFlags = new Flags() {
+        int mIsolatedServiceDeadlineSeconds = 30;
+        @Override public int getIsolatedServiceDeadlineSeconds() {
+            return mIsolatedServiceDeadlineSeconds;
+        }
+    };
+
+    @Rule
+    public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
+            .mockStatic(FlagsFactory.class)
+            .spyStatic(StableFlags.class)
+            .setStrictness(Strictness.LENIENT)
+            .build();
     @Before
     public void setup() throws Exception {
         PhFlagsTestUtil.setUpDeviceConfigPermissions();
         ShellUtils.runShellCommand("settings put global hidden_api_policy 1");
+        ExtendedMockito.doReturn(mSpyFlags).when(FlagsFactory::getFlags);
+        ExtendedMockito.doReturn(SdkLevel.isAtLeastU() && mIsSipFeatureEnabled).when(
+                () -> StableFlags.get(KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED));
 
         mDao = EventsDao.getInstanceForTest(mContext);
         Query mTestQuery = new Query.Builder(
@@ -122,7 +161,6 @@
     }
 
     @Test
-    @Ignore("TODO: b/342475912 - temporary disable failing tests.")
     public void testDedupMultiplePayloads() throws Exception {
         mTestEventPayload =
                 new EventUrlPayload(createEventParameters(), null, null);
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/util/AllowListUtilsTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/util/AllowListUtilsTest.java
index 21b132b..9bc75be 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/util/AllowListUtilsTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/util/AllowListUtilsTest.java
@@ -137,5 +137,40 @@
                 "com.test.app2",
                 "EFGH",
                 "com.test.app1:ABCD;com.test.app2:EFGH;com.test.app3:PQRS"));
+        // Match - Wildcard left side.
+        assertTrue(AllowListUtils.isPairAllowListed(
+                "com.test.app1",
+                "ABCD",
+                "com.test.app2",
+                "EFGH",
+                "*;com.test.app2"));
+        // Match - Wildcard right side.
+        assertTrue(AllowListUtils.isPairAllowListed(
+                "com.test.app1",
+                "ABCD",
+                "com.test.app2",
+                "EFGH",
+                "com.test.app1;*"));
+        // Match - Wildcard both sides.
+        assertTrue(AllowListUtils.isPairAllowListed(
+                "com.test.app1",
+                "ABCD",
+                "com.test.app2",
+                "EFGH",
+                "*;*"));
+        // No match - Wildcard left side, right side mismatch
+        assertFalse(AllowListUtils.isPairAllowListed(
+                "com.test.app1",
+                "ABCD",
+                "com.test.app2",
+                "EFGH",
+                "*;com.test.app3"));
+        // No Match - Left side mismatch, Wildcard right side.
+        assertFalse(AllowListUtils.isPairAllowListed(
+                "com.test.app1",
+                "ABCD",
+                "com.test.app2",
+                "EFGH",
+                "com.test.app3;*"));
     }
 }
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/util/DebugUtilsTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/util/DebugUtilsTest.java
index 028bada..5a7823f 100644
--- a/tests/servicetests/src/com/android/ondevicepersonalization/services/util/DebugUtilsTest.java
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/util/DebugUtilsTest.java
@@ -162,11 +162,13 @@
                 .thenReturn(0);
     }
 
-    class TestFlags implements Flags {
-        public boolean mIsolatedServiceDebuggingEnabled;
+    private static final class TestFlags implements Flags {
+        private final boolean mIsolatedServiceDebuggingEnabled;
+
         TestFlags(boolean value) {
             mIsolatedServiceDebuggingEnabled = value;
         }
+
         @Override public boolean isIsolatedServiceDebuggingEnabled() {
             return mIsolatedServiceDebuggingEnabled;
         }
diff --git a/tests/servicetests/src/com/android/ondevicepersonalization/services/util/NoiseUtilTest.java b/tests/servicetests/src/com/android/ondevicepersonalization/services/util/NoiseUtilTest.java
new file mode 100644
index 0000000..c5f13a0
--- /dev/null
+++ b/tests/servicetests/src/com/android/ondevicepersonalization/services/util/NoiseUtilTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 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.ondevicepersonalization.services.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.concurrent.ThreadLocalRandom;
+
+public class NoiseUtilTest {
+    @Mock ThreadLocalRandom mMockRandom;
+    private NoiseUtil mNoiseUtil;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mNoiseUtil = new NoiseUtil();
+    }
+
+    @Test
+    public void applyNoise_ToBestValue_returnActualValue() {
+        when(mMockRandom.nextDouble()).thenReturn(0.2);
+        int output = mNoiseUtil.applyNoiseToBestValue(5, 10, mMockRandom);
+        assertThat(output).isEqualTo(5);
+    }
+
+    @Test
+    public void applyNoise_ToBestValue_returnFakeValue() {
+        when(mMockRandom.nextDouble()).thenReturn(0.02);
+        when(mMockRandom.nextInt(anyInt())).thenReturn(6);
+        int output = mNoiseUtil.applyNoiseToBestValue(5, 10, mMockRandom);
+        assertThat(output).isEqualTo(6);
+    }
+
+    @Test
+    public void invalidActualValue() {
+        int output = mNoiseUtil.applyNoiseToBestValue(11, 10, mMockRandom);
+        assertThat(output).isEqualTo(-1);
+    }
+
+    @Test
+    public void invalidNegativeValue() {
+        int output = mNoiseUtil.applyNoiseToBestValue(-2, 10, mMockRandom);
+        assertThat(output).isEqualTo(-1);
+    }
+
+    @Test
+    public void applyNoise_ToBestValue_returnNotActualFakeValue() {
+        when(mMockRandom.nextDouble()).thenReturn(0.02);
+        when(mMockRandom.nextInt(anyInt())).thenReturn(5, 7);
+        int output = mNoiseUtil.applyNoiseToBestValue(5, 10, mMockRandom);
+        assertThat(output).isEqualTo(7);
+    }
+}
diff --git a/tests/servicetests/src/com/test/TestPersonalizationHandler.java b/tests/servicetests/src/com/test/TestPersonalizationHandler.java
index 0644429..60a8b5f 100644
--- a/tests/servicetests/src/com/test/TestPersonalizationHandler.java
+++ b/tests/servicetests/src/com/test/TestPersonalizationHandler.java
@@ -38,6 +38,7 @@
 import android.annotation.NonNull;
 import android.content.ContentValues;
 import android.os.OutcomeReceiver;
+import android.os.PersistableBundle;
 import android.util.Log;
 
 import java.util.ArrayList;
@@ -48,7 +49,11 @@
 
 // TODO(b/249345663) Move this class and related manifest to separate APK for more realistic testing
 public class TestPersonalizationHandler implements IsolatedWorker {
-    public final String TAG = "TestPersonalizationHandler";
+    public static final String TAG = "TestPersonalizationHandler";
+
+    /** Bundle key that mimics a timeout in {@link #onExecute}. */
+    public static final String TIMEOUT_KEY = "timeout_key";
+
     private final KeyValueStore mRemoteData;
 
     TestPersonalizationHandler(KeyValueStore remoteData) {
@@ -82,6 +87,12 @@
             @NonNull ExecuteInput input,
             @NonNull OutcomeReceiver<ExecuteOutput, IsolatedServiceException> receiver) {
         Log.d(TAG, "onExecute() started.");
+        PersistableBundle inputBundle = input.getAppParams();
+        if (inputBundle != null && inputBundle.getBoolean("timeout_key", false)) {
+            Log.d(TAG, "onExecute() skipped.");
+            return;
+        }
+        Log.d(TAG, "onExecute() continuing.");
         ContentValues logData = new ContentValues();
         logData.put("id", "bid1");
         logData.put("pr", 5.0);
diff --git a/tests/systemserviceapitests/AndroidManifest.xml b/tests/systemserviceapitests/AndroidManifest.xml
index 27f721e..144bc29 100644
--- a/tests/systemserviceapitests/AndroidManifest.xml
+++ b/tests/systemserviceapitests/AndroidManifest.xml
@@ -21,7 +21,7 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
 
-    <application android:label="OnDevicePersonalizationSystemServiceApiTests">
+    <application android:debuggable="true" android:label="OnDevicePersonalizationSystemServiceApiTests">
         <uses-library android:name="android.test.runner"/>
     </application>
     <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/systemserviceimpltests/Android.bp b/tests/systemserviceimpltests/Android.bp
index 3d382b5..916c5cf 100644
--- a/tests/systemserviceimpltests/Android.bp
+++ b/tests/systemserviceimpltests/Android.bp
@@ -41,6 +41,7 @@
     ],
     sdk_version: "module_current",
     min_sdk_version: "Tiramisu",
+    compile_multilib: "both",
     test_suites: [
         "general-tests",
         "mts-ondevicepersonalization",
diff --git a/tests/testutils/src/com/android/ondevicepersonalization/testing/utils/ResultReceiver.java b/tests/testutils/src/com/android/ondevicepersonalization/testing/utils/ResultReceiver.java
index 7010d89..aa5dafe 100644
--- a/tests/testutils/src/com/android/ondevicepersonalization/testing/utils/ResultReceiver.java
+++ b/tests/testutils/src/com/android/ondevicepersonalization/testing/utils/ResultReceiver.java
@@ -15,21 +15,44 @@
  */
 package com.android.ondevicepersonalization.testing.utils;
 
+import static org.junit.Assert.assertTrue;
+
 import android.os.OutcomeReceiver;
 
+import java.time.Duration;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
 
 /**
  * A synchronous wrapper around OutcomeReceiver for testing.
  */
 public class ResultReceiver<T> implements OutcomeReceiver<T, Exception> {
     private final CountDownLatch mLatch = new CountDownLatch(1);
+    private final Duration mDeadline;
     private T mResult = null;
     private Exception mException = null;
     private boolean mSuccess = false;
     private boolean mError = false;
     private boolean mCalled = false;
 
+    /** Creates a ResultReceiver. */
+    public ResultReceiver() {
+        this(Duration.ofSeconds(30));
+    }
+
+    /** Creates a ResultReceiver with a deadline. */
+    public ResultReceiver(Duration deadline) {
+        mDeadline = deadline;
+    }
+
+    private void await() throws InterruptedException {
+        if (mDeadline != null) {
+            assertTrue(mLatch.await(mDeadline.toMillis(), TimeUnit.MILLISECONDS));
+        } else {
+            mLatch.await();
+        }
+    }
+
     @Override public void onResult(T result) {
         mCalled = true;
         mSuccess = true;
@@ -46,37 +69,37 @@
 
     /** Returns the result passed to the OutcomeReceiver. */
     public T getResult() throws InterruptedException {
-        mLatch.await();
+        await();
         return mResult;
     }
 
     /** Returns the exception passed to the OutcomeReceiver. */
     public Exception getException() throws InterruptedException {
-        mLatch.await();
+        await();
         return mException;
     }
 
     /** Returns true if onResult() was called. */
     public boolean isSuccess() throws InterruptedException {
-        mLatch.await();
+        await();
         return mSuccess;
     }
 
     /** Returns true if onError() was called. */
     public boolean isError() throws InterruptedException {
-        mLatch.await();
+        await();
         return mError;
     }
 
     /** Returns true if onResult() or onError() was called. */
     public boolean isCalled() throws InterruptedException {
-        mLatch.await();
+        await();
         return mCalled;
     }
 
     /** Returns the exception message. */
     public String getErrorMessage() throws InterruptedException {
-        mLatch.await();
+        await();
         if (mException != null) {
             return mException.getClass().getSimpleName()
                     + ": " + mException.getMessage();