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