Add client error logging and background job logging infra to ODP, flags
disabled by default

Bug:323574204
Test: atest OnDevicePersonalizationDownloadProcessingJobServiceTests
OnDevicePersonalizationMaintenanceJobServiceTest
OnDevicePersonalizationDataProcessingAsyncCallableTests
UserDataCollectionJobServiceTest MddJobServiceTest -c, manually executed job
1000-1006

Change-Id: I5ef653cb3c5ae3f0685623d70f44e7a339774ed5
diff --git a/Android.bp b/Android.bp
index 1a0fdbe..378b47e 100644
--- a/Android.bp
+++ b/Android.bp
@@ -120,6 +120,7 @@
         "owasp-java-encoder",
         "tensorflowlite_java",
         "tensorflow_core_proto_java_lite",
+        "adservices-shared-spe",
     ],
     sdk_version: "module_current",
     min_sdk_version: "33",
diff --git a/src/com/android/ondevicepersonalization/services/Flags.java b/src/com/android/ondevicepersonalization/services/Flags.java
index aff0a59..2f81145 100644
--- a/src/com/android/ondevicepersonalization/services/Flags.java
+++ b/src/com/android/ondevicepersonalization/services/Flags.java
@@ -16,13 +16,15 @@
 
 package com.android.ondevicepersonalization.services;
 
+import com.android.adservices.shared.common.flags.ModuleSharedFlags;
+
 /**
  * OnDevicePersonalization Feature Flags interface. This Flags interface hold the default values
  * of flags. The default values in this class must match with the default values in PH since we
  * will migrate to Flag Codegen in the future. With that migration, the Flags.java file will be
  * generated from the GCL.
  */
-public interface Flags {
+public interface Flags extends ModuleSharedFlags {
     /**
      * Global OnDevicePersonalization Kill Switch. This overrides all other killswitches.
      * The default value is true which means OnDevicePersonalization is disabled.
@@ -79,6 +81,21 @@
      */
     boolean DEFAULT_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED = false;
 
+    /**
+     * Default value for enabling client error logging.
+     */
+    boolean DEFAULT_CLIENT_ERROR_LOGGING_ENABLED = false;
+
+    /**
+     * Default value for enabling background jobs logging.
+     */
+    boolean DEFAULT_BACKGROUND_JOBS_LOGGING_ENABLED = false;
+
+    /**
+     * Default value for background job sampling logging rate.
+     */
+    int DEFAULT_BACKGROUND_JOB_SAMPLING_LOGGING_RATE = 5;
+
     String DEFAULT_CALLER_APP_ALLOW_LIST =
             "android.ondevicepersonalization,"
                     + "android.ondevicepersonalization.test.scenario,"
@@ -185,4 +202,8 @@
     default Object getStableFlag(String flagName) {
         return null;
     }
+
+    default boolean getEnableClientErrorLogging() {
+        return DEFAULT_CLIENT_ERROR_LOGGING_ENABLED;
+    }
 }
diff --git a/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfig.java b/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfig.java
index f0fcb14..3ffc631 100644
--- a/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfig.java
+++ b/src/com/android/ondevicepersonalization/services/OnDevicePersonalizationConfig.java
@@ -20,6 +20,8 @@
 import com.android.ondevicepersonalization.services.download.OnDevicePersonalizationDownloadProcessingJobService;
 import com.android.ondevicepersonalization.services.maintenance.OnDevicePersonalizationMaintenanceJobService;
 
+import java.util.Map;
+
 /** Hard-coded configs for OnDevicePersonalization */
 public class OnDevicePersonalizationConfig {
     private OnDevicePersonalizationConfig() {}
@@ -29,34 +31,65 @@
      * com.android.ondevicepersonalization.services.download.mdd.MddJobService})
      */
     public static final int MDD_MAINTENANCE_PERIODIC_TASK_JOB_ID = 1000;
+    public static final String MDD_MAINTENANCE_PERIODIC_TASK_JOB_NAME =
+            "MDD_MAINTENANCE_PERIODIC_TASK";
 
     /**
      * Job ID for Mdd Charging Periodic Task ({@link
      * com.android.ondevicepersonalization.services.download.mdd.MddJobService})
      */
     public static final int MDD_CHARGING_PERIODIC_TASK_JOB_ID = 1001;
