1 /*
2  * Copyright (C) 2022 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.customaudience;
18 
19 import static android.adservices.common.AdServicesStatusUtils.STATUS_INTERNAL_ERROR;
20 import static android.adservices.common.AdServicesStatusUtils.STATUS_SUCCESS;
21 
22 import static com.android.adservices.service.stats.AdServicesLoggerUtil.FIELD_UNSET;
23 
24 import android.adservices.common.AdTechIdentifier;
25 import android.annotation.NonNull;
26 import android.content.pm.PackageManager;
27 import android.net.Uri;
28 import android.util.Pair;
29 
30 import com.android.adservices.LoggerFactory;
31 import com.android.adservices.concurrency.AdServicesExecutors;
32 import com.android.adservices.data.adselection.AppInstallDao;
33 import com.android.adservices.data.customaudience.CustomAudienceDao;
34 import com.android.adservices.data.customaudience.CustomAudienceStats;
35 import com.android.adservices.data.customaudience.DBCustomAudienceBackgroundFetchData;
36 import com.android.adservices.data.enrollment.EnrollmentDao;
37 import com.android.adservices.service.Flags;
38 import com.android.adservices.service.common.httpclient.AdServicesHttpsClient;
39 import com.android.adservices.service.devapi.DevContext;
40 import com.android.adservices.service.stats.CustomAudienceLoggerFactory;
41 import com.android.adservices.service.stats.UpdateCustomAudienceExecutionLogger;
42 
43 import com.google.common.util.concurrent.FluentFuture;
44 
45 import java.io.IOException;
46 import java.nio.charset.StandardCharsets;
47 import java.time.Instant;
48 import java.util.Objects;
49 import java.util.concurrent.CancellationException;
50 
51 /** Runner executing actual background fetch tasks. */
52 public class BackgroundFetchRunner {
53     private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
54     private final CustomAudienceDao mCustomAudienceDao;
55     private final AppInstallDao mAppInstallDao;
56     private final PackageManager mPackageManager;
57     private final EnrollmentDao mEnrollmentDao;
58     private final Flags mFlags;
59     private final CustomAudienceLoggerFactory mCustomAudienceLoggerFactory;
60     private final AdServicesHttpsClient mHttpsClient;
61 
BackgroundFetchRunner( @onNull CustomAudienceDao customAudienceDao, @NonNull AppInstallDao appInstallDao, @NonNull PackageManager packageManager, @NonNull EnrollmentDao enrollmentDao, @NonNull Flags flags, @NonNull CustomAudienceLoggerFactory customAudienceLoggerFactory)62     public BackgroundFetchRunner(
63             @NonNull CustomAudienceDao customAudienceDao,
64             @NonNull AppInstallDao appInstallDao,
65             @NonNull PackageManager packageManager,
66             @NonNull EnrollmentDao enrollmentDao,
67             @NonNull Flags flags,
68             @NonNull CustomAudienceLoggerFactory customAudienceLoggerFactory) {
69         Objects.requireNonNull(customAudienceDao);
70         Objects.requireNonNull(appInstallDao);
71         Objects.requireNonNull(packageManager);
72         Objects.requireNonNull(enrollmentDao);
73         Objects.requireNonNull(flags);
74         Objects.requireNonNull(customAudienceLoggerFactory);
75         mCustomAudienceDao = customAudienceDao;
76         mAppInstallDao = appInstallDao;
77         mPackageManager = packageManager;
78         mEnrollmentDao = enrollmentDao;
79         mFlags = flags;
80         mCustomAudienceLoggerFactory = customAudienceLoggerFactory;
81         mHttpsClient =
82                 new AdServicesHttpsClient(
83                         AdServicesExecutors.getBlockingExecutor(),
84                         flags.getFledgeBackgroundFetchNetworkConnectTimeoutMs(),
85                         flags.getFledgeBackgroundFetchNetworkReadTimeoutMs(),
86                         flags.getFledgeBackgroundFetchMaxResponseSizeB());
87     }
88 
89     /**
90      * Deletes custom audiences whose expiration timestamps have passed.
91      *
92      * <p>Also clears corresponding update information from the background fetch DB.
93      */
deleteExpiredCustomAudiences(@onNull Instant jobStartTime)94     public void deleteExpiredCustomAudiences(@NonNull Instant jobStartTime) {
95         Objects.requireNonNull(jobStartTime);
96 
97         sLogger.d("Starting expired custom audience garbage collection");
98         int numCustomAudiencesDeleted =
99                 mCustomAudienceDao.deleteAllExpiredCustomAudienceData(jobStartTime);
100         sLogger.d("Deleted %d expired custom audiences", numCustomAudiencesDeleted);
101     }
102 
103     /**
104      * Deletes custom audiences whose owner applications which are not installed or in the app
105      * allowlist.
106      *
107      * <p>Also clears corresponding update information from the background fetch table.
108      */
deleteDisallowedOwnerCustomAudiences()109     public void deleteDisallowedOwnerCustomAudiences() {
110         sLogger.d("Starting custom audience disallowed owner garbage collection");
111         CustomAudienceStats deletedCAStats =
112                 mCustomAudienceDao.deleteAllDisallowedOwnerCustomAudienceData(
113                         mPackageManager, mFlags);
114         sLogger.d(
115                 "Deleted %d custom audiences belonging to %d disallowed owner apps",
116                 deletedCAStats.getTotalCustomAudienceCount(), deletedCAStats.getTotalOwnerCount());
117     }
118 
119     /**
120      * Deletes app install data whose packages are not installed or are not in the app allowlist.
121      */
deleteDisallowedPackageAppInstallEntries()122     public void deleteDisallowedPackageAppInstallEntries() {
123         sLogger.d("Starting app install disallowed package garbage collection");
124         int numDeleted = mAppInstallDao.deleteAllDisallowedPackageEntries(mPackageManager, mFlags);
125         sLogger.d("Deleted %d app install entries", numDeleted);
126     }
127 
128     /**
129      * Deletes custom audiences whose buyer ad techs which are not enrolled to use FLEDGE.
130      *
131      * <p>Also clears corresponding update information from the background fetch table.
132      */
deleteDisallowedBuyerCustomAudiences()133     public void deleteDisallowedBuyerCustomAudiences() {
134         sLogger.d("Starting custom audience disallowed buyer garbage collection");
135         CustomAudienceStats deletedCAStats =
136                 mCustomAudienceDao.deleteAllDisallowedBuyerCustomAudienceData(
137                         mEnrollmentDao, mFlags);
138         sLogger.d(
139                 "Deleted %d custom audiences belonging to %d disallowed buyer ad techs",
140                 deletedCAStats.getTotalCustomAudienceCount(), deletedCAStats.getTotalBuyerCount());
141     }
142 
143     /** Updates a single given custom audience and persists the results. */
updateCustomAudience( @onNull Instant jobStartTime, @NonNull final DBCustomAudienceBackgroundFetchData fetchData)144     public FluentFuture<UpdateResultType> updateCustomAudience(
145             @NonNull Instant jobStartTime,
146             @NonNull final DBCustomAudienceBackgroundFetchData fetchData) {
147         Objects.requireNonNull(jobStartTime);
148         Objects.requireNonNull(fetchData);
149 
150         UpdateCustomAudienceExecutionLogger updateCustomAudienceExecutionLogger =
151                 mCustomAudienceLoggerFactory.getUpdateCustomAudienceExecutionLogger();
152 
153         updateCustomAudienceExecutionLogger.start();
154 
155         return fetchAndValidateCustomAudienceUpdatableData(
156                         jobStartTime,
157                         fetchData.getBuyer(),
158                         fetchData.getDailyUpdateUri(),
159                         fetchData.getIsDebuggable())
160                 .transform(
161                         updatableData -> {
162                             int numOfAds = FIELD_UNSET;
163                             int adsDataSizeInBytes = FIELD_UNSET;
164                             int resultCode;
165 
166                             DBCustomAudienceBackgroundFetchData updatedData =
167                                     fetchData.copyWithUpdatableData(updatableData);
168 
169                             if (updatableData.getContainsSuccessfulUpdate()) {
170                                 mCustomAudienceDao.updateCustomAudienceAndBackgroundFetchData(
171                                         updatedData, updatableData);
172                                 if (Objects.nonNull(updatableData.getAds())) {
173                                     numOfAds = updatableData.getAds().size();
174                                     adsDataSizeInBytes =
175                                             updatableData
176                                                     .getAds()
177                                                     .toString()
178                                                     .getBytes(StandardCharsets.UTF_8)
179                                                     .length;
180                                 }
181                                 resultCode = STATUS_SUCCESS;
182                             } else {
183                                 // In a failed update, we don't need to update the main CA table, so
184                                 // only update the background fetch table
185                                 mCustomAudienceDao.persistCustomAudienceBackgroundFetchData(
186                                         updatedData);
187                                 resultCode = STATUS_INTERNAL_ERROR;
188                             }
189 
190                             try {
191                                 updateCustomAudienceExecutionLogger.close(
192                                         adsDataSizeInBytes, numOfAds, resultCode);
193                             } catch (Exception e) {
194                                 sLogger.d(
195                                         "Error when closing updateCustomAudienceExecutionLogger, "
196                                                 + "skipping metrics logging: %s",
197                                         e.getMessage());
198                             }
199 
200                             return updatableData.getInitialUpdateResult();
201                         },
202                         AdServicesExecutors.getBackgroundExecutor());
203     }
204 
205     /**
206      * Fetches the custom audience update from the given daily update URI and validates the response
207      * in a {@link CustomAudienceUpdatableData} object.
208      */
209     @NonNull
fetchAndValidateCustomAudienceUpdatableData( @onNull Instant jobStartTime, @NonNull AdTechIdentifier buyer, @NonNull Uri dailyFetchUri, boolean isDebuggable)210     public FluentFuture<CustomAudienceUpdatableData> fetchAndValidateCustomAudienceUpdatableData(
211             @NonNull Instant jobStartTime,
212             @NonNull AdTechIdentifier buyer,
213             @NonNull Uri dailyFetchUri,
214             boolean isDebuggable) {
215         Objects.requireNonNull(jobStartTime);
216         Objects.requireNonNull(buyer);
217         Objects.requireNonNull(dailyFetchUri);
218 
219         // If a CA was created in a debuggable context, set the developer context to assume
220         // developer options are enabled (as they were at the time of CA creation).
221         // This allows for testing against localhost servers outside the context of a binder
222         // connection from a debuggable app.
223         DevContext devContext =
224                 isDebuggable
225                         ? DevContext.builder().setDevOptionsEnabled(true).build()
226                         : DevContext.createForDevOptionsDisabled();
227 
228         // TODO(b/234884352): Perform k-anon check on daily fetch URI
229         return FluentFuture.from(mHttpsClient.fetchPayload(dailyFetchUri, devContext))
230                 .transform(
231                         updateResponse ->
232                                 Pair.create(
233                                         UpdateResultType.SUCCESS, updateResponse.getResponseBody()),
234                         AdServicesExecutors.getBackgroundExecutor())
235                 .catching(
236                         Throwable.class,
237                         t -> handleThrowable(t, dailyFetchUri),
238                         AdServicesExecutors.getBackgroundExecutor())
239                 .transform(
240                         fetchResultAndResponse ->
241                                 CustomAudienceUpdatableData.createFromResponseString(
242                                         jobStartTime,
243                                         buyer,
244                                         fetchResultAndResponse.first,
245                                         fetchResultAndResponse.second,
246                                         mFlags),
247                         AdServicesExecutors.getBackgroundExecutor());
248     }
249 
handleThrowable( Throwable t, @NonNull Uri dailyFetchUri)250     private Pair<UpdateResultType, String> handleThrowable(
251             Throwable t, @NonNull Uri dailyFetchUri) {
252         if (t instanceof IOException) {
253             // TODO(b/237342352): Investigate separating connect and read timeouts
254             sLogger.e(
255                     t,
256                     "Timed out while fetching custom audience update from %s",
257                     dailyFetchUri.toSafeString());
258             return Pair.create(UpdateResultType.NETWORK_FAILURE, "{}");
259         }
260         if (t instanceof CancellationException) {
261             sLogger.e(
262                     t,
263                     "Custom audience update cancelled while fetching from %s",
264                     dailyFetchUri.toSafeString());
265             return Pair.create(UpdateResultType.UNKNOWN, "{}");
266         }
267 
268         sLogger.e(
269                 t,
270                 "Encountered unexpected error while fetching custom audience update from" + " %s",
271                 dailyFetchUri.toSafeString());
272         return Pair.create(UpdateResultType.UNKNOWN, "{}");
273     }
274 
275     /** Represents the result of an update attempt prior to parsing the update response. */
276     public enum UpdateResultType {
277         SUCCESS,
278         UNKNOWN,
279         K_ANON_FAILURE,
280         // TODO(b/237342352): Consolidate if we don't need to distinguish network timeouts
281         NETWORK_FAILURE,
282         NETWORK_READ_TIMEOUT_FAILURE,
283         RESPONSE_VALIDATION_FAILURE
284     }
285 }
286