• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 package com.android.server.adservices.consent;
17 
18 
19 import android.annotation.NonNull;
20 import android.app.adservices.consent.ConsentParcel;
21 
22 import com.android.internal.annotations.VisibleForTesting;
23 import com.android.server.adservices.LogUtil;
24 import com.android.server.adservices.common.BooleanFileDatastore;
25 import com.android.server.adservices.feature.PrivacySandboxFeatureType;
26 
27 import java.io.File;
28 import java.io.IOException;
29 import java.util.Objects;
30 
31 /**
32  * Manager to handle user's consent. We will have one ConsentManager instance per user.
33  *
34  * @hide
35  */
36 public final class ConsentManager {
37     public static final String ERROR_MESSAGE_DATASTORE_EXCEPTION_WHILE_GET_CONTENT =
38             "getConsent method failed. Revoked consent is returned as fallback.";
39 
40     public static final String VERSION_KEY = "android.app.adservices.consent.VERSION";
41 
42     @VisibleForTesting
43     static final String NOTIFICATION_DISPLAYED_ONCE = "NOTIFICATION-DISPLAYED-ONCE";
44 
45     static final String GA_UX_NOTIFICATION_DISPLAYED_ONCE = "GA-UX-NOTIFICATION-DISPLAYED-ONCE";
46 
47     static final String TOPICS_CONSENT_PAGE_DISPLAYED = "TOPICS-CONSENT-PAGE-DISPLAYED";
48 
49     static final String FLEDGE_AND_MSMT_CONSENT_PAGE_DISPLAYED =
50             "FLDEGE-AND-MSMT-CONDENT-PAGE-DISPLAYED";
51 
52     private static final String CONSENT_API_TYPE_PREFIX = "CONSENT_API_TYPE_";
53 
54     // Deprecate this since we store each version in its own folder.
55     static final int STORAGE_VERSION = 1;
56     static final String STORAGE_XML_IDENTIFIER = "ConsentManagerStorageIdentifier.xml";
57 
58     private final BooleanFileDatastore mDatastore;
59 
60     @VisibleForTesting static final String DEFAULT_CONSENT = "DEFAULT_CONSENT";
61 
62     @VisibleForTesting static final String TOPICS_DEFAULT_CONSENT = "TOPICS_DEFAULT_CONSENT";
63 
64     @VisibleForTesting static final String FLEDGE_DEFAULT_CONSENT = "FLEDGE_DEFAULT_CONSENT";
65 
66     @VisibleForTesting
67     static final String MEASUREMENT_DEFAULT_CONSENT = "MEASUREMENT_DEFAULT_CONSENT";
68 
69     @VisibleForTesting static final String DEFAULT_AD_ID_STATE = "DEFAULT_AD_ID_STATE";
70 
71     @VisibleForTesting
72     static final String MANUAL_INTERACTION_WITH_CONSENT_RECORDED =
73             "MANUAL_INTERACTION_WITH_CONSENT_RECORDED";
74 
ConsentManager(@onNull BooleanFileDatastore datastore)75     private ConsentManager(@NonNull BooleanFileDatastore datastore) {
76         Objects.requireNonNull(datastore);
77 
78         mDatastore = datastore;
79     }
80 
81     /** Create a ConsentManager with base directory and for userIdentifier */
82     @NonNull
createConsentManager(@onNull String baseDir, int userIdentifier)83     public static ConsentManager createConsentManager(@NonNull String baseDir, int userIdentifier)
84             throws IOException {
85         Objects.requireNonNull(baseDir, "Base dir must be provided.");
86 
87         // The Data store is in folder with the following format.
88         // /data/system/adservices/user_id/consent/data_schema_version/
89         // Create the consent directory if needed.
90         String consentDataStoreDir =
91                 ConsentDatastoreLocationHelper.getConsentDataStoreDirAndCreateDir(
92                         baseDir, userIdentifier);
93 
94         BooleanFileDatastore datastore = createAndInitBooleanFileDatastore(consentDataStoreDir);
95 
96         return new ConsentManager(datastore);
97     }
98 
99     @NonNull
100     @VisibleForTesting
createAndInitBooleanFileDatastore(String consentDataStoreDir)101     static BooleanFileDatastore createAndInitBooleanFileDatastore(String consentDataStoreDir)
102             throws IOException {
103         // Create the DataStore and initialize it.
104         BooleanFileDatastore datastore =
105                 new BooleanFileDatastore(
106                         consentDataStoreDir, STORAGE_XML_IDENTIFIER, STORAGE_VERSION, VERSION_KEY);
107         datastore.initialize();
108         // TODO(b/259607624): implement a method in the datastore which would support
109         // this exact scenario - if the value is null, return default value provided
110         // in the parameter (similar to SP apply etc.)
111         if (datastore.get(NOTIFICATION_DISPLAYED_ONCE) == null) {
112             datastore.put(NOTIFICATION_DISPLAYED_ONCE, false);
113         }
114         if (datastore.get(GA_UX_NOTIFICATION_DISPLAYED_ONCE) == null) {
115             datastore.put(GA_UX_NOTIFICATION_DISPLAYED_ONCE, false);
116         }
117         if (datastore.get(TOPICS_CONSENT_PAGE_DISPLAYED) == null) {
118             datastore.put(TOPICS_CONSENT_PAGE_DISPLAYED, false);
119         }
120         if (datastore.get(FLEDGE_AND_MSMT_CONSENT_PAGE_DISPLAYED) == null) {
121             datastore.put(FLEDGE_AND_MSMT_CONSENT_PAGE_DISPLAYED, false);
122         }
123         return datastore;
124     }
125 
126     /** Retrieves the consent for all PP API services. */
getConsent(@onsentParcel.ConsentApiType int consentApiType)127     public ConsentParcel getConsent(@ConsentParcel.ConsentApiType int consentApiType) {
128         LogUtil.d("ConsentManager.getConsent() is invoked for consentApiType = " + consentApiType);
129 
130         synchronized (this) {
131             try {
132                 return new ConsentParcel.Builder()
133                         .setConsentApiType(consentApiType)
134                         .setIsGiven(mDatastore.get(getConsentApiTypeKey(consentApiType)))
135                         .build();
136             } catch (NullPointerException | IllegalArgumentException e) {
137                 LogUtil.e(e, ERROR_MESSAGE_DATASTORE_EXCEPTION_WHILE_GET_CONTENT);
138                 return ConsentParcel.createRevokedConsent(consentApiType);
139             }
140         }
141     }
142 
143     /** Set Consent */
setConsent(ConsentParcel consentParcel)144     public void setConsent(ConsentParcel consentParcel) throws IOException {
145         synchronized (this) {
146             mDatastore.put(
147                     getConsentApiTypeKey(consentParcel.getConsentApiType()),
148                     consentParcel.isIsGiven());
149             if (consentParcel.getConsentApiType() == ConsentParcel.ALL_API) {
150                 // Convert from 1 to 3 consents.
151                 mDatastore.put(
152                         getConsentApiTypeKey(ConsentParcel.TOPICS), consentParcel.isIsGiven());
153                 mDatastore.put(
154                         getConsentApiTypeKey(ConsentParcel.FLEDGE), consentParcel.isIsGiven());
155                 mDatastore.put(
156                         getConsentApiTypeKey(ConsentParcel.MEASUREMENT), consentParcel.isIsGiven());
157             } else {
158                 // Convert from 3 consents to 1 consent.
159                 if (mDatastore.get(
160                                 getConsentApiTypeKey(ConsentParcel.TOPICS), /* defaultValue */
161                                 false)
162                         && mDatastore.get(
163                                 getConsentApiTypeKey(ConsentParcel.FLEDGE), /* defaultValue */
164                                 false)
165                         && mDatastore.get(
166                                 getConsentApiTypeKey(ConsentParcel.MEASUREMENT), /* defaultValue */
167                                 false)) {
168                     mDatastore.put(getConsentApiTypeKey(ConsentParcel.ALL_API), true);
169                 } else {
170                     mDatastore.put(getConsentApiTypeKey(ConsentParcel.ALL_API), false);
171                 }
172             }
173         }
174     }
175 
176     /**
177      * Saves information to the storage that notification was displayed for the first time to the
178      * user.
179      */
recordNotificationDisplayed()180     public void recordNotificationDisplayed() throws IOException {
181         synchronized (this) {
182             try {
183                 // TODO(b/229725886): add metrics / logging
184                 mDatastore.put(NOTIFICATION_DISPLAYED_ONCE, true);
185             } catch (IOException e) {
186                 LogUtil.e(e, "Record notification failed due to IOException thrown by Datastore.");
187             }
188         }
189     }
190 
191     /**
192      * Returns information whether Consent Notification was displayed or not.
193      *
194      * @return true if Consent Notification was displayed, otherwise false.
195      */
wasNotificationDisplayed()196     public boolean wasNotificationDisplayed() {
197         synchronized (this) {
198             return mDatastore.get(NOTIFICATION_DISPLAYED_ONCE);
199         }
200     }
201 
202     /**
203      * Saves information to the storage that GA UX notification was displayed for the first time to
204      * the user.
205      */
recordGaUxNotificationDisplayed()206     public void recordGaUxNotificationDisplayed() throws IOException {
207         synchronized (this) {
208             try {
209                 // TODO(b/229725886): add metrics / logging
210                 mDatastore.put(GA_UX_NOTIFICATION_DISPLAYED_ONCE, true);
211             } catch (IOException e) {
212                 LogUtil.e(e, "Record notification failed due to IOException thrown by Datastore.");
213             }
214         }
215     }
216 
217     /**
218      * Returns information whether GA Ux Consent Notification was displayed or not.
219      *
220      * @return true if GA UX Consent Notification was displayed, otherwise false.
221      */
wasGaUxNotificationDisplayed()222     public boolean wasGaUxNotificationDisplayed() {
223         synchronized (this) {
224             Boolean displayed = mDatastore.get(GA_UX_NOTIFICATION_DISPLAYED_ONCE);
225             return displayed != null ? displayed : false;
226         }
227     }
228 
229     /** Saves the default consent of a user. */
recordDefaultConsent(boolean defaultConsent)230     public void recordDefaultConsent(boolean defaultConsent) throws IOException {
231         synchronized (this) {
232             try {
233                 mDatastore.put(DEFAULT_CONSENT, defaultConsent);
234             } catch (IOException e) {
235                 LogUtil.e(
236                         e,
237                         "Record default consent failed due to IOException thrown by Datastore: "
238                                 + e.getMessage());
239             }
240         }
241     }
242 
243     /** Saves the default topics consent of a user. */
recordTopicsDefaultConsent(boolean defaultConsent)244     public void recordTopicsDefaultConsent(boolean defaultConsent) throws IOException {
245         synchronized (this) {
246             try {
247                 mDatastore.put(TOPICS_DEFAULT_CONSENT, defaultConsent);
248             } catch (IOException e) {
249                 LogUtil.e(
250                         e,
251                         "Record topics default consent failed due to IOException thrown by"
252                                 + " Datastore: "
253                                 + e.getMessage());
254             }
255         }
256     }
257 
258     /** Saves the default FLEDGE consent of a user. */
recordFledgeDefaultConsent(boolean defaultConsent)259     public void recordFledgeDefaultConsent(boolean defaultConsent) throws IOException {
260         synchronized (this) {
261             try {
262                 mDatastore.put(FLEDGE_DEFAULT_CONSENT, defaultConsent);
263             } catch (IOException e) {
264                 LogUtil.e(
265                         e,
266                         "Record fledge default consent failed due to IOException thrown by"
267                                 + " Datastore: "
268                                 + e.getMessage());
269             }
270         }
271     }
272 
273     /** Saves the default measurement consent of a user. */
recordMeasurementDefaultConsent(boolean defaultConsent)274     public void recordMeasurementDefaultConsent(boolean defaultConsent) throws IOException {
275         synchronized (this) {
276             try {
277                 mDatastore.put(MEASUREMENT_DEFAULT_CONSENT, defaultConsent);
278             } catch (IOException e) {
279                 LogUtil.e(
280                         e,
281                         "Record measurement default consent failed due to IOException thrown by"
282                                 + " Datastore: "
283                                 + e.getMessage());
284             }
285         }
286     }
287 
288     /** Saves the default AdId state of a user. */
recordDefaultAdIdState(boolean defaultAdIdState)289     public void recordDefaultAdIdState(boolean defaultAdIdState) throws IOException {
290         synchronized (this) {
291             try {
292                 mDatastore.put(DEFAULT_AD_ID_STATE, defaultAdIdState);
293             } catch (IOException e) {
294                 LogUtil.e(
295                         e,
296                         "Record default AdId failed due to IOException thrown by Datastore: "
297                                 + e.getMessage());
298             }
299         }
300     }
301 
302     /** Saves the information whether the user interated manually with the consent. */
recordUserManualInteractionWithConsent(int interaction)303     public void recordUserManualInteractionWithConsent(int interaction) {
304         synchronized (this) {
305             try {
306                 switch (interaction) {
307                     case -1:
308                         mDatastore.put(MANUAL_INTERACTION_WITH_CONSENT_RECORDED, false);
309                         break;
310                     case 0:
311                         mDatastore.remove(MANUAL_INTERACTION_WITH_CONSENT_RECORDED);
312                         break;
313                     case 1:
314                         mDatastore.put(MANUAL_INTERACTION_WITH_CONSENT_RECORDED, true);
315                         break;
316                     default:
317                         throw new IllegalArgumentException(
318                                 String.format(
319                                         "InteractionId < %d > can not be handled.", interaction));
320                 }
321             } catch (IOException e) {
322                 LogUtil.e(
323                         e,
324                         "Record manual interaction with consent failed due to IOException thrown"
325                                 + " by Datastore: "
326                                 + e.getMessage());
327             }
328         }
329     }
330 
331     /** Returns information whether user interacted with consent manually. */
getUserManualInteractionWithConsent()332     public int getUserManualInteractionWithConsent() {
333         synchronized (this) {
334             Boolean userManualInteractionWithConsent =
335                     mDatastore.get(MANUAL_INTERACTION_WITH_CONSENT_RECORDED);
336             if (userManualInteractionWithConsent == null) {
337                 return 0;
338             } else if (Boolean.TRUE.equals(userManualInteractionWithConsent)) {
339                 return 1;
340             } else {
341                 return -1;
342             }
343         }
344     }
345 
346     /**
347      * Returns the default consent state.
348      *
349      * @return true if default consent is given, otherwise false.
350      */
getDefaultConsent()351     public boolean getDefaultConsent() {
352         synchronized (this) {
353             Boolean defaultConsent = mDatastore.get(DEFAULT_CONSENT);
354             return defaultConsent != null ? defaultConsent : false;
355         }
356     }
357 
358     /**
359      * Returns the topics default consent state.
360      *
361      * @return true if topics default consent is given, otherwise false.
362      */
getTopicsDefaultConsent()363     public boolean getTopicsDefaultConsent() {
364         synchronized (this) {
365             Boolean topicsDefaultConsent = mDatastore.get(TOPICS_DEFAULT_CONSENT);
366             return topicsDefaultConsent != null ? topicsDefaultConsent : false;
367         }
368     }
369 
370     /**
371      * Returns the FLEDGE default consent state.
372      *
373      * @return true if default consent is given, otherwise false.
374      */
getFledgeDefaultConsent()375     public boolean getFledgeDefaultConsent() {
376         synchronized (this) {
377             Boolean fledgeDefaultConsent = mDatastore.get(DEFAULT_CONSENT);
378             return fledgeDefaultConsent != null ? fledgeDefaultConsent : false;
379         }
380     }
381 
382     /**
383      * Returns the measurement default consent state.
384      *
385      * @return true if default consent is given, otherwise false.
386      */
getMeasurementDefaultConsent()387     public boolean getMeasurementDefaultConsent() {
388         synchronized (this) {
389             Boolean measurementDefaultConsent = mDatastore.get(DEFAULT_CONSENT);
390             return measurementDefaultConsent != null ? measurementDefaultConsent : false;
391         }
392     }
393 
394     /**
395      * Returns the default AdId state when consent notification was sent.
396      *
397      * @return true if AdId is enabled by default, otherwise false.
398      */
getDefaultAdIdState()399     public boolean getDefaultAdIdState() {
400         synchronized (this) {
401             Boolean defaultAdIdState = mDatastore.get(DEFAULT_AD_ID_STATE);
402             return defaultAdIdState != null ? defaultAdIdState : false;
403         }
404     }
405 
406     /** Set the current enabled privacy sandbox feature. */
setCurrentPrivacySandboxFeature(String currentFeatureType)407     public void setCurrentPrivacySandboxFeature(String currentFeatureType) {
408         synchronized (this) {
409             for (PrivacySandboxFeatureType featureType : PrivacySandboxFeatureType.values()) {
410                 try {
411                     if (featureType.name().equals(currentFeatureType)) {
412                         mDatastore.put(featureType.name(), true);
413                     } else {
414                         mDatastore.put(featureType.name(), false);
415                     }
416                 } catch (IOException e) {
417                     LogUtil.e(
418                             "IOException caught while saving privacy sandbox feature."
419                                     + e.getMessage());
420                 }
421             }
422         }
423     }
424 
425     /** Returns whether a privacy sandbox feature is enabled. */
isPrivacySandboxFeatureEnabled(PrivacySandboxFeatureType featureType)426     public boolean isPrivacySandboxFeatureEnabled(PrivacySandboxFeatureType featureType) {
427         synchronized (this) {
428             Boolean isFeatureEnabled = mDatastore.get(featureType.name());
429             return isFeatureEnabled != null ? isFeatureEnabled : false;
430         }
431     }
432 
433     /**
434      * Deletes the user directory which contains consent information present at
435      * /data/system/adservices/user_id
436      */
deleteUserDirectory(File dir)437     public boolean deleteUserDirectory(File dir) throws IOException {
438         synchronized (this) {
439             boolean success = true;
440             File[] files = dir.listFiles();
441             // files will be null if dir is not a directory
442             if (files != null) {
443                 for (File file : files) {
444                     if (!deleteUserDirectory(file)) {
445                         LogUtil.d("Failed to delete " + file);
446                         success = false;
447                     }
448                 }
449             }
450             return success && dir.delete();
451         }
452     }
453 
454     @VisibleForTesting
getConsentApiTypeKey(@onsentParcel.ConsentApiType int consentApiType)455     String getConsentApiTypeKey(@ConsentParcel.ConsentApiType int consentApiType) {
456         return CONSENT_API_TYPE_PREFIX + consentApiType;
457     }
458 
459     /** tearDown method used for Testing only. */
460     @VisibleForTesting
tearDownForTesting()461     public void tearDownForTesting() {
462         synchronized (this) {
463             mDatastore.tearDownForTesting();
464         }
465     }
466 
467     @VisibleForTesting static final String IS_AD_ID_ENABLED = "IS_AD_ID_ENABLED";
468 
469     /** Returns whether the isAdIdEnabled bit is true. */
isAdIdEnabled()470     public boolean isAdIdEnabled() {
471         synchronized (this) {
472             Boolean isAdIdEnabled = mDatastore.get(IS_AD_ID_ENABLED);
473             return isAdIdEnabled != null ? isAdIdEnabled : false;
474         }
475     }
476 
477     /** Set the AdIdEnabled bit in system server. */
setAdIdEnabled(boolean isAdIdEnabled)478     public void setAdIdEnabled(boolean isAdIdEnabled) throws IOException {
479         synchronized (this) {
480             try {
481                 mDatastore.put(IS_AD_ID_ENABLED, isAdIdEnabled);
482             } catch (IOException e) {
483                 LogUtil.e(e, "setAdIdEnabled operation failed: " + e.getMessage());
484             }
485         }
486     }
487 
488     @VisibleForTesting static final String IS_U18_ACCOUNT = "IS_U18_ACCOUNT";
489 
490     /** Returns whether the isU18Account bit is true. */
isU18Account()491     public boolean isU18Account() {
492         synchronized (this) {
493             Boolean isU18Account = mDatastore.get(IS_U18_ACCOUNT);
494             return isU18Account != null ? isU18Account : false;
495         }
496     }
497 
498     /** Set the U18Account bit in system server. */
setU18Account(boolean isU18Account)499     public void setU18Account(boolean isU18Account) throws IOException {
500         synchronized (this) {
501             try {
502                 mDatastore.put(IS_U18_ACCOUNT, isU18Account);
503             } catch (IOException e) {
504                 LogUtil.e(e, "setU18Account operation failed: " + e.getMessage());
505             }
506         }
507     }
508 
509     @VisibleForTesting static final String IS_ENTRY_POINT_ENABLED = "IS_ENTRY_POINT_ENABLED";
510 
511     /** Returns whether the isEntryPointEnabled bit is true. */
isEntryPointEnabled()512     public boolean isEntryPointEnabled() {
513         synchronized (this) {
514             Boolean isEntryPointEnabled = mDatastore.get(IS_ENTRY_POINT_ENABLED);
515             return isEntryPointEnabled != null ? isEntryPointEnabled : false;
516         }
517     }
518 
519     /** Set the EntryPointEnabled bit in system server. */
setEntryPointEnabled(boolean isEntryPointEnabled)520     public void setEntryPointEnabled(boolean isEntryPointEnabled) throws IOException {
521         synchronized (this) {
522             try {
523                 mDatastore.put(IS_ENTRY_POINT_ENABLED, isEntryPointEnabled);
524             } catch (IOException e) {
525                 LogUtil.e(e, "setEntryPointEnabled operation failed: " + e.getMessage());
526             }
527         }
528     }
529 
530     @VisibleForTesting static final String IS_ADULT_ACCOUNT = "IS_ADULT_ACCOUNT";
531 
532     /** Returns whether the isAdultAccount bit is true. */
isAdultAccount()533     public boolean isAdultAccount() {
534         synchronized (this) {
535             Boolean isAdultAccount = mDatastore.get(IS_ADULT_ACCOUNT);
536             return isAdultAccount != null ? isAdultAccount : false;
537         }
538     }
539 
540     /** Set the AdultAccount bit in system server. */
setAdultAccount(boolean isAdultAccount)541     public void setAdultAccount(boolean isAdultAccount) throws IOException {
542         synchronized (this) {
543             try {
544                 mDatastore.put(IS_ADULT_ACCOUNT, isAdultAccount);
545             } catch (IOException e) {
546                 LogUtil.e(e, "setAdultAccount operation failed: " + e.getMessage());
547             }
548         }
549     }
550 
551     @VisibleForTesting
552     static final String WAS_U18_NOTIFICATION_DISPLAYED = "WAS_U18_NOTIFICATION_DISPLAYED";
553 
554     /** Returns whether the wasU18NotificationDisplayed bit is true. */
wasU18NotificationDisplayed()555     public boolean wasU18NotificationDisplayed() {
556         synchronized (this) {
557             Boolean wasU18NotificationDisplayed = mDatastore.get(WAS_U18_NOTIFICATION_DISPLAYED);
558             return wasU18NotificationDisplayed != null ? wasU18NotificationDisplayed : false;
559         }
560     }
561 
562     /** Set the U18NotificationDisplayed bit in system server. */
setU18NotificationDisplayed(boolean wasU18NotificationDisplayed)563     public void setU18NotificationDisplayed(boolean wasU18NotificationDisplayed)
564             throws IOException {
565         synchronized (this) {
566             try {
567                 mDatastore.put(WAS_U18_NOTIFICATION_DISPLAYED, wasU18NotificationDisplayed);
568             } catch (IOException e) {
569                 LogUtil.e(e, "setU18NotificationDisplayed operation failed: " + e.getMessage());
570             }
571         }
572     }
573 }
574