+    public static final String MDD_CHARGING_PERIODIC_TASK_JOB_NAME =
+            "MDD_CHARGING_PERIODIC_TASK_JOB";
 
     /**
      * Job ID for Mdd Cellular Charging Task ({@link
      * com.android.ondevicepersonalization.services.download.mdd.MddJobService})
      */
     public static final int MDD_CELLULAR_CHARGING_PERIODIC_TASK_JOB_ID = 1002;
+    public static final String MDD_CELLULAR_CHARGING_PERIODIC_TASK_JOB_NAME =
+            "MDD_CELLULAR_CHARGING_PERIODIC_TASK_JOB";
 
     /**
      * Job ID for Mdd Wifi Charging Task ({@link
      * com.android.ondevicepersonalization.services.download.mdd.MddJobService})
      */
     public static final int MDD_WIFI_CHARGING_PERIODIC_TASK_JOB_ID = 1003;
+    public static final String MDD_WIFI_CHARGING_PERIODIC_TASK_JOB_NAME =
+            "MDD_WIFI_CHARGING_PERIODIC_TASK_JOB";
 
     /**
      * Job ID for Download Processing Task ({@link
      * 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}) */
     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}) */
     public static final int USER_DATA_COLLECTION_ID = 1006;
+    public static final String USER_DATA_COLLECTION_JOB_NAME =
+            "USER_DATA_COLLECTION_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
+    );
 }
diff --git a/src/com/android/ondevicepersonalization/services/PhFlags.java b/src/com/android/ondevicepersonalization/services/PhFlags.java
index 9772e92..0da02ff 100644
--- a/src/com/android/ondevicepersonalization/services/PhFlags.java
+++ b/src/com/android/ondevicepersonalization/services/PhFlags.java
@@ -65,6 +65,15 @@
     public static final String KEY_USER_CONSENT_CACHE_IN_MILLIS =
             "user_consent_cache_duration_millis";
 
+    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";
+
     // OnDevicePersonalization Namespace String from DeviceConfig class
     public static final String NAMESPACE_ON_DEVICE_PERSONALIZATION = "on_device_personalization";
 
@@ -191,30 +200,54 @@
         return SdkLevel.isAtLeastU() && DeviceConfig.getBoolean(
                 /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
                 /* name= */ KEY_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED,
-                /* defaultValue */ DEFAULT_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED);
+                /* defaultValue= */ DEFAULT_SHARED_ISOLATED_PROCESS_FEATURE_ENABLED);
     }
 
     @Override
     public String getCallerAppAllowList() {
         return DeviceConfig.getString(
                 /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
-                /* name */ KEY_CALLER_APP_ALLOW_LIST,
-                /* defaultValue */ DEFAULT_CALLER_APP_ALLOW_LIST);
+                /* name= */ KEY_CALLER_APP_ALLOW_LIST,
+                /* defaultValue= */ DEFAULT_CALLER_APP_ALLOW_LIST);
     }
 
     @Override
     public String getIsolatedServiceAllowList() {
         return DeviceConfig.getString(
                 /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
-                /* name */ KEY_ISOLATED_SERVICE_ALLOW_LIST,
-                /* defaultValue */ DEFAULT_ISOLATED_SERVICE_ALLOW_LIST);
+                /* name= */ KEY_ISOLATED_SERVICE_ALLOW_LIST,
+                /* defaultValue= */ DEFAULT_ISOLATED_SERVICE_ALLOW_LIST);
     }
 
     @Override
     public long getUserConsentCacheInMillis() {
         return DeviceConfig.getLong(
                 /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
-                /* name */ KEY_USER_CONSENT_CACHE_IN_MILLIS,
-                /* defaultValue */ USER_CONSENT_CACHE_IN_MILLIS);
+                /* name= */ KEY_USER_CONSENT_CACHE_IN_MILLIS,
+                /* defaultValue= */ USER_CONSENT_CACHE_IN_MILLIS);
+    }
+
+    @Override
+    public boolean getEnableClientErrorLogging() {
+        return DeviceConfig.getBoolean(
+                /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                /* name= */ KEY_ODP_ENABLE_CLIENT_ERROR_LOGGING,
+                /* defaultValue= */ DEFAULT_CLIENT_ERROR_LOGGING_ENABLED);
+    }
+
+    @Override
+    public boolean getBackgroundJobsLoggingEnabled() {
+        return DeviceConfig.getBoolean(
+                /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                /* name= */ KEY_ODP_BACKGROUND_JOBS_LOGGING_ENABLED,
+                /* defaultValue= */ DEFAULT_BACKGROUND_JOBS_LOGGING_ENABLED);
+    }
+
+    @Override
+    public int getBackgroundJobSamplingLoggingRate() {
+        return DeviceConfig.getInt(
+                /* namespace= */ NAMESPACE_ON_DEVICE_PERSONALIZATION,
+                /* name= */ KEY_ODP_BACKGROUND_JOB_SAMPLING_LOGGING_RATE,
+                /* defaultValue= */ DEFAULT_BACKGROUND_JOB_SAMPLING_LOGGING_RATE);
     }
 }
