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