diff options
author | Aditya Gupta <adigupt@google.com> | 2023-08-24 12:59:56 +0000 |
---|---|---|
committer | Aditya Gupta <adigupt@google.com> | 2023-08-30 10:01:41 +0000 |
commit | 59f4e3aace94eb88aab7eb40411917bdb4bb4e0e (patch) | |
tree | dceb7f5d41fdc68099f0b7468b6f81827b14e586 /adservices/service-core/java | |
parent | 2841c570b45b7dba5d722a9baccfef835c9028f2 (diff) | |
download | AdServices-59f4e3aace94eb88aab7eb40411917bdb4bb4e0e.tar.gz |
adding background job to send ad selection debug reports in a batch
Test: atest
Bug: 293867930
Change-Id: Ifefed807cf988ffcd43f1c95fa3e536e671ffe35
Diffstat (limited to 'adservices/service-core/java')
10 files changed, 676 insertions, 4 deletions
diff --git a/adservices/service-core/java/com/android/adservices/service/Flags.java b/adservices/service-core/java/com/android/adservices/service/Flags.java index 009cb47ab4..c4aa2cccbb 100644 --- a/adservices/service-core/java/com/android/adservices/service/Flags.java +++ b/adservices/service-core/java/com/android/adservices/service/Flags.java @@ -1083,6 +1083,15 @@ public interface Flags { return FLEDGE_EVENT_LEVEL_DEBUG_REPORTING_ENABLED; } + boolean FLEDGE_EVENT_LEVEL_DEBUG_REPORT_SEND_IMMEDIATELY = false; + + /** + * @return whether to call remote URLs for debug reporting. + */ + default boolean getFledgeEventLevelDebugReportSendImmediately() { + return FLEDGE_EVENT_LEVEL_DEBUG_REPORT_SEND_IMMEDIATELY; + } + int FLEDGE_EVENT_LEVEL_DEBUG_REPORTING_BATCH_DELAY_SECONDS = 60 * 15; /** @@ -1101,6 +1110,56 @@ public interface Flags { return FLEDGE_EVENT_LEVEL_DEBUG_REPORTING_MAX_ITEMS_PER_BATCH; } + int FLEDGE_DEBUG_REPORT_SENDER_JOB_NETWORK_CONNECT_TIMEOUT_MS = 5 * 1000; // 5 seconds + + /** + * @return the maximum time in milliseconds allowed for a network call to open its initial + * connection during the FLEDGE debug report sender job. + */ + default int getFledgeDebugReportSenderJobNetworkConnectionTimeoutMs() { + return FLEDGE_DEBUG_REPORT_SENDER_JOB_NETWORK_CONNECT_TIMEOUT_MS; + } + + int FLEDGE_DEBUG_REPORT_SENDER_JOB_NETWORK_READ_TIMEOUT_MS = 30 * 1000; // 30 seconds + + /** + * @return the maximum time in milliseconds allowed for a network call to read a response from a + * target server during the FLEDGE debug report sender job. + */ + default int getFledgeDebugReportSenderJobNetworkReadTimeoutMs() { + return FLEDGE_DEBUG_REPORT_SENDER_JOB_NETWORK_READ_TIMEOUT_MS; + } + + long FLEDGE_DEBUG_REPORT_SENDER_JOB_MAX_RUNTIME_MS = 10L * 60L * 1000L; // 5 minutes + + /** + * @return the maximum amount of time (in milliseconds) each FLEDGE debug report sender job is + * allowed to run. + */ + default long getFledgeDebugReportSenderJobMaxRuntimeMs() { + return FLEDGE_DEBUG_REPORT_SENDER_JOB_MAX_RUNTIME_MS; + } + + long FLEDGE_DEBUG_REPORT_SENDER_JOB_PERIOD_MS = TimeUnit.MINUTES.toMillis(10); + + /** + * @return the best effort max time (in milliseconds) between each FLEDGE debug report sender + * job run. + */ + default long getFledgeDebugReportSenderJobPeriodMs() { + return FLEDGE_DEBUG_REPORT_SENDER_JOB_PERIOD_MS; + } + + long FLEDGE_DEBUG_REPORT_SENDER_JOB_FLEX_MS = TimeUnit.MINUTES.toMillis(2); + + /** + * @return the amount of flex (in milliseconds) around the end of each period to run each FLEDGE + * debug report sender job. + */ + default long getFledgeDebugReportSenderJobFlexMs() { + return FLEDGE_DEBUG_REPORT_SENDER_JOB_FLEX_MS; + } + boolean FLEDGE_AD_SELECTION_OFF_DEVICE_REQUEST_COMPRESSION_ENABLED = true; /** Returns whether to compress requests sent off device for ad selection. */ diff --git a/adservices/service-core/java/com/android/adservices/service/FlagsConstants.java b/adservices/service-core/java/com/android/adservices/service/FlagsConstants.java index 28fdf3241e..d31b514f4f 100644 --- a/adservices/service-core/java/com/android/adservices/service/FlagsConstants.java +++ b/adservices/service-core/java/com/android/adservices/service/FlagsConstants.java @@ -357,10 +357,22 @@ public final class FlagsConstants { // Event-level debug reporting for Protected Audience. public static final String KEY_FLEDGE_EVENT_LEVEL_DEBUG_REPORTING_ENABLED = "fledge_event_level_debug_reporting_enabled"; + public static final String KEY_FLEDGE_EVENT_LEVEL_DEBUG_REPORT_SEND_IMMEDIATELY = + "fledge_event_level_debug_report_send_immediately"; public static final String KEY_FLEDGE_EVENT_LEVEL_DEBUG_REPORTING_BATCH_DELAY_SECONDS = "fledge_event_level_debug_reporting_batch_delay_seconds"; public static final String KEY_FLEDGE_EVENT_LEVEL_DEBUG_REPORTING_MAX_ITEMS_PER_BATCH = "fledge_event_level_debug_reporting_max_items_per_batch"; + public static final String KEY_FLEDGE_DEBUG_REPORTI_SENDER_JOB_NETWORK_CONNECT_TIMEOUT_MS = + "fledge_debug_report_sender_job_network_connect_timeout_ms"; + public static final String KEY_FLEDGE_DEBUG_REPORTI_SENDER_JOB_NETWORK_READ_TIMEOUT_MS = + "fledge_debug_report_sender_job_network_read_timeout_ms"; + public static final String KEY_FLEDGE_DEBUG_REPORTI_SENDER_JOB_MAX_TIMEOUT_MS = + "fledge_debug_report_sender_job_max_timeout_ms"; + public static final String KEY_FLEDGE_DEBUG_REPORT_SENDER_JOB_PERIOD_MS = + "fledge_debug_report_sender_job_period_ms"; + public static final String KEY_FLEDGE_DEBUG_REPORT_SENDER_JOB_FLEX_MS = + "fledge_debug_report_sender_job_flex_ms"; // Server-auction flags for Protected Audience. public static final String KEY_FLEDGE_AUCTION_SERVER_ENABLED = "fledge_auction_server_enabled"; diff --git a/adservices/service-core/java/com/android/adservices/service/FlagsFactory.java b/adservices/service-core/java/com/android/adservices/service/FlagsFactory.java index 4ed6c1dc1b..03a159708c 100644 --- a/adservices/service-core/java/com/android/adservices/service/FlagsFactory.java +++ b/adservices/service-core/java/com/android/adservices/service/FlagsFactory.java @@ -89,6 +89,11 @@ public class FlagsFactory { public boolean getFledgeAuctionServerEnabled() { return true; } + + @Override + public boolean getFledgeEventLevelDebugReportingEnabled() { + return true; + } }; } } diff --git a/adservices/service-core/java/com/android/adservices/service/PhFlags.java b/adservices/service-core/java/com/android/adservices/service/PhFlags.java index 07a74688d5..dff746a2c9 100644 --- a/adservices/service-core/java/com/android/adservices/service/PhFlags.java +++ b/adservices/service-core/java/com/android/adservices/service/PhFlags.java @@ -48,7 +48,6 @@ import java.util.stream.Collectors; /** Flags Implementation that delegates to DeviceConfig. */ // TODO(b/228037065): Add validation logics for Feature flags read from PH. public final class PhFlags implements Flags { - private static final PhFlags sSingleton = new PhFlags(); /** Returns the singleton instance of the PhFlags. */ @@ -2283,6 +2282,14 @@ public final class PhFlags implements Flags { } @Override + public boolean getFledgeEventLevelDebugReportSendImmediately() { + return DeviceConfig.getBoolean( + FlagsConstants.NAMESPACE_ADSERVICES, + /* flagName */ FlagsConstants.KEY_FLEDGE_EVENT_LEVEL_DEBUG_REPORT_SEND_IMMEDIATELY, + /* defaultValue */ FLEDGE_EVENT_LEVEL_DEBUG_REPORT_SEND_IMMEDIATELY); + } + + @Override public int getFledgeEventLevelDebugReportingBatchDelaySeconds() { return DeviceConfig.getInt( FlagsConstants.NAMESPACE_ADSERVICES, @@ -2303,6 +2310,53 @@ public final class PhFlags implements Flags { } @Override + public int getFledgeDebugReportSenderJobNetworkConnectionTimeoutMs() { + return DeviceConfig.getInt( + FlagsConstants.NAMESPACE_ADSERVICES, + /* flagName */ FlagsConstants + .KEY_FLEDGE_DEBUG_REPORTI_SENDER_JOB_NETWORK_CONNECT_TIMEOUT_MS, + /* defaultValue */ + FLEDGE_DEBUG_REPORT_SENDER_JOB_NETWORK_CONNECT_TIMEOUT_MS); + } + + @Override + public int getFledgeDebugReportSenderJobNetworkReadTimeoutMs() { + return DeviceConfig.getInt( + FlagsConstants.NAMESPACE_ADSERVICES, + /* flagName */ FlagsConstants + .KEY_FLEDGE_DEBUG_REPORTI_SENDER_JOB_NETWORK_READ_TIMEOUT_MS, + /* defaultValue */ + FLEDGE_DEBUG_REPORT_SENDER_JOB_NETWORK_READ_TIMEOUT_MS); + } + + @Override + public long getFledgeDebugReportSenderJobMaxRuntimeMs() { + return DeviceConfig.getLong( + FlagsConstants.NAMESPACE_ADSERVICES, + /* flagName */ FlagsConstants.KEY_FLEDGE_DEBUG_REPORTI_SENDER_JOB_MAX_TIMEOUT_MS, + /* defaultValue */ + FLEDGE_DEBUG_REPORT_SENDER_JOB_MAX_RUNTIME_MS); + } + + @Override + public long getFledgeDebugReportSenderJobPeriodMs() { + return DeviceConfig.getLong( + FlagsConstants.NAMESPACE_ADSERVICES, + /* flagName */ FlagsConstants.KEY_FLEDGE_DEBUG_REPORT_SENDER_JOB_PERIOD_MS, + /* defaultValue */ + FLEDGE_DEBUG_REPORT_SENDER_JOB_PERIOD_MS); + } + + @Override + public long getFledgeDebugReportSenderJobFlexMs() { + return DeviceConfig.getLong( + FlagsConstants.NAMESPACE_ADSERVICES, + /* flagName */ FlagsConstants.KEY_FLEDGE_DEBUG_REPORT_SENDER_JOB_FLEX_MS, + /* defaultValue */ + FLEDGE_DEBUG_REPORT_SENDER_JOB_FLEX_MS); + } + + @Override public boolean getEnforceForegroundStatusForFledgeRunAdSelection() { return DeviceConfig.getBoolean( FlagsConstants.NAMESPACE_ADSERVICES, @@ -4017,6 +4071,12 @@ public final class PhFlags implements Flags { writer.println( "\t" + + FlagsConstants.KEY_FLEDGE_EVENT_LEVEL_DEBUG_REPORT_SEND_IMMEDIATELY + + " = " + + getFledgeEventLevelDebugReportSendImmediately()); + + writer.println( + "\t" + FlagsConstants.KEY_FLEDGE_EVENT_LEVEL_DEBUG_REPORTING_BATCH_DELAY_SECONDS + " = " + getFledgeEventLevelDebugReportingBatchDelaySeconds()); @@ -4025,11 +4085,44 @@ public final class PhFlags implements Flags { + FlagsConstants.KEY_FLEDGE_EVENT_LEVEL_DEBUG_REPORTING_MAX_ITEMS_PER_BATCH + " = " + getFledgeEventLevelDebugReportingMaxItemsPerBatch()); + + writer.println( + "\t" + + FlagsConstants + .KEY_FLEDGE_DEBUG_REPORTI_SENDER_JOB_NETWORK_CONNECT_TIMEOUT_MS + + " = " + + getFledgeDebugReportSenderJobNetworkConnectionTimeoutMs()); + + writer.println( + "\t" + + FlagsConstants.KEY_FLEDGE_DEBUG_REPORTI_SENDER_JOB_NETWORK_READ_TIMEOUT_MS + + " = " + + getFledgeDebugReportSenderJobNetworkReadTimeoutMs()); + + writer.println( + "\t" + + FlagsConstants.KEY_FLEDGE_DEBUG_REPORTI_SENDER_JOB_MAX_TIMEOUT_MS + + " = " + + getFledgeDebugReportSenderJobMaxRuntimeMs()); + + writer.println( + "\t" + + FlagsConstants.KEY_FLEDGE_DEBUG_REPORT_SENDER_JOB_PERIOD_MS + + " = " + + getFledgeDebugReportSenderJobPeriodMs()); + + writer.println( + "\t" + + FlagsConstants.KEY_FLEDGE_DEBUG_REPORT_SENDER_JOB_FLEX_MS + + " = " + + getFledgeDebugReportSenderJobFlexMs()); + writer.println( "\t" + FlagsConstants.KEY_PROTECTED_SIGNALS_SERVICE_KILL_SWITCH + " = " + getProtectedSignalsServiceKillSwitch()); + writer.println("==== AdServices PH Flags Throttling Related Flags ===="); writer.println( "\t" diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/DebugReportSenderJobService.java b/adservices/service-core/java/com/android/adservices/service/adselection/DebugReportSenderJobService.java new file mode 100644 index 0000000000..844a9f04dc --- /dev/null +++ b/adservices/service-core/java/com/android/adservices/service/adselection/DebugReportSenderJobService.java @@ -0,0 +1,268 @@ +/* + * 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.adservices.service.adselection; + +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_EXTSERVICES_JOB_ON_TPLUS; +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_USER_CONSENT_REVOKED; +import static com.android.adservices.spe.AdservicesJobInfo.FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB; + +import android.annotation.SuppressLint; +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 android.os.Build; + +import androidx.annotation.RequiresApi; + +import com.android.adservices.LogUtil; +import com.android.adservices.LoggerFactory; +import com.android.adservices.concurrency.AdServicesExecutors; +import com.android.adservices.service.Flags; +import com.android.adservices.service.FlagsFactory; +import com.android.adservices.service.common.compat.ServiceCompatUtils; +import com.android.adservices.service.consent.AdServicesApiType; +import com.android.adservices.service.consent.ConsentManager; +import com.android.adservices.spe.AdservicesJobServiceLogger; +import com.android.internal.annotations.VisibleForTesting; + +import com.google.common.util.concurrent.FutureCallback; + +import java.time.Clock; +import java.time.Instant; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +/** + * Debug report sender for FLEDGE Select Ads API, executing periodic pinging of debug reports and + * cleanup.. + */ +// TODO(b/269798827): Enable for R. +@SuppressLint("LineLength") +@RequiresApi(Build.VERSION_CODES.S) +public class DebugReportSenderJobService extends JobService { + private static final int FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID = + FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB.getJobId(); + + @Override + public boolean onStartJob(JobParameters params) { + // Always ensure that the first thing this job does is check if it should be running, and + // cancel itself if it's not supposed to be. + if (ServiceCompatUtils.shouldDisableExtServicesJobOnTPlus(this)) { + LogUtil.d( + "Disabling DebugReportSenderJobService job because it's running in " + + " ExtServices on T+"); + return skipAndCancelKeyFetchJob( + params, + AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_EXTSERVICES_JOB_ON_TPLUS, + false); + } + LoggerFactory.getFledgeLogger().d("DebugReportSenderJobService.onStartJob"); + + AdservicesJobServiceLogger.getInstance(this) + .recordOnStartJob(FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID); + + if (FlagsFactory.getFlags().getFledgeSelectAdsKillSwitch()) { + LoggerFactory.getFledgeLogger() + .d("FLEDGE Ad Selection API is disabled ; skipping and cancelling job"); + return skipAndCancelKeyFetchJob( + params, + AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON, + true); + } + + if (!FlagsFactory.getFlags().getFledgeEventLevelDebugReportingEnabled()) { + LoggerFactory.getFledgeLogger() + .d( + "FLEDGE Ad Selection Debug Reporting is disabled ; skipping and" + + " cancelling job"); + return skipAndCancelKeyFetchJob( + params, + AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON, + true); + } + + // Skip the execution and cancel the job if user consent is revoked. Use the aggregated + // consent with Beta UX and use the per-API consent with GA UX. + if ((FlagsFactory.getFlags().getGaUxFeatureEnabled() + && (!ConsentManager.getInstance(this) + .getConsent(AdServicesApiType.FLEDGE) + .isGiven())) + || (!FlagsFactory.getFlags().getGaUxFeatureEnabled() + && !ConsentManager.getInstance(this).getConsent().isGiven())) { + LoggerFactory.getFledgeLogger() + .d("User Consent is revoked ; skipping and cancelling job"); + return skipAndCancelKeyFetchJob( + params, + AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_USER_CONSENT_REVOKED, + true); + } + + // TODO(b/235841960): Consider using com.android.adservices.service.stats.Clock instead of + // Java Clock + Instant jobStartTime = Clock.systemUTC().instant(); + LoggerFactory.getFledgeLogger() + .d( + "Starting FLEDGE DebugReportSenderJobService job at %s", + jobStartTime.toString()); + + DebugReportSenderWorker.getInstance(this) + .runDebugReportSender() + .addCallback( + new FutureCallback<Void>() { + // Never manually reschedule the background fetch job, since it is + // already scheduled periodically and should try again multiple times + // per day + @Override + public void onSuccess(Void result) { + boolean shouldRetry = false; + AdservicesJobServiceLogger.getInstance( + DebugReportSenderJobService.this) + .recordJobFinished( + FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID, + /* isSuccessful= */ true, + shouldRetry); + + jobFinished(params, shouldRetry); + } + + @Override + public void onFailure(Throwable t) { + logExceptionMessage(t); + boolean shouldRetry = false; + AdservicesJobServiceLogger.getInstance( + DebugReportSenderJobService.this) + .recordJobFinished( + FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID, + /* isSuccessful= */ false, + shouldRetry); + + jobFinished(params, shouldRetry); + } + }, + AdServicesExecutors.getLightWeightExecutor()); + return true; + } + + private void logExceptionMessage(Throwable t) { + if (t instanceof InterruptedException) { + LoggerFactory.getFledgeLogger() + .e(t, "FLEDGE DebugReport Sender JobService interrupted"); + } else if (t instanceof ExecutionException) { + LoggerFactory.getFledgeLogger() + .e(t, "FLEDGE DebugReport Sender JobService failed due to internal error"); + } else if (t instanceof TimeoutException) { + LoggerFactory.getFledgeLogger() + .e(t, "FLEDGE DebugReport Sender JobService timeout exceeded"); + } else { + LoggerFactory.getFledgeLogger() + .e(t, "FLEDGE DebugReport Sender JobService failed due to unexpected error"); + } + } + + private boolean skipAndCancelKeyFetchJob( + final JobParameters params, int skipReason, boolean doRecord) { + this.getSystemService(JobScheduler.class) + .cancel(FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID); + if (doRecord) { + AdservicesJobServiceLogger.getInstance(this) + .recordJobSkipped(FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID, skipReason); + } + jobFinished(params, false); + return false; + } + + @Override + public boolean onStopJob(JobParameters params) { + LoggerFactory.getFledgeLogger().d("DebugReportSenderJobService.onStopJob"); + DebugReportSenderWorker.getInstance(this).stopWork(); + + boolean shouldRetry = true; + + AdservicesJobServiceLogger.getInstance(this) + .recordOnStopJob( + params, FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID, shouldRetry); + return shouldRetry; + } + + /** + * Attempts to schedule the FLEDGE Ad Selection debug report sender as a singleton periodic job + * if it is not already scheduled. + * + * <p>The debug report sender job primarily sends debug reports generated for ad selections. It + * also prunes the ad selection debug report database of any expired data. + */ + public static void scheduleIfNeeded(Context context, boolean forceSchedule) { + Flags flags = FlagsFactory.getFlags(); + if (!flags.getFledgeEventLevelDebugReportingEnabled()) { + LoggerFactory.getFledgeLogger() + .v("FLEDGE Ad selection Debug reporting is disabled; skipping schedule"); + return; + } + + final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + + // Scheduling a job can be expensive, and forcing a schedule could interrupt a job that is + // already in progress + // TODO(b/221837833): Intelligently decide when to overwrite a scheduled job + if ((jobScheduler.getPendingJob(FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID) == null) + || forceSchedule) { + schedule(context, flags); + LoggerFactory.getFledgeLogger() + .d("Scheduled FLEDGE ad selection Debug report sender job"); + } else { + LoggerFactory.getFledgeLogger() + .v( + "FLEDGE ad selection Debug report sender job already scheduled," + + " skipping reschedule"); + } + } + + /** + * Actually schedules the FLEDGE Ad Selection Debug Report sender job as a singleton periodic + * job. + * + * <p>Split out from {@link #scheduleIfNeeded(Context, boolean)} for mockable testing without + * pesky permissions. + */ + @VisibleForTesting + protected static void schedule(Context context, Flags flags) { + if (!flags.getFledgeEventLevelDebugReportingEnabled()) { + LoggerFactory.getFledgeLogger() + .v("FLEDGE Ad selection Debug reporting is disabled; skipping schedule"); + return; + } + + final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + final JobInfo job = + new JobInfo.Builder( + FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB_ID, + new ComponentName(context, DebugReportSenderJobService.class)) + .setRequiresBatteryNotLow(true) + .setRequiresDeviceIdle(true) + .setPeriodic( + flags.getFledgeDebugReportSenderJobPeriodMs(), + flags.getFledgeDebugReportSenderJobFlexMs()) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) + .setPersisted(true) + .build(); + jobScheduler.schedule(job); + } +} diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/DebugReportSenderWorker.java b/adservices/service-core/java/com/android/adservices/service/adselection/DebugReportSenderWorker.java new file mode 100644 index 0000000000..c6f15538cc --- /dev/null +++ b/adservices/service-core/java/com/android/adservices/service/adselection/DebugReportSenderWorker.java @@ -0,0 +1,212 @@ +/* + * 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.adservices.service.adselection; + +import android.annotation.NonNull; +import android.content.Context; +import android.net.Uri; + +import com.android.adservices.LoggerFactory; +import com.android.adservices.concurrency.AdServicesExecutors; +import com.android.adservices.data.adselection.AdSelectionDebugReportDao; +import com.android.adservices.data.adselection.AdSelectionDebugReportingDatabase; +import com.android.adservices.data.adselection.DBAdSelectionDebugReport; +import com.android.adservices.service.Flags; +import com.android.adservices.service.FlagsFactory; +import com.android.adservices.service.common.SingletonRunner; +import com.android.adservices.service.common.httpclient.AdServicesHttpsClient; +import com.android.adservices.service.devapi.DevContext; +import com.android.internal.annotations.VisibleForTesting; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; + +import java.time.Clock; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** Worker class to send and clean debug reports generated for ad selection. */ +public class DebugReportSenderWorker { + private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger(); + public static final String JOB_DESCRIPTION = "Ad selection debug report sender job"; + private static final Object SINGLETON_LOCK = new Object(); + private static volatile DebugReportSenderWorker sDebugReportSenderWorker; + @NonNull private final AdSelectionDebugReportDao mAdSelectionDebugReportDao; + @NonNull private final AdServicesHttpsClient mAdServicesHttpsClient; + @NonNull private final Flags mFlags; + @NonNull private final Clock mClock; + private final SingletonRunner<Void> mSingletonRunner = + new SingletonRunner<>(JOB_DESCRIPTION, this::doRun); + + @VisibleForTesting + protected DebugReportSenderWorker( + @NonNull AdSelectionDebugReportDao adSelectionDebugReportDao, + @NonNull AdServicesHttpsClient adServicesHttpsClient, + @NonNull Flags flags, + @NonNull Clock clock) { + Objects.requireNonNull(adSelectionDebugReportDao); + Objects.requireNonNull(adServicesHttpsClient); + Objects.requireNonNull(flags); + Objects.requireNonNull(clock); + + mAdSelectionDebugReportDao = adSelectionDebugReportDao; + mAdServicesHttpsClient = adServicesHttpsClient; + mClock = clock; + mFlags = flags; + } + + /** + * Gets an instance of a {@link DebugReportSenderWorker}. If an instance hasn't been + * initialized, a new singleton will be created and returned. + */ + @NonNull + public static DebugReportSenderWorker getInstance(@NonNull Context context) { + Objects.requireNonNull(context); + + if (sDebugReportSenderWorker == null) { + synchronized (SINGLETON_LOCK) { + if (sDebugReportSenderWorker == null) { + AdSelectionDebugReportDao adSelectionDebugReportDao = + AdSelectionDebugReportingDatabase.getInstance(context) + .getAdSelectionDebugReportDao(); + Flags flags = FlagsFactory.getFlags(); + AdServicesHttpsClient adServicesHttpsClient = + new AdServicesHttpsClient( + AdServicesExecutors.getBlockingExecutor(), + flags.getFledgeDebugReportSenderJobNetworkConnectionTimeoutMs(), + flags.getFledgeDebugReportSenderJobNetworkReadTimeoutMs(), + AdServicesHttpsClient.DEFAULT_MAX_BYTES); + sDebugReportSenderWorker = + new DebugReportSenderWorker( + adSelectionDebugReportDao, + adServicesHttpsClient, + flags, + Clock.systemUTC()); + } + } + } + return sDebugReportSenderWorker; + } + + /** + * Runs the debug report sender job for Ad Selection Debug Reports. + * + * @return A future to be used to check when the task has completed. + */ + public FluentFuture<Void> runDebugReportSender() { + sLogger.d("Starting %s", JOB_DESCRIPTION); + return mSingletonRunner.runSingleInstance(); + } + + /** Requests that any ongoing work be stopped gracefully and waits for work to be stopped. */ + public void stopWork() { + mSingletonRunner.stopWork(); + } + + private FluentFuture<List<DBAdSelectionDebugReport>> getDebugReports( + @NonNull Supplier<Boolean> shouldStop, @NonNull Instant jobStartTime) { + if (shouldStop.get()) { + sLogger.d("Stopping " + JOB_DESCRIPTION); + return FluentFuture.from(Futures.immediateFuture(ImmutableList.of())); + } + int batchSizeForDebugReports = mFlags.getFledgeEventLevelDebugReportingMaxItemsPerBatch(); + sLogger.v("Getting %d debug reports from database", batchSizeForDebugReports); + return FluentFuture.from( + AdServicesExecutors.getBackgroundExecutor() + .submit( + () -> { + List<DBAdSelectionDebugReport> debugReports = + mAdSelectionDebugReportDao.getDebugReportsBeforeTime( + jobStartTime, batchSizeForDebugReports); + if (debugReports == null) { + sLogger.v("no debug reports to send"); + return Collections.emptyList(); + } + sLogger.v( + "found %d debug reports from database", + debugReports.size()); + return debugReports; + })); + } + + private FluentFuture<Void> cleanupDebugReportsData(Instant jobStartTime) { + sLogger.v( + "cleaning up old debug reports from the database at time %s", + jobStartTime.toString()); + return FluentFuture.from( + AdServicesExecutors.getBackgroundExecutor() + .submit( + () -> { + mAdSelectionDebugReportDao.deleteDebugReportsBeforeTime( + jobStartTime); + return null; + })); + } + + private ListenableFuture<Void> sendDebugReports( + @NonNull List<DBAdSelectionDebugReport> dbAdSelectionDebugReports) { + + if (dbAdSelectionDebugReports.isEmpty()) { + sLogger.d("No debug reports found to send"); + return FluentFuture.from(Futures.immediateVoidFuture()); + } + + sLogger.d("Sending %d debug reports", dbAdSelectionDebugReports.size()); + List<ListenableFuture<Void>> futures = + dbAdSelectionDebugReports.stream() + .map(this::sendDebugReport) + .collect(Collectors.toList()); + return Futures.whenAllComplete(futures) + .call(() -> null, AdServicesExecutors.getBlockingExecutor()); + } + + private ListenableFuture<Void> sendDebugReport( + DBAdSelectionDebugReport dbAdSelectionDebugReport) { + Uri debugReportUri = dbAdSelectionDebugReport.getDebugReportUri(); + DevContext devContext = + DevContext.builder() + .setDevOptionsEnabled(dbAdSelectionDebugReport.getDevOptionsEnabled()) + .build(); + sLogger.v("Sending debug report %s", debugReportUri.toString()); + try { + return mAdServicesHttpsClient.getAndReadNothing(debugReportUri, devContext); + } catch (Exception ignored) { + sLogger.v("Failed to send debug report %s", debugReportUri.toString()); + return Futures.immediateVoidFuture(); + } + } + + private FluentFuture<Void> doRun(@NonNull Supplier<Boolean> shouldStop) { + Instant jobStartTime = mClock.instant(); + return getDebugReports(shouldStop, jobStartTime) + .transform(this::sendDebugReports, AdServicesExecutors.getBackgroundExecutor()) + .transformAsync( + ignored -> cleanupDebugReportsData(jobStartTime), + AdServicesExecutors.getBackgroundExecutor()) + .withTimeout( + mFlags.getFledgeDebugReportSenderJobMaxRuntimeMs(), + TimeUnit.MILLISECONDS, + AdServicesExecutors.getScheduler()); + } +} diff --git a/adservices/service-core/java/com/android/adservices/service/common/BackgroundJobsManager.java b/adservices/service-core/java/com/android/adservices/service/common/BackgroundJobsManager.java index 671ab81de4..25e492e1c1 100644 --- a/adservices/service-core/java/com/android/adservices/service/common/BackgroundJobsManager.java +++ b/adservices/service-core/java/com/android/adservices/service/common/BackgroundJobsManager.java @@ -18,6 +18,7 @@ package com.android.adservices.service.common; import static com.android.adservices.spe.AdservicesJobInfo.COBALT_LOGGING_JOB; import static com.android.adservices.spe.AdservicesJobInfo.CONSENT_NOTIFICATION_JOB; +import static com.android.adservices.spe.AdservicesJobInfo.FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB; import static com.android.adservices.spe.AdservicesJobInfo.FLEDGE_BACKGROUND_FETCH_JOB; import static com.android.adservices.spe.AdservicesJobInfo.MAINTENANCE_JOB; import static com.android.adservices.spe.AdservicesJobInfo.MEASUREMENT_AGGREGATE_FALLBACK_REPORTING_JOB; @@ -43,8 +44,10 @@ import androidx.annotation.RequiresApi; import com.android.adservices.cobalt.CobaltJobService; import com.android.adservices.download.MddJobService; +import com.android.adservices.service.Flags; import com.android.adservices.service.FlagsFactory; import com.android.adservices.service.MaintenanceJobService; +import com.android.adservices.service.adselection.DebugReportSenderJobService; import com.android.adservices.service.consent.AdServicesApiType; import com.android.adservices.service.measurement.DeleteExpiredJobService; import com.android.adservices.service.measurement.DeleteUninstalledJobService; @@ -130,8 +133,12 @@ public class BackgroundJobsManager { * @param context application context. */ public static void scheduleFledgeBackgroundJobs(@NonNull Context context) { - if (!FlagsFactory.getFlags().getFledgeSelectAdsKillSwitch()) { + Flags flags = FlagsFactory.getFlags(); + if (!flags.getFledgeSelectAdsKillSwitch()) { MaintenanceJobService.scheduleIfNeeded(context, /* forceSchedule= */ false); + if (flags.getFledgeEventLevelDebugReportingEnabled()) { + DebugReportSenderJobService.scheduleIfNeeded(context, false); + } } } @@ -287,6 +294,7 @@ public class BackgroundJobsManager { Objects.requireNonNull(jobScheduler); jobScheduler.cancel(FLEDGE_BACKGROUND_FETCH_JOB.getJobId()); + jobScheduler.cancel(FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB.getJobId()); } /** diff --git a/adservices/service-core/java/com/android/adservices/service/common/FledgeMaintenanceTasksWorker.java b/adservices/service-core/java/com/android/adservices/service/common/FledgeMaintenanceTasksWorker.java index 19643e8bd0..a24d8bffcc 100644 --- a/adservices/service-core/java/com/android/adservices/service/common/FledgeMaintenanceTasksWorker.java +++ b/adservices/service-core/java/com/android/adservices/service/common/FledgeMaintenanceTasksWorker.java @@ -23,6 +23,8 @@ import android.content.pm.PackageManager; import com.android.adservices.LoggerFactory; import com.android.adservices.data.adselection.AdSelectionDatabase; +import com.android.adservices.data.adselection.AdSelectionDebugReportDao; +import com.android.adservices.data.adselection.AdSelectionDebugReportingDatabase; import com.android.adservices.data.adselection.AdSelectionEntryDao; import com.android.adservices.data.adselection.AdSelectionServerDatabase; import com.android.adservices.data.adselection.EncryptionContextDao; @@ -41,6 +43,7 @@ import java.util.Objects; public class FledgeMaintenanceTasksWorker { private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger(); @NonNull private final AdSelectionEntryDao mAdSelectionEntryDao; + @NonNull private final AdSelectionDebugReportDao mAdSelectionDebugReportDao; @NonNull private final FrequencyCapDao mFrequencyCapDao; @NonNull private final EnrollmentDao mEnrollmentDao; @NonNull private final EncryptionContextDao mEncryptionContextDao; @@ -54,6 +57,7 @@ public class FledgeMaintenanceTasksWorker { @NonNull FrequencyCapDao frequencyCapDao, @NonNull EnrollmentDao enrollmentDao, @NonNull EncryptionContextDao encryptionContextDao, + @NonNull AdSelectionDebugReportDao adSelectionDebugReportDao, @NonNull Clock clock) { Objects.requireNonNull(flags); Objects.requireNonNull(adSelectionEntryDao); @@ -61,6 +65,7 @@ public class FledgeMaintenanceTasksWorker { Objects.requireNonNull(enrollmentDao); Objects.requireNonNull(clock); Objects.requireNonNull(encryptionContextDao); + Objects.requireNonNull(adSelectionDebugReportDao); mFlags = flags; mAdSelectionEntryDao = adSelectionEntryDao; @@ -68,6 +73,7 @@ public class FledgeMaintenanceTasksWorker { mEnrollmentDao = enrollmentDao; mEncryptionContextDao = encryptionContextDao; mClock = clock; + mAdSelectionDebugReportDao = adSelectionDebugReportDao; } private FledgeMaintenanceTasksWorker(@NonNull Context context) { @@ -79,6 +85,9 @@ public class FledgeMaintenanceTasksWorker { mEncryptionContextDao = AdSelectionServerDatabase.getInstance(context).encryptionContextDao(); mClock = Clock.systemUTC(); + mAdSelectionDebugReportDao = + AdSelectionDebugReportingDatabase.getInstance(context) + .getAdSelectionDebugReportDao(); } /** Creates a new instance of {@link FledgeMaintenanceTasksWorker}. */ @@ -114,6 +123,11 @@ public class FledgeMaintenanceTasksWorker { sLogger.v("Clearing expired Encryption Context"); mEncryptionContextDao.removeExpiredEncryptionContext(expirationTime); } + + if (mFlags.getFledgeEventLevelDebugReportingEnabled()) { + sLogger.v("Clearing expired debug reports "); + mAdSelectionDebugReportDao.deleteDebugReportsBeforeTime(expirationTime); + } } /** diff --git a/adservices/service-core/java/com/android/adservices/service/common/httpclient/AdServicesHttpsClient.java b/adservices/service-core/java/com/android/adservices/service/common/httpclient/AdServicesHttpsClient.java index 61d927d1bb..ef60d31b93 100644 --- a/adservices/service-core/java/com/android/adservices/service/common/httpclient/AdServicesHttpsClient.java +++ b/adservices/service-core/java/com/android/adservices/service/common/httpclient/AdServicesHttpsClient.java @@ -79,9 +79,9 @@ import javax.net.ssl.X509TrustManager; */ public class AdServicesHttpsClient { + public static final long DEFAULT_MAX_BYTES = 1048576; private static final int DEFAULT_TIMEOUT_MS = 5000; // Setting default max content size to 1024 * 1024 which is ~ 1MB - private static final long DEFAULT_MAX_BYTES = 1048576; private static final String CONTENT_SIZE_ERROR = "Content size exceeds limit!"; private static final String RETRY_AFTER_HEADER_FIELD = "Retry-After"; private final int mConnectTimeoutMs; diff --git a/adservices/service-core/java/com/android/adservices/spe/AdservicesJobInfo.java b/adservices/service-core/java/com/android/adservices/spe/AdservicesJobInfo.java index 19575dc7db..465a41bdcf 100644 --- a/adservices/service-core/java/com/android/adservices/spe/AdservicesJobInfo.java +++ b/adservices/service-core/java/com/android/adservices/spe/AdservicesJobInfo.java @@ -72,7 +72,8 @@ public enum AdservicesJobInfo { MEASUREMENT_VERBOSE_DEBUG_REPORTING_FALLBACK_JOB( "MEASUREMENT_VERBOSE_DEBUG_REPORTING_FALLBACK_JOB", 24), - MEASUREMENT_DEBUG_REPORTING_FALLBACK_JOB("MEASUREMENT_DEBUG_REPORTING_FALLBACK_JOB", 25); + MEASUREMENT_DEBUG_REPORTING_FALLBACK_JOB("MEASUREMENT_DEBUG_REPORTING_FALLBACK_JOB", 25), + FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB("FLEDGE_AD_SELECTION_DEBUG_REPORT_SENDER_JOB", 26); private final String mJobServiceName; private final int mJobId; |