diff --git a/src/com/android/ondevicepersonalization/services/data/user/UserDataCollectionJobService.java b/src/com/android/ondevicepersonalization/services/data/user/UserDataCollectionJobService.java
index b5c13d7..0a19d83 100644
--- a/src/com/android/ondevicepersonalization/services/data/user/UserDataCollectionJobService.java
+++ b/src/com/android/ondevicepersonalization/services/data/user/UserDataCollectionJobService.java
@@ -18,6 +18,10 @@
 
 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_KILL_SWITCH_ON;
+import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_PERSONALIZATION_NOT_ENABLED;
+import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.USER_DATA_COLLECTION_ID;
+
 import android.app.job.JobInfo;
 import android.app.job.JobParameters;
 import android.app.job.JobScheduler;
@@ -25,11 +29,10 @@
 import android.content.ComponentName;
 import android.content.Context;
 
-
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 import com.android.ondevicepersonalization.services.FlagsFactory;
-import com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig;
 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;
@@ -53,14 +56,14 @@
     public static int schedule(Context context) {
         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
         if (jobScheduler.getPendingJob(
-                OnDevicePersonalizationConfig.USER_DATA_COLLECTION_ID) != null) {
+                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(
-                OnDevicePersonalizationConfig.USER_DATA_COLLECTION_ID, serviceComponent);
+                USER_DATA_COLLECTION_ID, serviceComponent);
 
         // Constraints
         builder.setRequiresDeviceIdle(true);
@@ -77,12 +80,18 @@
     @Override
     public boolean onStartJob(JobParameters params) {
         sLogger.d(TAG + ": onStartJob()");
+        OdpJobServiceLogger.getInstance(this)
+                .recordOnStartJob(USER_DATA_COLLECTION_ID);
         if (FlagsFactory.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().isPersonalizationStatusEnabled()) {
             sLogger.d(TAG + ": Personalization is not allowed, finishing job.");
+            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;
         }
@@ -107,15 +116,27 @@
                     @Override
                     public void onSuccess(Void result) {
                         sLogger.d(TAG + ": User data collection job completed.");
-                        jobFinished(params, /* wantsReschedule = */ false);
+                        boolean wantsReschedule = false;
+                        OdpJobServiceLogger.getInstance(UserDataCollectionJobService.this)
+                                .recordJobFinished(
+                                        USER_DATA_COLLECTION_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(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 = */ false);
+                        jobFinished(params, wantsReschedule);
                     }
                 },
                 OnDevicePersonalizationExecutors.getBackgroundExecutor());
@@ -129,14 +150,23 @@
             mFuture.cancel(true);
         }
         // Reschedule the job since it ended before finishing
-        return true;
+        boolean wantsReschedule = true;
+        OdpJobServiceLogger.getInstance(this)
+                .recordOnStopJob(
+                        params,
+                        USER_DATA_COLLECTION_ID,
+                        wantsReschedule);
+        return wantsReschedule;
     }
 
