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