blob: 42912284b0d2543238942eb061ab49987d3ebc57 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.adservices.consent;
import android.annotation.NonNull;
import android.app.adservices.consent.ConsentParcel;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.adservices.LogUtil;
import com.android.server.adservices.common.BooleanFileDatastore;
import com.android.server.adservices.feature.PrivacySandboxFeatureType;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
/**
* Manager to handle user's consent. We will have one ConsentManager instance per user.
*
* @hide
*/
public final class ConsentManager {
public static final String ERROR_MESSAGE_DATASTORE_EXCEPTION_WHILE_GET_CONTENT =
"getConsent method failed. Revoked consent is returned as fallback.";
public static final String VERSION_KEY = "android.app.adservices.consent.VERSION";
@VisibleForTesting
static final String NOTIFICATION_DISPLAYED_ONCE = "NOTIFICATION-DISPLAYED-ONCE";
static final String GA_UX_NOTIFICATION_DISPLAYED_ONCE = "GA-UX-NOTIFICATION-DISPLAYED-ONCE";
static final String TOPICS_CONSENT_PAGE_DISPLAYED = "TOPICS-CONSENT-PAGE-DISPLAYED";
static final String FLEDGE_AND_MSMT_CONSENT_PAGE_DISPLAYED =
"FLDEGE-AND-MSMT-CONDENT-PAGE-DISPLAYED";
private static final String CONSENT_API_TYPE_PREFIX = "CONSENT_API_TYPE_";
// Deprecate this since we store each version in its own folder.
static final int STORAGE_VERSION = 1;
static final String STORAGE_XML_IDENTIFIER = "ConsentManagerStorageIdentifier.xml";
private final BooleanFileDatastore mDatastore;
@VisibleForTesting static final String DEFAULT_CONSENT = "DEFAULT_CONSENT";
@VisibleForTesting static final String TOPICS_DEFAULT_CONSENT = "TOPICS_DEFAULT_CONSENT";
@VisibleForTesting static final String FLEDGE_DEFAULT_CONSENT = "FLEDGE_DEFAULT_CONSENT";
@VisibleForTesting
static final String MEASUREMENT_DEFAULT_CONSENT = "MEASUREMENT_DEFAULT_CONSENT";
@VisibleForTesting static final String DEFAULT_AD_ID_STATE = "DEFAULT_AD_ID_STATE";
@VisibleForTesting
static final String MANUAL_INTERACTION_WITH_CONSENT_RECORDED =
"MANUAL_INTERACTION_WITH_CONSENT_RECORDED";
private ConsentManager(@NonNull BooleanFileDatastore datastore) {
Objects.requireNonNull(datastore);
mDatastore = datastore;
}
/** Create a ConsentManager with base directory and for userIdentifier */
@NonNull
public static ConsentManager createConsentManager(@NonNull String baseDir, int userIdentifier)
throws IOException {
Objects.requireNonNull(baseDir, "Base dir must be provided.");
// The Data store is in folder with the following format.
// /data/system/adservices/user_id/consent/data_schema_version/
// Create the consent directory if needed.
String consentDataStoreDir =
ConsentDatastoreLocationHelper.getConsentDataStoreDirAndCreateDir(
baseDir, userIdentifier);
BooleanFileDatastore datastore = createAndInitBooleanFileDatastore(consentDataStoreDir);
return new ConsentManager(datastore);
}
@NonNull
@VisibleForTesting
static BooleanFileDatastore createAndInitBooleanFileDatastore(String consentDataStoreDir)
throws IOException {
// Create the DataStore and initialize it.
BooleanFileDatastore datastore =
new BooleanFileDatastore(
consentDataStoreDir, STORAGE_XML_IDENTIFIER, STORAGE_VERSION, VERSION_KEY);
datastore.initialize();
// TODO(b/259607624): implement a method in the datastore which would support
// this exact scenario - if the value is null, return default value provided
// in the parameter (similar to SP apply etc.)
if (datastore.get(NOTIFICATION_DISPLAYED_ONCE) == null) {
datastore.put(NOTIFICATION_DISPLAYED_ONCE, false);
}
if (datastore.get(GA_UX_NOTIFICATION_DISPLAYED_ONCE) == null) {
datastore.put(GA_UX_NOTIFICATION_DISPLAYED_ONCE, false);
}
if (datastore.get(TOPICS_CONSENT_PAGE_DISPLAYED) == null) {
datastore.put(TOPICS_CONSENT_PAGE_DISPLAYED, false);
}
if (datastore.get(FLEDGE_AND_MSMT_CONSENT_PAGE_DISPLAYED) == null) {
datastore.put(FLEDGE_AND_MSMT_CONSENT_PAGE_DISPLAYED, false);
}
return datastore;
}
/** Retrieves the consent for all PP API services. */
public ConsentParcel getConsent(@ConsentParcel.ConsentApiType int consentApiType) {
LogUtil.d("ConsentManager.getConsent() is invoked for consentApiType = " + consentApiType);
synchronized (this) {
try {
return new ConsentParcel.Builder()
.setConsentApiType(consentApiType)
.setIsGiven(mDatastore.get(getConsentApiTypeKey(consentApiType)))
.build();
} catch (NullPointerException | IllegalArgumentException e) {
LogUtil.e(e, ERROR_MESSAGE_DATASTORE_EXCEPTION_WHILE_GET_CONTENT);
return ConsentParcel.createRevokedConsent(consentApiType);
}
}
}
/** Set Consent */
public void setConsent(ConsentParcel consentParcel) throws IOException {
synchronized (this) {
mDatastore.put(
getConsentApiTypeKey(consentParcel.getConsentApiType()),
consentParcel.isIsGiven());
if (consentParcel.getConsentApiType() == ConsentParcel.ALL_API) {
// Convert from 1 to 3 consents.
mDatastore.put(
getConsentApiTypeKey(ConsentParcel.TOPICS), consentParcel.isIsGiven());
mDatastore.put(
getConsentApiTypeKey(ConsentParcel.FLEDGE), consentParcel.isIsGiven());
mDatastore.put(
getConsentApiTypeKey(ConsentParcel.MEASUREMENT), consentParcel.isIsGiven());
} else {
// Convert from 3 consents to 1 consent.
if (mDatastore.get(
getConsentApiTypeKey(ConsentParcel.TOPICS), /* defaultValue */
false)
&& mDatastore.get(
getConsentApiTypeKey(ConsentParcel.FLEDGE), /* defaultValue */
false)
&& mDatastore.get(
getConsentApiTypeKey(ConsentParcel.MEASUREMENT), /* defaultValue */
false)) {
mDatastore.put(getConsentApiTypeKey(ConsentParcel.ALL_API), true);
} else {
mDatastore.put(getConsentApiTypeKey(ConsentParcel.ALL_API), false);
}
}
}
}
/**
* Saves information to the storage that notification was displayed for the first time to the
* user.
*/
public void recordNotificationDisplayed() throws IOException {
synchronized (this) {
try {
// TODO(b/229725886): add metrics / logging
mDatastore.put(NOTIFICATION_DISPLAYED_ONCE, true);
} catch (IOException e) {
LogUtil.e(e, "Record notification failed due to IOException thrown by Datastore.");
}
}
}
/**
* Returns information whether Consent Notification was displayed or not.
*
* @return true if Consent Notification was displayed, otherwise false.
*/
public boolean wasNotificationDisplayed() {
synchronized (this) {
return mDatastore.get(NOTIFICATION_DISPLAYED_ONCE);
}
}
/**
* Saves information to the storage that GA UX notification was displayed for the first time to
* the user.
*/
public void recordGaUxNotificationDisplayed() throws IOException {
synchronized (this) {
try {
// TODO(b/229725886): add metrics / logging
mDatastore.put(GA_UX_NOTIFICATION_DISPLAYED_ONCE, true);
} catch (IOException e) {
LogUtil.e(e, "Record notification failed due to IOException thrown by Datastore.");
}
}
}
/**
* Returns information whether GA Ux Consent Notification was displayed or not.
*
* @return true if GA UX Consent Notification was displayed, otherwise false.
*/
public boolean wasGaUxNotificationDisplayed() {
synchronized (this) {
Boolean displayed = mDatastore.get(GA_UX_NOTIFICATION_DISPLAYED_ONCE);
return displayed != null ? displayed : false;
}
}
/** Saves the default consent of a user. */
public void recordDefaultConsent(boolean defaultConsent) throws IOException {
synchronized (this) {
try {
mDatastore.put(DEFAULT_CONSENT, defaultConsent);
} catch (IOException e) {
LogUtil.e(
e,
"Record default consent failed due to IOException thrown by Datastore: "
+ e.getMessage());
}
}
}
/** Saves the default topics consent of a user. */
public void recordTopicsDefaultConsent(boolean defaultConsent) throws IOException {
synchronized (this) {
try {
mDatastore.put(TOPICS_DEFAULT_CONSENT, defaultConsent);
} catch (IOException e) {
LogUtil.e(
e,
"Record topics default consent failed due to IOException thrown by"
+ " Datastore: "
+ e.getMessage());
}
}
}
/** Saves the default FLEDGE consent of a user. */
public void recordFledgeDefaultConsent(boolean defaultConsent) throws IOException {
synchronized (this) {
try {
mDatastore.put(FLEDGE_DEFAULT_CONSENT, defaultConsent);
} catch (IOException e) {
LogUtil.e(
e,
"Record fledge default consent failed due to IOException thrown by"
+ " Datastore: "
+ e.getMessage());
}
}
}
/** Saves the default measurement consent of a user. */
public void recordMeasurementDefaultConsent(boolean defaultConsent) throws IOException {
synchronized (this) {
try {
mDatastore.put(MEASUREMENT_DEFAULT_CONSENT, defaultConsent);
} catch (IOException e) {
LogUtil.e(
e,
"Record measurement default consent failed due to IOException thrown by"
+ " Datastore: "
+ e.getMessage());
}
}
}
/** Saves the default AdId state of a user. */
public void recordDefaultAdIdState(boolean defaultAdIdState) throws IOException {
synchronized (this) {
try {
mDatastore.put(DEFAULT_AD_ID_STATE, defaultAdIdState);
} catch (IOException e) {
LogUtil.e(
e,
"Record default AdId failed due to IOException thrown by Datastore: "
+ e.getMessage());
}
}
}
/** Saves the information whether the user interated manually with the consent. */
public void recordUserManualInteractionWithConsent(int interaction) {
synchronized (this) {
try {
switch (interaction) {
case -1:
mDatastore.put(MANUAL_INTERACTION_WITH_CONSENT_RECORDED, false);
break;
case 0:
mDatastore.remove(MANUAL_INTERACTION_WITH_CONSENT_RECORDED);
break;
case 1:
mDatastore.put(MANUAL_INTERACTION_WITH_CONSENT_RECORDED, true);
break;
default:
throw new IllegalArgumentException(
String.format(
"InteractionId < %d > can not be handled.", interaction));
}
} catch (IOException e) {
LogUtil.e(
e,
"Record manual interaction with consent failed due to IOException thrown"
+ " by Datastore: "
+ e.getMessage());
}
}
}
/** Returns information whether user interacted with consent manually. */
public int getUserManualInteractionWithConsent() {
synchronized (this) {
Boolean userManualInteractionWithConsent =
mDatastore.get(MANUAL_INTERACTION_WITH_CONSENT_RECORDED);
if (userManualInteractionWithConsent == null) {
return 0;
} else if (Boolean.TRUE.equals(userManualInteractionWithConsent)) {
return 1;
} else {
return -1;
}
}
}
/**
* Returns the default consent state.
*
* @return true if default consent is given, otherwise false.
*/
public boolean getDefaultConsent() {
synchronized (this) {
Boolean defaultConsent = mDatastore.get(DEFAULT_CONSENT);
return defaultConsent != null ? defaultConsent : false;
}
}
/**
* Returns the topics default consent state.
*
* @return true if topics default consent is given, otherwise false.
*/
public boolean getTopicsDefaultConsent() {
synchronized (this) {
Boolean topicsDefaultConsent = mDatastore.get(TOPICS_DEFAULT_CONSENT);
return topicsDefaultConsent != null ? topicsDefaultConsent : false;
}
}
/**
* Returns the FLEDGE default consent state.
*
* @return true if default consent is given, otherwise false.
*/
public boolean getFledgeDefaultConsent() {
synchronized (this) {
Boolean fledgeDefaultConsent = mDatastore.get(DEFAULT_CONSENT);
return fledgeDefaultConsent != null ? fledgeDefaultConsent : false;
}
}
/**
* Returns the measurement default consent state.
*
* @return true if default consent is given, otherwise false.
*/
public boolean getMeasurementDefaultConsent() {
synchronized (this) {
Boolean measurementDefaultConsent = mDatastore.get(DEFAULT_CONSENT);
return measurementDefaultConsent != null ? measurementDefaultConsent : false;
}
}
/**
* Returns the default AdId state when consent notification was sent.
*
* @return true if AdId is enabled by default, otherwise false.
*/
public boolean getDefaultAdIdState() {
synchronized (this) {
Boolean defaultAdIdState = mDatastore.get(DEFAULT_AD_ID_STATE);
return defaultAdIdState != null ? defaultAdIdState : false;
}
}
/** Set the current enabled privacy sandbox feature. */
public void setCurrentPrivacySandboxFeature(String currentFeatureType) {
synchronized (this) {
for (PrivacySandboxFeatureType featureType : PrivacySandboxFeatureType.values()) {
try {
if (featureType.name().equals(currentFeatureType)) {
mDatastore.put(featureType.name(), true);
} else {
mDatastore.put(featureType.name(), false);
}
} catch (IOException e) {
LogUtil.e(
"IOException caught while saving privacy sandbox feature."
+ e.getMessage());
}
}
}
}
/** Returns whether a privacy sandbox feature is enabled. */
public boolean isPrivacySandboxFeatureEnabled(PrivacySandboxFeatureType featureType) {
synchronized (this) {
Boolean isFeatureEnabled = mDatastore.get(featureType.name());
return isFeatureEnabled != null ? isFeatureEnabled : false;
}
}
/**
* Deletes the user directory which contains consent information present at
* /data/system/adservices/user_id
*/
public boolean deleteUserDirectory(File dir) throws IOException {
synchronized (this) {
boolean success = true;
File[] files = dir.listFiles();
// files will be null if dir is not a directory
if (files != null) {
for (File file : files) {
if (!deleteUserDirectory(file)) {
LogUtil.d("Failed to delete " + file);
success = false;
}
}
}
return success && dir.delete();
}
}
@VisibleForTesting
String getConsentApiTypeKey(@ConsentParcel.ConsentApiType int consentApiType) {
return CONSENT_API_TYPE_PREFIX + consentApiType;
}
/** tearDown method used for Testing only. */
@VisibleForTesting
public void tearDownForTesting() {
synchronized (this) {
mDatastore.tearDownForTesting();
}
}
@VisibleForTesting static final String IS_U18_ACCOUNT = "IS_U18_ACCOUNT";
/** Returns whether the isU18Account bit is true. */
public boolean isU18Account() {
synchronized (this) {
Boolean isU18Account = mDatastore.get(IS_U18_ACCOUNT);
return isU18Account != null ? isU18Account : false;
}
}
/** Set the U18Account bit in system server. */
public void setU18Account(boolean isU18Account) throws IOException {
synchronized (this) {
try {
mDatastore.put(IS_U18_ACCOUNT, isU18Account);
} catch (IOException e) {
LogUtil.e(e, "setU18Account operation failed: " + e.getMessage());
}
}
}
@VisibleForTesting static final String IS_ENTRY_POINT_ENABLED = "IS_ENTRY_POINT_ENABLED";
/** Returns whether the isEntryPointEnabled bit is true. */
public boolean isEntryPointEnabled() {
synchronized (this) {
Boolean isEntryPointEnabled = mDatastore.get(IS_ENTRY_POINT_ENABLED);
return isEntryPointEnabled != null ? isEntryPointEnabled : false;
}
}
/** Set the EntryPointEnabled bit in system server. */
public void setEntryPointEnabled(boolean isEntryPointEnabled) throws IOException {
synchronized (this) {
try {
mDatastore.put(IS_ENTRY_POINT_ENABLED, isEntryPointEnabled);
} catch (IOException e) {
LogUtil.e(e, "setEntryPointEnabled operation failed: " + e.getMessage());
}
}
}
@VisibleForTesting static final String IS_ADULT_ACCOUNT = "IS_ADULT_ACCOUNT";
/** Returns whether the isAdultAccount bit is true. */
public boolean isAdultAccount() {
synchronized (this) {
Boolean isAdultAccount = mDatastore.get(IS_ADULT_ACCOUNT);
return isAdultAccount != null ? isAdultAccount : false;
}
}
/** Set the AdultAccount bit in system server. */
public void setAdultAccount(boolean isAdultAccount) throws IOException {
synchronized (this) {
try {
mDatastore.put(IS_ADULT_ACCOUNT, isAdultAccount);
} catch (IOException e) {
LogUtil.e(e, "setAdultAccount operation failed: " + e.getMessage());
}
}
}
}