-    private boolean cancelAndFinishJob(final JobParameters params) {
+    private boolean cancelAndFinishJob(final JobParameters params, int skipReason) {
         JobScheduler jobScheduler = this.getSystemService(JobScheduler.class);
         if (jobScheduler != null) {
-            jobScheduler.cancel(OnDevicePersonalizationConfig.USER_DATA_COLLECTION_ID);
+            jobScheduler.cancel(USER_DATA_COLLECTION_ID);
         }
+        OdpJobServiceLogger.getInstance(this).recordJobSkipped(
+                USER_DATA_COLLECTION_ID,
+                skipReason);
         jobFinished(params, /* wantsReschedule = */ false);
         return true;
     }
diff --git a/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDownloadProcessingJobService.java b/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDownloadProcessingJobService.java
index f34ce91..0298e3d 100644
--- a/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDownloadProcessingJobService.java
+++ b/src/com/android/ondevicepersonalization/services/download/OnDevicePersonalizationDownloadProcessingJobService.java
@@ -19,6 +19,9 @@
 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;
+
 import android.app.job.JobInfo;
 import android.app.job.JobParameters;
 import android.app.job.JobScheduler;
@@ -28,13 +31,12 @@
 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.OnDevicePersonalizationConfig;
 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;
 
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -56,14 +58,14 @@
     public static int schedule(Context context) {
         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
         if (jobScheduler.getPendingJob(
-                OnDevicePersonalizationConfig.DOWNLOAD_PROCESSING_TASK_JOB_ID) != null) {
+                DOWNLOAD_PROCESSING_TASK_JOB_ID) != null) {
             sLogger.d(TAG + ": Job is already scheduled. Doing nothing,");
             return RESULT_FAILURE;
         }
         ComponentName serviceComponent = new ComponentName(context,
                 OnDevicePersonalizationDownloadProcessingJobService.class);
         JobInfo.Builder builder = new JobInfo.Builder(
-                OnDevicePersonalizationConfig.DOWNLOAD_PROCESSING_TASK_JOB_ID, serviceComponent);
+                DOWNLOAD_PROCESSING_TASK_JOB_ID, serviceComponent);
 
         // Constraints.
         builder.setRequiresDeviceIdle(true);
@@ -77,8 +79,12 @@
     @Override
     public boolean onStartJob(JobParameters params) {
         sLogger.d(TAG + ": onStartJob()");
+        OdpJobServiceLogger.getInstance(this).recordOnStartJob(DOWNLOAD_PROCESSING_TASK_JOB_ID);
         if (FlagsFactory.getFlags().getGlobalKillSwitch()) {
             sLogger.d(TAG + ": GlobalKillSwitch enabled, finishing job.");
+            OdpJobServiceLogger.getInstance(this).recordJobSkipped(
+                    DOWNLOAD_PROCESSING_TASK_JOB_ID,
+                    AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON);
             jobFinished(params, /* wantsReschedule = */ false);
             return true;
         }
@@ -102,7 +108,14 @@
             }
         }
         var unused = Futures.whenAllComplete(mFutures).call(() -> {
-            jobFinished(params, /* wantsReschedule */ false);
+            boolean wantsReschedule = false;
+            OdpJobServiceLogger.getInstance(
+                    OnDevicePersonalizationDownloadProcessingJobService.this)
+                    .recordJobFinished(
+                            DOWNLOAD_PROCESSING_TASK_JOB_ID,
+                            /* isSuccessful= */ false,
+                            wantsReschedule);
+            jobFinished(params, wantsReschedule);
             return null;
         }, OnDevicePersonalizationExecutors.getLightweightExecutor());
 
@@ -117,6 +130,12 @@
             }
         }
         // Reschedule the job since it ended before finishing
-        return true;
+        boolean wantsReschedule = true;
+        OdpJobServiceLogger.getInstance(this)
+                .recordOnStopJob(
+                        params,
+                        DOWNLOAD_PROCESSING_TASK_JOB_ID,
+                        wantsReschedule);
+        return wantsReschedule;
     }
 }
diff --git a/src/com/android/ondevicepersonalization/services/download/mdd/MddJobService.java b/src/com/android/ondevicepersonalization/services/download/mdd/MddJobService.java
index eab8ecf..09eb33c 100644
--- a/src/com/android/ondevicepersonalization/services/download/mdd/MddJobService.java
+++ b/src/com/android/ondevicepersonalization/services/download/mdd/MddJobService.java
@@ -16,6 +16,8 @@
 
 package com.android.ondevicepersonalization.services.download.mdd;
 
+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.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_PERSONALIZATION_NOT_ENABLED;
 import static com.android.ondevicepersonalization.services.download.mdd.MddTaskScheduler.MDD_TASK_TAG_KEY;
 
 import static com.google.android.libraries.mobiledatadownload.TaskScheduler.WIFI_CHARGING_PERIODIC_TASK;
@@ -26,12 +28,12 @@
 import android.content.Context;
 import android.os.PersistableBundle;
 
