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.devapi; 18 19 import android.adservices.adselection.AdSelectionConfig; 20 import android.adservices.adselection.AdSelectionFromOutcomesConfig; 21 import android.adservices.adselection.PerBuyerDecisionLogic; 22 import android.adservices.common.AdSelectionSignals; 23 import android.adservices.common.AdTechIdentifier; 24 import android.annotation.NonNull; 25 26 import androidx.annotation.Nullable; 27 28 import com.android.adservices.data.adselection.AdSelectionEntryDao; 29 import com.android.adservices.data.adselection.DBAdSelectionFromOutcomesOverride; 30 import com.android.adservices.data.adselection.DBAdSelectionOverride; 31 import com.android.adservices.data.adselection.DBBuyerDecisionOverride; 32 33 import com.google.common.hash.HashFunction; 34 import com.google.common.hash.Hasher; 35 import com.google.common.hash.Hashing; 36 37 import java.util.List; 38 import java.util.Map; 39 import java.util.Objects; 40 import java.util.stream.Collectors; 41 42 /** Helper class to support the runtime retrieval of dev overrides for the AdSelection API. */ 43 public class AdSelectionDevOverridesHelper { 44 private static final HashFunction sHashFunction = Hashing.murmur3_128(); 45 private static final String API_NOT_AUTHORIZED_MSG = 46 "This API is not enabled for the given app because either dev options are disabled or" 47 + " the app is not debuggable."; 48 49 private final DevContext mDevContext; 50 private final AdSelectionEntryDao mAdSelectionEntryDao; 51 52 /** 53 * Creates an instance of {@link AdSelectionDevOverridesHelper} with the given {@link 54 * DevContext} and {@link AdSelectionEntryDao}. 55 */ AdSelectionDevOverridesHelper( @onNull DevContext devContext, @NonNull AdSelectionEntryDao adSelectionEntryDao)56 public AdSelectionDevOverridesHelper( 57 @NonNull DevContext devContext, @NonNull AdSelectionEntryDao adSelectionEntryDao) { 58 Objects.requireNonNull(devContext); 59 Objects.requireNonNull(adSelectionEntryDao); 60 61 this.mDevContext = devContext; 62 this.mAdSelectionEntryDao = adSelectionEntryDao; 63 } 64 65 /** 66 * @return a low-collision ID for the given {@link AdSelectionConfig} instance. We are accepting 67 * collision since this is a developer targeted feature and the collision should be low rate 68 * enough not to constitute a serious issue. 69 */ calculateAdSelectionConfigId( @onNull AdSelectionConfig adSelectionConfig)70 public static String calculateAdSelectionConfigId( 71 @NonNull AdSelectionConfig adSelectionConfig) { 72 // See go/hashing#java 73 Hasher hasher = sHashFunction.newHasher(); 74 hasher.putUnencodedChars(adSelectionConfig.getSeller().toString()) 75 .putUnencodedChars(adSelectionConfig.getDecisionLogicUri().toString()) 76 .putUnencodedChars(adSelectionConfig.getAdSelectionSignals().toString()) 77 .putUnencodedChars(adSelectionConfig.getSellerSignals().toString()); 78 79 adSelectionConfig.getCustomAudienceBuyers().stream() 80 .map(AdTechIdentifier::toString) 81 .forEach(hasher::putUnencodedChars); 82 adSelectionConfig.getPerBuyerSignals().entrySet().stream() 83 .forEach( 84 buyerAndSignals -> { 85 hasher.putUnencodedChars(buyerAndSignals.getKey().toString()) 86 .putUnencodedChars(buyerAndSignals.getValue().toString()); 87 }); 88 return hasher.hash().toString(); 89 } 90 91 /** 92 * @return a low-collision ID for the given {@link AdSelectionConfig} instance. We are accepting 93 * collision since this is a developer targeted feature and the collision should be low rate 94 * enough not to constitute a serious issue. 95 */ calculateAdSelectionFromOutcomesConfigId( @onNull AdSelectionFromOutcomesConfig adSelectionFromOutcomesConfig)96 public static String calculateAdSelectionFromOutcomesConfigId( 97 @NonNull AdSelectionFromOutcomesConfig adSelectionFromOutcomesConfig) { 98 // See go/hashing#java 99 Hasher hasher = sHashFunction.newHasher(); 100 hasher.putUnencodedChars(adSelectionFromOutcomesConfig.getSelectionLogicUri().toString()) 101 .putUnencodedChars(adSelectionFromOutcomesConfig.getSelectionSignals().toString()); 102 return hasher.hash().toString(); 103 } 104 105 /** 106 * Looks for an override for the given {@link AdSelectionConfig}. Will return {@code null} if 107 * {@link DevContext#getDevOptionsEnabled()} returns null for the {@link DevContext} passed in 108 * the constructor or if there is no override created by the app with package name specified in 109 * {@link DevContext#getCallingAppPackageName()}. 110 */ 111 @Nullable getDecisionLogicOverride(@onNull AdSelectionConfig adSelectionConfig)112 public String getDecisionLogicOverride(@NonNull AdSelectionConfig adSelectionConfig) { 113 Objects.requireNonNull(adSelectionConfig); 114 115 if (!mDevContext.getDevOptionsEnabled()) { 116 return null; 117 } 118 return mAdSelectionEntryDao.getDecisionLogicOverride( 119 calculateAdSelectionConfigId(adSelectionConfig), 120 mDevContext.getCallingAppPackageName()); 121 } 122 123 /** 124 * Looks for an override for the given {@link AdSelectionConfig}. Will return {@code null} if 125 * {@link DevContext#getDevOptionsEnabled()} returns null for the {@link DevContext} passed in 126 * the constructor or if there is no override created by the app with package name specified in 127 * {@link DevContext#getCallingAppPackageName()}. 128 */ 129 @Nullable getPerBuyerDecisionLogicOverride( @onNull AdSelectionConfig adSelectionConfig)130 public Map<AdTechIdentifier, String> getPerBuyerDecisionLogicOverride( 131 @NonNull AdSelectionConfig adSelectionConfig) { 132 Objects.requireNonNull(adSelectionConfig); 133 134 if (!mDevContext.getDevOptionsEnabled()) { 135 return null; 136 } 137 return mAdSelectionEntryDao 138 .getPerBuyerDecisionLogicOverride( 139 calculateAdSelectionConfigId(adSelectionConfig), 140 mDevContext.getCallingAppPackageName()) 141 .stream() 142 .collect( 143 Collectors.toMap( 144 DBBuyerDecisionOverride::getBuyer, 145 DBBuyerDecisionOverride::getDecisionLogic)); 146 } 147 148 /** 149 * Looks for an override for the given {@link AdSelectionConfig}. Will return {@code null} if 150 * {@link DevContext#getDevOptionsEnabled()} returns false for the {@link DevContext} passed in 151 * the constructor or if there is no override created by the app with package name specified in 152 * {@link DevContext#getCallingAppPackageName()}. 153 */ 154 @Nullable getTrustedScoringSignalsOverride( @onNull AdSelectionConfig adSelectionConfig)155 public AdSelectionSignals getTrustedScoringSignalsOverride( 156 @NonNull AdSelectionConfig adSelectionConfig) { 157 Objects.requireNonNull(adSelectionConfig); 158 159 if (!mDevContext.getDevOptionsEnabled()) { 160 return null; 161 } 162 String overrideSignals = 163 mAdSelectionEntryDao.getTrustedScoringSignalsOverride( 164 calculateAdSelectionConfigId(adSelectionConfig), 165 mDevContext.getCallingAppPackageName()); 166 return overrideSignals == null ? null : AdSelectionSignals.fromString(overrideSignals); 167 } 168 169 /** 170 * Adds an override of the {@code decisionLogicJS} along with {@link 171 * DevContext#getCallingAppPackageName()} for the given {@link AdSelectionConfig}. 172 * 173 * @throws SecurityException if{@link DevContext#getDevOptionsEnabled()} returns false for the 174 * {@link DevContext} 175 */ addAdSelectionSellerOverride( @onNull AdSelectionConfig adSelectionConfig, @NonNull String decisionLogicJS, @NonNull AdSelectionSignals trustedScoringSignals, @NonNull PerBuyerDecisionLogic perBuyerDecisionLogic)176 public void addAdSelectionSellerOverride( 177 @NonNull AdSelectionConfig adSelectionConfig, 178 @NonNull String decisionLogicJS, 179 @NonNull AdSelectionSignals trustedScoringSignals, 180 @NonNull PerBuyerDecisionLogic perBuyerDecisionLogic) { 181 Objects.requireNonNull(adSelectionConfig); 182 Objects.requireNonNull(decisionLogicJS); 183 184 if (!mDevContext.getDevOptionsEnabled()) { 185 throw new SecurityException(API_NOT_AUTHORIZED_MSG); 186 } 187 final String adSelectionConfigId = calculateAdSelectionConfigId(adSelectionConfig); 188 mAdSelectionEntryDao.persistAdSelectionOverride( 189 DBAdSelectionOverride.builder() 190 .setAdSelectionConfigId(adSelectionConfigId) 191 .setAppPackageName(mDevContext.getCallingAppPackageName()) 192 .setDecisionLogicJS(decisionLogicJS) 193 .setTrustedScoringSignals(trustedScoringSignals.toString()) 194 .build()); 195 196 List<DBBuyerDecisionOverride> dbBuyerDecisionOverrideList = 197 perBuyerDecisionLogic.getPerBuyerLogicMap().entrySet().stream() 198 .map( 199 x -> 200 DBBuyerDecisionOverride.builder() 201 .setBuyer(x.getKey()) 202 .setDecisionLogic(x.getValue().getLogic()) 203 .setAdSelectionConfigId(adSelectionConfigId) 204 .setAppPackageName( 205 mDevContext.getCallingAppPackageName()) 206 .build()) 207 .collect(Collectors.toList()); 208 mAdSelectionEntryDao.persistPerBuyerDecisionLogicOverride(dbBuyerDecisionOverrideList); 209 } 210 211 /** 212 * Removes an override for the given {@link AdSelectionConfig}. 213 * 214 * @throws SecurityException if{@link DevContext#getDevOptionsEnabled()} returns false for the 215 * {@link DevContext} 216 */ removeAdSelectionSellerOverride(@onNull AdSelectionConfig adSelectionConfig)217 public void removeAdSelectionSellerOverride(@NonNull AdSelectionConfig adSelectionConfig) { 218 Objects.requireNonNull(adSelectionConfig); 219 220 if (!mDevContext.getDevOptionsEnabled()) { 221 throw new SecurityException(API_NOT_AUTHORIZED_MSG); 222 } 223 224 String adSelectionConfigId = calculateAdSelectionConfigId(adSelectionConfig); 225 String appPackageName = mDevContext.getCallingAppPackageName(); 226 227 mAdSelectionEntryDao.removeAdSelectionOverrideByIdAndPackageName( 228 adSelectionConfigId, appPackageName); 229 mAdSelectionEntryDao.removeBuyerDecisionLogicOverrideByIdAndPackageName( 230 adSelectionConfigId, appPackageName); 231 } 232 233 /** 234 * Removes all ad selection overrides that match {@link DevContext#getCallingAppPackageName()}. 235 * 236 * @throws SecurityException if{@link DevContext#getDevOptionsEnabled()} returns false for the 237 * {@link DevContext} 238 */ removeAllDecisionLogicOverrides()239 public void removeAllDecisionLogicOverrides() { 240 if (!mDevContext.getDevOptionsEnabled()) { 241 throw new SecurityException(API_NOT_AUTHORIZED_MSG); 242 } 243 244 mAdSelectionEntryDao.removeAllAdSelectionOverrides(mDevContext.getCallingAppPackageName()); 245 mAdSelectionEntryDao.removeAllBuyerDecisionOverrides( 246 mDevContext.getCallingAppPackageName()); 247 } 248 249 /** 250 * Looks for an override for the given {@link AdSelectionFromOutcomesConfig}. Will return {@code 251 * null} if {@link DevContext#getDevOptionsEnabled()} returns false for the {@link DevContext} 252 * passed in the constructor or if there is no override created by the app with package name 253 * specified in {@link DevContext#getCallingAppPackageName()}. 254 */ 255 @Nullable getSelectionLogicOverride(@onNull AdSelectionFromOutcomesConfig config)256 public String getSelectionLogicOverride(@NonNull AdSelectionFromOutcomesConfig config) { 257 Objects.requireNonNull(config); 258 259 if (!mDevContext.getDevOptionsEnabled()) { 260 return null; 261 } 262 return mAdSelectionEntryDao.getSelectionLogicOverride( 263 calculateAdSelectionFromOutcomesConfigId(config), 264 mDevContext.getCallingAppPackageName()); 265 } 266 267 /** 268 * Looks for an override for the given {@link AdSelectionFromOutcomesConfig}. Will return {@code 269 * null} if {@link DevContext#getDevOptionsEnabled()} returns false for the {@link DevContext} 270 * passed in the constructor or if there is no override created by the app with package name 271 * specified in {@link DevContext#getCallingAppPackageName()}. 272 */ 273 @Nullable getSelectionSignalsOverride( @onNull AdSelectionFromOutcomesConfig config)274 public AdSelectionSignals getSelectionSignalsOverride( 275 @NonNull AdSelectionFromOutcomesConfig config) { 276 Objects.requireNonNull(config); 277 278 if (!mDevContext.getDevOptionsEnabled()) { 279 return null; 280 } 281 String overrideSignals = 282 mAdSelectionEntryDao.getSelectionSignalsOverride( 283 calculateAdSelectionFromOutcomesConfigId(config), 284 mDevContext.getCallingAppPackageName()); 285 return overrideSignals == null ? null : AdSelectionSignals.fromString(overrideSignals); 286 } 287 288 /** 289 * Adds an override of the {@code decisionLogicJS} along with {@link 290 * DevContext#getCallingAppPackageName()} for the given {@link AdSelectionConfig}. 291 * 292 * @throws SecurityException if{@link DevContext#getDevOptionsEnabled()} returns false for the 293 * {@link DevContext} 294 */ addAdSelectionOutcomeSelectorOverride( @onNull AdSelectionFromOutcomesConfig adSelectionFromOutcomesConfig, @NonNull String selectionLogicJs, @NonNull AdSelectionSignals selectionSignals)295 public void addAdSelectionOutcomeSelectorOverride( 296 @NonNull AdSelectionFromOutcomesConfig adSelectionFromOutcomesConfig, 297 @NonNull String selectionLogicJs, 298 @NonNull AdSelectionSignals selectionSignals) { 299 Objects.requireNonNull(adSelectionFromOutcomesConfig); 300 Objects.requireNonNull(selectionLogicJs); 301 Objects.requireNonNull(selectionSignals); 302 303 if (!mDevContext.getDevOptionsEnabled()) { 304 throw new SecurityException(API_NOT_AUTHORIZED_MSG); 305 } 306 mAdSelectionEntryDao.persistAdSelectionFromOutcomesOverride( 307 DBAdSelectionFromOutcomesOverride.builder() 308 .setAdSelectionFromOutcomesConfigId( 309 calculateAdSelectionFromOutcomesConfigId( 310 adSelectionFromOutcomesConfig)) 311 .setAppPackageName(mDevContext.getCallingAppPackageName()) 312 .setSelectionLogicJs(selectionLogicJs) 313 .setSelectionSignals(selectionSignals.toString()) 314 .build()); 315 } 316 317 /** 318 * Removes an override for the given {@link AdSelectionFromOutcomesConfig}. 319 * 320 * @throws SecurityException if{@link DevContext#getDevOptionsEnabled()} returns false for the 321 * {@link DevContext} 322 */ removeAdSelectionOutcomeSelectorOverride( @onNull AdSelectionFromOutcomesConfig config)323 public void removeAdSelectionOutcomeSelectorOverride( 324 @NonNull AdSelectionFromOutcomesConfig config) { 325 Objects.requireNonNull(config); 326 327 if (!mDevContext.getDevOptionsEnabled()) { 328 throw new SecurityException(API_NOT_AUTHORIZED_MSG); 329 } 330 331 String adSelectionConfigId = calculateAdSelectionFromOutcomesConfigId(config); 332 String appPackageName = mDevContext.getCallingAppPackageName(); 333 334 mAdSelectionEntryDao.removeAdSelectionFromOutcomesOverrideByIdAndPackageName( 335 adSelectionConfigId, appPackageName); 336 } 337 338 /** 339 * Removes all ad selection from outcomes overrides that match {@link DevContext 340 * #getCallingAppPackageName()}. 341 * 342 * @throws SecurityException if{@link DevContext#getDevOptionsEnabled()} returns false for the 343 * {@link DevContext} 344 */ removeAllSelectionLogicOverrides()345 public void removeAllSelectionLogicOverrides() { 346 if (!mDevContext.getDevOptionsEnabled()) { 347 throw new SecurityException(API_NOT_AUTHORIZED_MSG); 348 } 349 350 mAdSelectionEntryDao.removeAllAdSelectionFromOutcomesOverrides( 351 mDevContext.getCallingAppPackageName()); 352 } 353 } 354