1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.adservices.service.adselection;
18 
19 import android.annotation.NonNull;
20 import android.content.Context;
21 import android.net.Uri;
22 
23 import com.android.adservices.LoggerFactory;
24 import com.android.adservices.concurrency.AdServicesExecutors;
25 import com.android.adservices.data.adselection.AdSelectionDebugReportDao;
26 import com.android.adservices.data.adselection.AdSelectionDebugReportingDatabase;
27 import com.android.adservices.data.adselection.DBAdSelectionDebugReport;
28 import com.android.adservices.service.Flags;
29 import com.android.adservices.service.FlagsFactory;
30 import com.android.adservices.service.common.SingletonRunner;
31 import com.android.adservices.service.common.httpclient.AdServicesHttpsClient;
32 import com.android.adservices.service.devapi.DevContext;
33 import com.android.internal.annotations.VisibleForTesting;
34 
35 import com.google.common.collect.ImmutableList;
36 import com.google.common.util.concurrent.FluentFuture;
37 import com.google.common.util.concurrent.Futures;
38 import com.google.common.util.concurrent.ListenableFuture;
39 
40 import java.time.Clock;
41 import java.time.Instant;
42 import java.util.Collections;
43 import java.util.List;
44 import java.util.Objects;
45 import java.util.concurrent.TimeUnit;
46 import java.util.function.Supplier;
47 import java.util.stream.Collectors;
48 
49 /** Worker class to send and clean debug reports generated for ad selection. */
50 public class DebugReportSenderWorker {
51     private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
52     public static final String JOB_DESCRIPTION = "Ad selection debug report sender job";
53     private static final Object SINGLETON_LOCK = new Object();
54     private static volatile DebugReportSenderWorker sDebugReportSenderWorker;
55     @NonNull private final AdSelectionDebugReportDao mAdSelectionDebugReportDao;
56     @NonNull private final AdServicesHttpsClient mAdServicesHttpsClient;
57     @NonNull private final Flags mFlags;
58     @NonNull private final Clock mClock;
59     private final SingletonRunner<Void> mSingletonRunner =
60             new SingletonRunner<>(JOB_DESCRIPTION, this::doRun);
61 
62     @VisibleForTesting
DebugReportSenderWorker( @onNull AdSelectionDebugReportDao adSelectionDebugReportDao, @NonNull AdServicesHttpsClient adServicesHttpsClient, @NonNull Flags flags, @NonNull Clock clock)63     protected DebugReportSenderWorker(
64             @NonNull AdSelectionDebugReportDao adSelectionDebugReportDao,
65             @NonNull AdServicesHttpsClient adServicesHttpsClient,
66             @NonNull Flags flags,
67             @NonNull Clock clock) {
68         Objects.requireNonNull(adSelectionDebugReportDao);
69         Objects.requireNonNull(adServicesHttpsClient);
70         Objects.requireNonNull(flags);
71         Objects.requireNonNull(clock);
72 
73         mAdSelectionDebugReportDao = adSelectionDebugReportDao;
74         mAdServicesHttpsClient = adServicesHttpsClient;
75         mClock = clock;
76         mFlags = flags;
77     }
78 
79     /**
80      * Gets an instance of a {@link DebugReportSenderWorker}. If an instance hasn't been
81      * initialized, a new singleton will be created and returned.
82      */
83     @NonNull
getInstance(@onNull Context context)84     public static DebugReportSenderWorker getInstance(@NonNull Context context) {
85         Objects.requireNonNull(context);
86 
87         if (sDebugReportSenderWorker == null) {
88             synchronized (SINGLETON_LOCK) {
89                 if (sDebugReportSenderWorker == null) {
90                     AdSelectionDebugReportDao adSelectionDebugReportDao =
91                             AdSelectionDebugReportingDatabase.getInstance(context)
92                                     .getAdSelectionDebugReportDao();
93                     Flags flags = FlagsFactory.getFlags();
94                     AdServicesHttpsClient adServicesHttpsClient =
95                             new AdServicesHttpsClient(
96                                     AdServicesExecutors.getBlockingExecutor(),
97                                     flags.getFledgeDebugReportSenderJobNetworkConnectionTimeoutMs(),
98                                     flags.getFledgeDebugReportSenderJobNetworkReadTimeoutMs(),
99                                     AdServicesHttpsClient.DEFAULT_MAX_BYTES);
100                     sDebugReportSenderWorker =
101                             new DebugReportSenderWorker(
102                                     adSelectionDebugReportDao,
103                                     adServicesHttpsClient,
104                                     flags,
105                                     Clock.systemUTC());
106                 }
107             }
108         }
109         return sDebugReportSenderWorker;
110     }
111 
112     /**
113      * Runs the debug report sender job for Ad Selection Debug Reports.
114      *
115      * @return A future to be used to check when the task has completed.
116      */
runDebugReportSender()117     public FluentFuture<Void> runDebugReportSender() {
118         sLogger.d("Starting %s", JOB_DESCRIPTION);
119         return mSingletonRunner.runSingleInstance();
120     }
121 
122     /** Requests that any ongoing work be stopped gracefully and waits for work to be stopped. */
stopWork()123     public void stopWork() {
124         mSingletonRunner.stopWork();
125     }
126 
getDebugReports( @onNull Supplier<Boolean> shouldStop, @NonNull Instant jobStartTime)127     private FluentFuture<List<DBAdSelectionDebugReport>> getDebugReports(
128             @NonNull Supplier<Boolean> shouldStop, @NonNull Instant jobStartTime) {
129         if (shouldStop.get()) {
130             sLogger.d("Stopping " + JOB_DESCRIPTION);
131             return FluentFuture.from(Futures.immediateFuture(ImmutableList.of()));
132         }
133         int batchSizeForDebugReports = mFlags.getFledgeEventLevelDebugReportingMaxItemsPerBatch();
134         sLogger.v("Getting %d debug reports from database", batchSizeForDebugReports);
135         return FluentFuture.from(
136                 AdServicesExecutors.getBackgroundExecutor()
137                         .submit(
138                                 () -> {
139                                     List<DBAdSelectionDebugReport> debugReports =
140                                             mAdSelectionDebugReportDao.getDebugReportsBeforeTime(
141                                                     jobStartTime, batchSizeForDebugReports);
142                                     if (debugReports == null) {
143                                         sLogger.v("no debug reports to send");
144                                         return Collections.emptyList();
145                                     }
146                                     sLogger.v(
147                                             "found %d debug reports from database",
148                                             debugReports.size());
149                                     return debugReports;
150                                 }));
151     }
152 
cleanupDebugReportsData(Instant jobStartTime)153     private FluentFuture<Void> cleanupDebugReportsData(Instant jobStartTime) {
154         sLogger.v(
155                 "cleaning up old debug reports from the database at time %s",
156                 jobStartTime.toString());
157         return FluentFuture.from(
158                 AdServicesExecutors.getBackgroundExecutor()
159                         .submit(
160                                 () -> {
161                                     mAdSelectionDebugReportDao.deleteDebugReportsBeforeTime(
162                                             jobStartTime);
163                                     return null;
164                                 }));
165     }
166 
167     private ListenableFuture<Void> sendDebugReports(
168             @NonNull List<DBAdSelectionDebugReport> dbAdSelectionDebugReports) {
169 
170         if (dbAdSelectionDebugReports.isEmpty()) {
171             sLogger.d("No debug reports found to send");
172             return FluentFuture.from(Futures.immediateVoidFuture());
173         }
174 
175         sLogger.d("Sending %d debug reports", dbAdSelectionDebugReports.size());
176         List<ListenableFuture<Void>> futures =
177                 dbAdSelectionDebugReports.stream()
178                         .map(this::sendDebugReport)
179                         .collect(Collectors.toList());
180         return Futures.whenAllComplete(futures)
181                 .call(() -> null, AdServicesExecutors.getBlockingExecutor());
182     }
183 
184     private ListenableFuture<Void> sendDebugReport(
185             DBAdSelectionDebugReport dbAdSelectionDebugReport) {
186         Uri debugReportUri = dbAdSelectionDebugReport.getDebugReportUri();
187         DevContext devContext =
188                 DevContext.builder()
189                         .setDevOptionsEnabled(dbAdSelectionDebugReport.getDevOptionsEnabled())
190                         .build();
191         sLogger.v("Sending debug report %s", debugReportUri.toString());
192         try {
193             return mAdServicesHttpsClient.getAndReadNothing(debugReportUri, devContext);
194         } catch (Exception ignored) {
195             sLogger.v("Failed to send debug report %s", debugReportUri.toString());
196             return Futures.immediateVoidFuture();
197         }
198     }
199 
200     private FluentFuture<Void> doRun(@NonNull Supplier<Boolean> shouldStop) {
201         Instant jobStartTime = mClock.instant();
202         return getDebugReports(shouldStop, jobStartTime)
203                 .transform(this::sendDebugReports, AdServicesExecutors.getBackgroundExecutor())
204                 .transformAsync(
205                         ignored -> cleanupDebugReportsData(jobStartTime),
206                         AdServicesExecutors.getBackgroundExecutor())
207                 .withTimeout(
208                         mFlags.getFledgeDebugReportSenderJobMaxRuntimeMs(),
209                         TimeUnit.MILLISECONDS,
210                         AdServicesExecutors.getScheduler());
211     }
212 }
213