-
 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.user.UserPrivacyStatus;
 import com.android.ondevicepersonalization.services.download.OnDevicePersonalizationDownloadProcessingJobService;
+import com.android.ondevicepersonalization.services.statsd.joblogging.OdpJobServiceLogger;
 
 import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures;
 import com.google.common.util.concurrent.FutureCallback;
@@ -49,14 +51,19 @@
 
     @Override
     public boolean onStartJob(JobParameters params) {
+        int jobId = getMddTaskJobId(params);
         sLogger.d(TAG + ": onStartJob()");
+        OdpJobServiceLogger.getInstance(this).recordOnStartJob(jobId);
         if (FlagsFactory.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().isPersonalizationStatusEnabled()) {
             sLogger.d(TAG + ": Personalization is not allowed, finishing job.");
+            OdpJobServiceLogger.getInstance(this).recordJobSkipped(jobId,
+                    AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_PERSONALIZATION_NOT_ENABLED);
             jobFinished(params, false);
             return true;
         }
@@ -79,17 +86,27 @@
                         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 = */ false);
+                        jobFinished(params, wantsReschedule);
                     }
 
                     @Override
                     public void onFailure(Throwable t) {
-                        sLogger.e(TAG + ": Failed to handle JobService: " + params.getJobId(), 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 = */ false);
+                        jobFinished(params, wantsReschedule);
                     }
                 },
                 OnDevicePersonalizationExecutors.getBackgroundExecutor());
@@ -104,15 +121,19 @@
             OnDevicePersonalizationDownloadProcessingJobService.schedule(this);
         }
         // Reschedule the job since it ended before finishing
-        return true;
+        boolean wantsReschedule = true;
+        OdpJobServiceLogger.getInstance(this)
+                .recordOnStopJob(params, getMddTaskJobId(params), wantsReschedule);
+        return wantsReschedule;
     }
 
-    private boolean cancelAndFinishJob(final JobParameters params) {
+    private boolean cancelAndFinishJob(final JobParameters params, int skipReason) {
         JobScheduler jobScheduler = this.getSystemService(JobScheduler.class);
+        int jobId = getMddTaskJobId(params);
         if (jobScheduler != null) {
-            int jobId = getMddTaskJobId(params);
             jobScheduler.cancel(jobId);
         }
+        OdpJobServiceLogger.getInstance(this).recordJobSkipped(jobId, skipReason);
         jobFinished(params, /* wantsReschedule = */ false);
         return true;
     }
diff --git a/src/com/android/ondevicepersonalization/services/maintenance/OnDevicePersonalizationMaintenanceJobService.java b/src/com/android/ondevicepersonalization/services/maintenance/OnDevicePersonalizationMaintenanceJobService.java
index 580a426..5ad956b 100644
--- a/src/com/android/ondevicepersonalization/services/maintenance/OnDevicePersonalizationMaintenanceJobService.java
+++ b/src/com/android/ondevicepersonalization/services/maintenance/OnDevicePersonalizationMaintenanceJobService.java
@@ -19,6 +19,10 @@
 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.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_PERSONALIZATION_NOT_ENABLED;
+import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.MAINTENANCE_TASK_JOB_ID;
+
 import android.app.job.JobInfo;
 import android.app.job.JobParameters;
 import android.app.job.JobScheduler;
@@ -31,7 +35,6 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
 import com.android.ondevicepersonalization.services.FlagsFactory;
-import com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig;
 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
 import com.android.ondevicepersonalization.services.data.events.EventsDao;
 import com.android.ondevicepersonalization.services.data.user.UserPrivacyStatus;
@@ -40,6 +43,7 @@
 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;
 import com.android.ondevicepersonalization.services.util.PackageUtils;
 
 import com.google.common.util.concurrent.FutureCallback;
@@ -75,14 +79,14 @@
     public static int schedule(Context context) {
         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
         if (jobScheduler.getPendingJob(
-                OnDevicePersonalizationConfig.MAINTENANCE_TASK_JOB_ID) != null) {
+                MAINTENANCE_TASK_JOB_ID) != null) {
             sLogger.d(TAG + ": Job is already scheduled. Doing nothing,");
             return RESULT_FAILURE;
         }
         ComponentName serviceComponent = new ComponentName(context,
                 OnDevicePersonalizationMaintenanceJobService.class);
         JobInfo.Builder builder = new JobInfo.Builder(
-                OnDevicePersonalizationConfig.MAINTENANCE_TASK_JOB_ID, serviceComponent);
+                MAINTENANCE_TASK_JOB_ID, serviceComponent);
 
         // Constraints.
         builder.setRequiresDeviceIdle(true);
@@ -206,12 +210,18 @@
     @Override
     public boolean onStartJob(JobParameters params) {
         sLogger.d(TAG + ": onStartJob()");
+        OdpJobServiceLogger.getInstance(this).recordOnStartJob(
+                MAINTENANCE_TASK_JOB_ID);
         if (FlagsFactory.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().isPersonalizationStatusEnabled()) {
             sLogger.d(TAG + ": Personalization is not allowed, finishing job.");
+            OdpJobServiceLogger.getInstance(this).recordJobSkipped(
+                    MAINTENANCE_TASK_JOB_ID,
+                    AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_PERSONALIZATION_NOT_ENABLED);
             jobFinished(params, false);
             return true;
         }
@@ -234,17 +244,31 @@
                     @Override
                     public void onSuccess(Void result) {
                         sLogger.d(TAG + ": Maintenance job completed.");
+                        boolean wantsReschedule = false;
+                        OdpJobServiceLogger.getInstance(
+                                OnDevicePersonalizationMaintenanceJobService.this)
+                                .recordJobFinished(
+                                        MAINTENANCE_TASK_JOB_ID,
+                                        /* isSuccessful= */ true,
+                                        wantsReschedule);
                         // Tell the JobScheduler that the job has completed and does not needs to be
                         // rescheduled.
-                        jobFinished(params, /* wantsReschedule = */ false);
+                        jobFinished(params, wantsReschedule);
                     }
 
                     @Override
                     public void onFailure(Throwable t) {
                         sLogger.e(TAG + ": Failed to handle JobService: " + params.getJobId(), t);
+                        boolean wantsReschedule = false;
+                        OdpJobServiceLogger.getInstance(
+                                OnDevicePersonalizationMaintenanceJobService.this)
+                                .recordJobFinished(
+                                        MAINTENANCE_TASK_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 = */ false);
+                        jobFinished(params, wantsReschedule);
                     }
                 },
                 OnDevicePersonalizationExecutors.getBackgroundExecutor());
@@ -258,14 +282,23 @@
             mFuture.cancel(true);
         }
         // Reschedule the job since it ended before finishing
-        return true;
+        boolean wantsReschedule = true;
+        OdpJobServiceLogger.getInstance(this)
+                .recordOnStopJob(
+                        params,
+                        MAINTENANCE_TASK_JOB_ID,
+                        wantsReschedule);
+        return wantsReschedule;
     }
 
-    private boolean cancelAndFinishJob(final JobParameters params) {
+    private boolean cancelAndFinishJob(final JobParameters params, int skipReason) {
         JobScheduler jobScheduler = this.getSystemService(JobScheduler.class);
         if (jobScheduler != null) {
-            jobScheduler.cancel(OnDevicePersonalizationConfig.MAINTENANCE_TASK_JOB_ID);
+            jobScheduler.cancel(MAINTENANCE_TASK_JOB_ID);
         }
+        OdpJobServiceLogger.getInstance(this).recordJobSkipped(
+                MAINTENANCE_TASK_JOB_ID,
+                skipReason);
         jobFinished(params, /* wantsReschedule = */ false);
         return true;
     }
diff --git a/src/com/android/ondevicepersonalization/services/statsd/errorlogging/ClientErrorLogger.java b/src/com/android/ondevicepersonalization/services/statsd/errorlogging/ClientErrorLogger.java
new file mode 100644
index 0000000..c6a24f8
--- /dev/null
+++ b/src/com/android/ondevicepersonalization/services/statsd/errorlogging/ClientErrorLogger.java
@@ -0,0 +1,56 @@
+/*
+ * 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.statsd.errorlogging;
+
+import android.annotation.NonNull;
+
+import com.android.adservices.shared.errorlogging.AbstractAdServicesErrorLogger;
+import com.android.adservices.shared.errorlogging.StatsdAdServicesErrorLogger;
+import com.android.adservices.shared.errorlogging.StatsdAdServicesErrorLoggerImpl;
+import com.android.ondevicepersonalization.services.Flags;
+import com.android.ondevicepersonalization.services.FlagsFactory;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.Objects;
+
+public final class ClientErrorLogger extends AbstractAdServicesErrorLogger {
+    private final Flags mFlags;
+
+    private static class LazyInstanceHolder {
+        static final ClientErrorLogger LAZY_INSTANCE =
+                new ClientErrorLogger(
+                        FlagsFactory.getFlags(), StatsdAdServicesErrorLoggerImpl.getInstance());
+    }
+
+    /** Returns the instance of {@link ClientErrorLogger}. */
+    @NonNull
+    public static ClientErrorLogger getInstance() {
+        return ClientErrorLogger.LazyInstanceHolder.LAZY_INSTANCE;
+    }
+
+    @VisibleForTesting
+    ClientErrorLogger(Flags flags, StatsdAdServicesErrorLogger statsdAdServicesErrorLogger) {
+        super(statsdAdServicesErrorLogger);
+        mFlags = Objects.requireNonNull(flags);
+    }
+
+    @Override
+    protected boolean isEnabled(int errorCode) {
+        return mFlags.getEnableClientErrorLogging();
+    }
+}
diff --git a/src/com/android/ondevicepersonalization/services/statsd/joblogging/OdpJobServiceLogger.java b/src/com/android/ondevicepersonalization/services/statsd/joblogging/OdpJobServiceLogger.java
new file mode 100644
index 0000000..b82bea3
--- /dev/null
+++ b/src/com/android/ondevicepersonalization/services/statsd/joblogging/OdpJobServiceLogger.java
@@ -0,0 +1,73 @@
+/*
+ * 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.statsd.joblogging;
+
+import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.JOB_ID_TO_NAME_MAP;
+
+import android.content.Context;
+
+import com.android.adservices.shared.common.flags.ModuleSharedFlags;
+import com.android.adservices.shared.errorlogging.AdServicesErrorLogger;
+import com.android.adservices.shared.spe.logging.JobServiceLogger;
+import com.android.adservices.shared.spe.logging.StatsdJobServiceLogger;
+import com.android.adservices.shared.util.Clock;
+import com.android.internal.annotations.GuardedBy;
+import com.android.ondevicepersonalization.services.FlagsFactory;
+import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
+import com.android.ondevicepersonalization.services.statsd.errorlogging.ClientErrorLogger;
+
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/** 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 final Object SINGLETON_LOCK = new Object();
+
+    /** Create an instance of {@link JobServiceLogger}. */
+    public OdpJobServiceLogger(
+            Context context,
+            Clock clock,
+            StatsdJobServiceLogger statsdLogger,
+            AdServicesErrorLogger errorLogger,
+            Executor executor,
+            Map<Integer, String> jobIdToNameMap,
+            ModuleSharedFlags flags) {
+        super(context, clock, statsdLogger, errorLogger, executor, jobIdToNameMap, flags);
+    }
+
+    /** Get a singleton instance of {@link JobServiceLogger} to be used. */
+    public static OdpJobServiceLogger getInstance(Context context) {
+        synchronized (SINGLETON_LOCK) {
+            if (sSingleton == null) {
+                sSingleton =
+                        new OdpJobServiceLogger(
+                                context,
+                                Clock.getInstance(),
+                                new OdpStatsdJobServiceLogger(),
+                                ClientErrorLogger.getInstance(),
+                                OnDevicePersonalizationExecutors.getBackgroundExecutor(),
+                                JOB_ID_TO_NAME_MAP,
+                                FlagsFactory.getFlags());
+            }
+
+            return sSingleton;
+        }
+    }
+}
diff --git a/src/com/android/ondevicepersonalization/services/statsd/joblogging/OdpStatsdJobServiceLogger.java b/src/com/android/ondevicepersonalization/services/statsd/joblogging/OdpStatsdJobServiceLogger.java
new file mode 100644
index 0000000..4c640e1
--- /dev/null
+++ b/src/com/android/ondevicepersonalization/services/statsd/joblogging/OdpStatsdJobServiceLogger.java
@@ -0,0 +1,40 @@
+/*
+ * 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.statsd.joblogging;
+
+import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED;
+import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__MODULE_NAME__MODULE_NAME_ON_DEVICE_PERSONALIZATION;
+
+import com.android.adservices.service.stats.AdServicesStatsLog;
+import com.android.adservices.shared.spe.logging.ExecutionReportedStats;
+import com.android.adservices.shared.spe.logging.StatsdJobServiceLogger;
+
+/** ODP implementation of {@link StatsdJobServicesLogger}. */
+public final class OdpStatsdJobServiceLogger implements StatsdJobServiceLogger {
+
+    /** Logging method for ODP background job execution stats. */
+    public void logExecutionReportedStats(ExecutionReportedStats stats) {
+        AdServicesStatsLog.write(
+                AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED,
+                stats.getJobId(),
+                stats.getExecutionLatencyMs(),
+                stats.getExecutionPeriodMinute(),
+                stats.getExecutionResultCode(),
+                stats.getStopReason(),
+                AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__MODULE_NAME__MODULE_NAME_ON_DEVICE_PERSONALIZATION);
+    }
+}
diff --git a/tests/manualtests/Android.bp b/tests/manualtests/Android.bp
index d42e63a..96d4956 100644
--- a/tests/manualtests/Android.bp
+++ b/tests/manualtests/Android.bp
@@ -52,6 +52,7 @@
         "modules-utils-list-slice",
         "owasp-java-encoder",
         "tensorflowlite_java",
+        "adservices-shared-spe",
     ],
     sdk_version: "module_current",
     target_sdk_version: "current",
diff --git a/tests/servicetests/Android.bp b/tests/servicetests/Android.bp
index a6cc46f..0a8d6f5 100644
--- a/tests/servicetests/Android.bp
+++ b/tests/servicetests/Android.bp
@@ -54,6 +54,7 @@
         "owasp-java-encoder",
         "compatibility-device-util-axt",
         "tensorflowlite_java",
+        "adservices-shared-spe",
     ],
     sdk_version: "module_current",
     target_sdk_version: "current",
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 d3b4436..c09b727 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
@@ -153,8 +153,29 @@
         mUserPrivacyStatus.setPersonalizationStatusEnabled(false);
         MockitoSession session = ExtendedMockito.mockitoSession().startMocking();
         try {
+            JobScheduler mJobScheduler = mContext.getSystemService(JobScheduler.class);
+            PersistableBundle extras = new PersistableBundle();
+            extras.putString(MDD_TASK_TAG_KEY, WIFI_CHARGING_PERIODIC_TASK);
+            JobInfo jobInfo =
+                    new JobInfo.Builder(
+                            MDD_WIFI_CHARGING_PERIODIC_TASK_JOB_ID,
+                            new ComponentName(mContext, MddJobService.class))
+                            .setRequiresDeviceIdle(true)
+                            .setRequiresCharging(false)
+                            .setRequiresBatteryNotLow(true)
+                            .setPeriodic(21_600_000L)
+                            .setPersisted(true)
+                            .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
+                            .setExtras(extras)
+                            .build();
+            mJobScheduler.schedule(jobInfo);
+            assertTrue(mJobScheduler.getPendingJob(MDD_WIFI_CHARGING_PERIODIC_TASK_JOB_ID)
+                    != null);
+            doReturn(mJobScheduler).when(mSpyService).getSystemService(JobScheduler.class);
             doNothing().when(mSpyService).jobFinished(any(), anyBoolean());
-            boolean result = mSpyService.onStartJob(mock(JobParameters.class));
+            JobParameters jobParameters = mock(JobParameters.class);
+            doReturn(extras).when(jobParameters).getExtras();
+            boolean result = mSpyService.onStartJob(jobParameters);
             assertTrue(result);
             verify(mSpyService, times(1)).jobFinished(any(), eq(false));
             verify(mMockJobScheduler, times(0)).schedule(any());
@@ -213,7 +234,12 @@
         MockitoSession session = ExtendedMockito.mockitoSession().strictness(
                 Strictness.LENIENT).startMocking();
         try {
-            assertTrue(mSpyService.onStopJob(mock(JobParameters.class)));
+            JobParameters jobParameters = mock(JobParameters.class);
+            PersistableBundle extras = new PersistableBundle();
+            extras.putString(MDD_TASK_TAG_KEY, WIFI_CHARGING_PERIODIC_TASK);
+            doReturn(extras).when(jobParameters).getExtras();
+
+            assertTrue(mSpyService.onStopJob(jobParameters));
             verify(mMockJobScheduler, times(0)).schedule(any());
         } finally {
             session.finishMocking();