summaryrefslogtreecommitdiff
path: root/adservices/service-core/java/com/android/adservices/service/consent
diff options
context:
space:
mode:
Diffstat (limited to 'adservices/service-core/java/com/android/adservices/service/consent')
-rw-r--r--adservices/service-core/java/com/android/adservices/service/consent/AdServicesStorageManager.java16
-rw-r--r--adservices/service-core/java/com/android/adservices/service/consent/AppConsentForRStorageManager.java266
-rw-r--r--adservices/service-core/java/com/android/adservices/service/consent/AppConsentStorageManager.java119
-rw-r--r--adservices/service-core/java/com/android/adservices/service/consent/ConsentCompositeStorage.java658
-rw-r--r--adservices/service-core/java/com/android/adservices/service/consent/ConsentConstants.java4
-rw-r--r--adservices/service-core/java/com/android/adservices/service/consent/ConsentManager.java31
-rw-r--r--adservices/service-core/java/com/android/adservices/service/consent/ConsentManagerV2.java1445
-rw-r--r--adservices/service-core/java/com/android/adservices/service/consent/ConsentMigrationUtils.java193
-rw-r--r--adservices/service-core/java/com/android/adservices/service/consent/IConsentStorage.java26
9 files changed, 2509 insertions, 249 deletions
diff --git a/adservices/service-core/java/com/android/adservices/service/consent/AdServicesStorageManager.java b/adservices/service-core/java/com/android/adservices/service/consent/AdServicesStorageManager.java
index c32e727024..f3aa866833 100644
--- a/adservices/service-core/java/com/android/adservices/service/consent/AdServicesStorageManager.java
+++ b/adservices/service-core/java/com/android/adservices/service/consent/AdServicesStorageManager.java
@@ -125,13 +125,20 @@ public final class AdServicesStorageManager implements IConsentStorage {
@Override
public AdServicesApiConsent getConsent(AdServicesApiType apiType) {
int consentApiType = apiType.toConsentApiType();
- return AdServicesApiConsent.getConsent(
- mAdServicesManager.getConsent(consentApiType).isIsGiven());
+ ConsentParcel consentParcel = mAdServicesManager.getConsent(consentApiType);
+ if (consentParcel == null) {
+ return AdServicesApiConsent.REVOKED;
+ }
+ return AdServicesApiConsent.getConsent(consentParcel.isIsGiven());
}
/** Returns the current privacy sandbox feature. */
@Override
- public PrivacySandboxFeatureType getCurrentPrivacySandboxFeature() {
+ public PrivacySandboxFeatureType getCurrentPrivacySandboxFeature() throws IOException {
+ if (mAdServicesManager == null
+ || mAdServicesManager.getCurrentPrivacySandboxFeature() == null) {
+ return PrivacySandboxFeatureType.PRIVACY_SANDBOX_UNSUPPORTED;
+ }
return PrivacySandboxFeatureType.valueOf(
mAdServicesManager.getCurrentPrivacySandboxFeature());
}
@@ -434,7 +441,8 @@ public final class AdServicesStorageManager implements IConsentStorage {
return mAdServicesManager.wasU18NotificationDisplayed();
}
- private PrivacySandboxUxCollection convertUxString(String uxString) {
+ private PrivacySandboxUxCollection convertUxString(@NonNull String uxString) {
+ Objects.requireNonNull(uxString);
return Stream.of(PrivacySandboxUxCollection.values())
.filter(ux -> uxString.equals(ux.toString()))
.findFirst()
diff --git a/adservices/service-core/java/com/android/adservices/service/consent/AppConsentForRStorageManager.java b/adservices/service-core/java/com/android/adservices/service/consent/AppConsentForRStorageManager.java
new file mode 100644
index 0000000000..259b57cbde
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/service/consent/AppConsentForRStorageManager.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2023 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.adservices.service.consent;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.adservices.data.common.BooleanFileDatastore;
+import com.android.adservices.data.consent.AppConsentDao;
+import com.android.adservices.service.extdata.AdServicesExtDataStorageServiceManager;
+import com.android.adservices.service.ui.data.UxStatesDao;
+
+import com.google.common.collect.ImmutableList;
+
+import java.io.IOException;
+
+/**
+ * AppConsentStorageManager to handle user's consent related Apis in Android R.
+ *
+ * <p>It shares similarities with AppConsentStorageManager's logic, but adds additional storage
+ * functionality specific to AdServicesExtDataStorageServiceManager.
+ *
+ * <p>Used in PPAPI_AND_ADEXT_SERVICE
+ */
+@RequiresApi(Build.VERSION_CODES.S)
+public class AppConsentForRStorageManager extends AppConsentStorageManager {
+
+ private final AdServicesExtDataStorageServiceManager mAdExtDataManager;
+ /**
+ * Constructor of AppConsentForRStorageManager
+ *
+ * @param datastore stores consent
+ * @param appConsentDao mostly used by FLEDGE
+ * @param uxStatesDao stores ux related data
+ */
+ public AppConsentForRStorageManager(
+ BooleanFileDatastore datastore,
+ AppConsentDao appConsentDao,
+ UxStatesDao uxStatesDao,
+ AdServicesExtDataStorageServiceManager adExtDataManager) {
+ super(datastore, appConsentDao, uxStatesDao);
+ this.mAdExtDataManager = adExtDataManager;
+ }
+
+ /** Clear ConsentForUninstalledApp, not support for Measurement. */
+ @Override
+ public void clearAllAppConsentData() {
+ // PPAPI_AND_ADEXT_SERVICE is only set on R which supports only
+ // Measurement.
+ throw new IllegalStateException(
+ getAdExtExceptionMessage(/* illegalAction= */ "reset consent for apps"));
+ }
+
+ /** Clear ConsentForUninstalledApp, not support for Measurement. */
+ @Override
+ public void clearConsentForUninstalledApp(String packageName) {
+ throw new IllegalStateException(
+ getAdExtExceptionMessage(/* illegalAction= */ "clear consent for uninstalled app"));
+ }
+
+ /** Clear ConsentForUninstalledApp, not support for Measurement. */
+ @Override
+ public void clearConsentForUninstalledApp(String packageName, int packageUid) {
+ throw new IllegalStateException(
+ getAdExtExceptionMessage(/* illegalAction= */ "clear consent for uninstalled app"));
+ }
+
+ /** Clear KnownAppsWithConsent flag, not support for Measurement. */
+ @Override
+ public void clearKnownAppsWithConsent() {
+ // PPAPI_AND_ADEXT_SERVICE is only set on R which supports only
+ // Measurement.
+ throw new IllegalStateException(
+ getAdExtExceptionMessage(/* illegalAction= */ "reset apps"));
+ }
+
+ /** Gets getAppsWithRevokedConsent flag, not support for Measurement. */
+ @Override
+ public ImmutableList<String> getAppsWithRevokedConsent() {
+ // PPAPI_AND_ADEXT_SERVICE is only set on R which supports only
+ // Measurement.
+ throw new IllegalStateException(
+ getAdExtExceptionMessage(/* illegalAction= */ "fetch apps with revoked consent"));
+ }
+
+ /** Gets Consent by api flag. */
+ @Override
+ public AdServicesApiConsent getConsent(AdServicesApiType apiType) {
+ if (apiType == AdServicesApiType.MEASUREMENTS) {
+ return AdServicesApiConsent.getConsent(mAdExtDataManager.getMsmtConsent());
+ }
+ return AdServicesApiConsent.REVOKED;
+ }
+
+ /** Gets getKnownAppsWithConsent flag, not support for Measurement. */
+ @Override
+ public ImmutableList<String> getKnownAppsWithConsent() throws IOException {
+ // PPAPI_AND_ADEXT_SERVICE is only set on R which supports only
+ // Measurement.
+ throw new IllegalStateException(
+ getAdExtExceptionMessage(/* illegalAction= */ "fetch apps with consent"));
+ }
+
+ /** Gets UserManualInteraction flag. */
+ @Override
+ public int getUserManualInteractionWithConsent() {
+ return mAdExtDataManager.getManualInteractionWithConsentStatus();
+ }
+
+ /** Gets isAdultAccount flag. */
+ @Override
+ public boolean isAdultAccount() {
+ return mAdExtDataManager.getIsAdultAccount();
+ }
+
+ /** Gets isConsentRevokedForApp flag, not support for Measurement. */
+ @Override
+ public boolean isConsentRevokedForApp(String packageName) {
+ // PPAPI_AND_ADEXT_SERVICE is only set on R which supports only
+ // Measurement.
+ throw new IllegalStateException(
+ getAdExtExceptionMessage(
+ /* illegalAction= */ "check if consent has been revoked for" + " app"));
+ }
+
+ /** Gets isU18 account flag. */
+ @Override
+ public boolean isU18Account() {
+ return mAdExtDataManager.getIsU18Account();
+ }
+
+ /** Records GA notification displayed. */
+ @Override
+ public void recordGaUxNotificationDisplayed(boolean wasGaUxDisplayed) {
+ // PPAPI_AND_ADEXT_SERVICE is only set on R which should never show
+ // GA UX.
+ throw new IllegalStateException(
+ getAdExtExceptionMessage(
+ /* illegalAction= */ "store if GA notification was displayed"));
+ }
+
+ /** Records notification displayed. */
+ @Override
+ public void recordNotificationDisplayed(boolean wasNotificationDisplayed) {
+ // PPAPI_AND_ADEXT_SERVICE is only set on R which should never show
+ // Beta UX.
+ throw new IllegalStateException(
+ getAdExtExceptionMessage(/* illegalAction= */ "store if beta notif was displayed"));
+ }
+
+ /** Records user manual interaction bit. */
+ @Override
+ public void recordUserManualInteractionWithConsent(int interaction) {
+ mAdExtDataManager.setManualInteractionWithConsentStatus(interaction);
+ }
+
+ /** Sets consent by api type. */
+ @Override
+ public void setAdultAccount(boolean isAdultAccount) {
+ mAdExtDataManager.setIsAdultAccount(isAdultAccount);
+ }
+
+ /** Sets consent by api type. */
+ @Override
+ public void setConsent(AdServicesApiType apiType, boolean isGiven) throws IOException {
+ if (apiType == AdServicesApiType.ALL_API) {
+ super.setConsent(apiType, isGiven);
+ return;
+ }
+ // PPAPI_AND_ADEXT_SERVICE is only set on R which supports only
+ // Measurement. There should never be a call to set consent for other PPAPIs.
+ if (apiType != AdServicesApiType.MEASUREMENTS) {
+ throw new IllegalStateException(
+ getAdExtExceptionMessage(
+ /* illegalAction= */ "set consent for a non-msmt API"));
+ }
+ mAdExtDataManager.setMsmtConsent(isGiven);
+ }
+
+ /**
+ * setConsentForApp.
+ *
+ * <p>PPAPI_AND_ADEXT_SERVICE is only set on R which only supports Measurement
+ */
+ @Override
+ public void setConsentForApp(String packageName, boolean isConsentRevoked) {
+ throw new IllegalStateException(
+ getAdExtExceptionMessage(/* illegalAction= */ "revoke consent for app"));
+ }
+
+ /**
+ * SetConsentForAppIfNew.
+ *
+ * <p>PPAPI_AND_ADEXT_SERVICE is only set on R which only supports Measurement
+ */
+ @Override
+ public boolean setConsentForAppIfNew(String packageName, boolean isConsentRevoked) {
+ throw new IllegalStateException(
+ getAdExtExceptionMessage(
+ /* illegalAction= */ "check if consent has been revoked for" + " app"));
+ }
+
+ @Override
+ public void recordDefaultConsent(AdServicesApiType apiType, boolean defaultConsent)
+ throws IOException {
+ if (apiType == AdServicesApiType.MEASUREMENTS) {
+ super.recordDefaultConsent(apiType, defaultConsent);
+ } else {
+ throw new IllegalStateException(
+ getAdExtExceptionMessage(
+ /* illegalAction= */ "record default consent for "
+ + apiType.toString()));
+ }
+ }
+
+ /** Stores isU18Account bit in AdExtData. */
+ @Override
+ public void setU18Account(boolean isU18Account) {
+ mAdExtDataManager.setIsU18Account(isU18Account);
+ }
+
+ /** Stores U18 notification bit in AdExtData. */
+ @Override
+ public void setU18NotificationDisplayed(boolean wasU18NotificationDisplayed) {
+ mAdExtDataManager.setNotificationDisplayed(wasU18NotificationDisplayed);
+ }
+
+ /** GA UX is never shown on R, so this info is not stored. */
+ @Override
+ public boolean wasGaUxNotificationDisplayed() {
+ return false;
+ }
+
+ /** Beta UX is never shown on R, so this info is not stored. */
+ @Override
+ public boolean wasNotificationDisplayed() {
+ return false;
+ }
+
+ /** Android R only U18 notification is allowed to be displayed. */
+ @Override
+ public boolean wasU18NotificationDisplayed() {
+ return mAdExtDataManager.getNotificationDisplayed();
+ }
+
+ private static String getAdExtExceptionMessage(String illegalAction) {
+ return String.format(
+ "Attempting to %s using PPAPI_AND_ADEXT_SERVICE consent source of truth!",
+ illegalAction);
+ }
+}
diff --git a/adservices/service-core/java/com/android/adservices/service/consent/AppConsentStorageManager.java b/adservices/service-core/java/com/android/adservices/service/consent/AppConsentStorageManager.java
index a7dc3050a4..af1b2ddd0b 100644
--- a/adservices/service-core/java/com/android/adservices/service/consent/AppConsentStorageManager.java
+++ b/adservices/service-core/java/com/android/adservices/service/consent/AppConsentStorageManager.java
@@ -31,6 +31,7 @@ import com.android.adservices.service.ui.ux.collection.PrivacySandboxUxCollectio
import com.google.common.collect.ImmutableList;
import java.io.IOException;
+import java.util.Objects;
import java.util.stream.Collectors;
/**
@@ -131,7 +132,8 @@ public class AppConsentStorageManager implements IConsentStorage {
*/
@Override
public AdServicesApiConsent getConsent(AdServicesApiType apiType) {
- return AdServicesApiConsent.getConsent(mDatastore.get(apiType.toPpApiDatastoreKey()));
+ return AdServicesApiConsent.getConsent(
+ Objects.requireNonNullElse(mDatastore.get(apiType.toPpApiDatastoreKey()), false));
}
/**
@@ -153,6 +155,15 @@ public class AppConsentStorageManager implements IConsentStorage {
return PrivacySandboxFeatureType.PRIVACY_SANDBOX_UNSUPPORTED;
}
+ /** Set the current privacy sandbox feature. */
+ @Override
+ public void setCurrentPrivacySandboxFeature(PrivacySandboxFeatureType featureType)
+ throws IOException {
+ for (PrivacySandboxFeatureType currentFeatureType : PrivacySandboxFeatureType.values()) {
+ mDatastore.put(currentFeatureType.name(), currentFeatureType == featureType);
+ }
+ }
+
/**
* Retrieves the default AdId state.
*
@@ -160,7 +171,8 @@ public class AppConsentStorageManager implements IConsentStorage {
*/
@Override
public boolean getDefaultAdIdState() {
- return mDatastore.get(ConsentConstants.DEFAULT_AD_ID_STATE);
+ return Objects.requireNonNullElse(
+ mDatastore.get(ConsentConstants.DEFAULT_AD_ID_STATE), false);
}
/**
@@ -170,7 +182,9 @@ public class AppConsentStorageManager implements IConsentStorage {
*/
@Override
public AdServicesApiConsent getDefaultConsent(AdServicesApiType apiType) {
- return AdServicesApiConsent.getConsent(mDatastore.get(apiType.toPpApiDatastoreKey()));
+ return AdServicesApiConsent.getConsent(
+ Objects.requireNonNullElse(
+ mDatastore.get(apiType.toDefaultConsentDatastoreKey()), false));
}
/** Returns current enrollment channel. */
@@ -215,16 +229,34 @@ public class AppConsentStorageManager implements IConsentStorage {
return mUxStatesDao.getUx();
}
+ /** Set the current UX to storage. */
+ @Override
+ public void setUx(PrivacySandboxUxCollection ux) {
+ mUxStatesDao.setUx(ux);
+ }
+
/** Returns whether the isAdIdEnabled bit is true. */
@Override
public boolean isAdIdEnabled() {
- return mDatastore.get(ConsentConstants.IS_AD_ID_ENABLED);
+ return Objects.requireNonNullElse(mDatastore.get(ConsentConstants.IS_AD_ID_ENABLED), false);
+ }
+
+ /** Set the AdIdEnabled bit to storage. */
+ @Override
+ public void setAdIdEnabled(boolean isAdIdEnabled) throws IOException {
+ mDatastore.put(ConsentConstants.IS_AD_ID_ENABLED, isAdIdEnabled);
}
/** Returns whether the isAdultAccount bit is true. */
@Override
public boolean isAdultAccount() {
- return mDatastore.get(ConsentConstants.IS_ADULT_ACCOUNT);
+ return Objects.requireNonNullElse(mDatastore.get(ConsentConstants.IS_ADULT_ACCOUNT), false);
+ }
+
+ /** Set the AdultAccount bit to storage. */
+ @Override
+ public void setAdultAccount(boolean isAdultAccount) throws IOException {
+ mDatastore.put(ConsentConstants.IS_ADULT_ACCOUNT, isAdultAccount);
}
/**
@@ -241,7 +273,8 @@ public class AppConsentStorageManager implements IConsentStorage {
@Override
public boolean isConsentRevokedForApp(String packageName) throws IllegalArgumentException {
try {
- return mAppConsentDao.isConsentRevokedForApp(packageName);
+ return Objects.requireNonNullElse(
+ mAppConsentDao.isConsentRevokedForApp(packageName), false);
} catch (IOException exception) {
LogUtil.e(exception, "FLEDGE consent check failed due to IOException");
}
@@ -251,13 +284,26 @@ public class AppConsentStorageManager implements IConsentStorage {
/** Returns whether the isEntryPointEnabled bit is true. */
@Override
public boolean isEntryPointEnabled() {
- return mDatastore.get(ConsentConstants.IS_ENTRY_POINT_ENABLED);
+ return Objects.requireNonNullElse(
+ mDatastore.get(ConsentConstants.IS_ENTRY_POINT_ENABLED), false);
+ }
+
+ /** Set the EntryPointEnabled bit to storage . */
+ @Override
+ public void setEntryPointEnabled(boolean isEntryPointEnabled) throws IOException {
+ mDatastore.put(ConsentConstants.IS_ENTRY_POINT_ENABLED, isEntryPointEnabled);
}
/** Returns whether the isU18Account bit is true. */
@Override
public boolean isU18Account() {
- return mDatastore.get(ConsentConstants.IS_U18_ACCOUNT);
+ return Objects.requireNonNullElse(mDatastore.get(ConsentConstants.IS_U18_ACCOUNT), false);
+ }
+
+ /** Set the U18Account bit to storage. */
+ @Override
+ public void setU18Account(boolean isU18Account) throws IOException {
+ mDatastore.put(ConsentConstants.IS_U18_ACCOUNT, isU18Account);
}
/** Saves the default AdId state bit to data stores based on source of truth. */
@@ -270,7 +316,7 @@ public class AppConsentStorageManager implements IConsentStorage {
@Override
public void recordDefaultConsent(AdServicesApiType apiType, boolean defaultConsent)
throws IOException {
- mDatastore.put(apiType.toPpApiDatastoreKey(), defaultConsent);
+ mDatastore.put(apiType.toDefaultConsentDatastoreKey(), defaultConsent);
}
/**
@@ -311,22 +357,9 @@ public class AppConsentStorageManager implements IConsentStorage {
}
}
- /** Set the AdIdEnabled bit to storage. */
- @Override
- public void setAdIdEnabled(boolean isAdIdEnabled) throws IOException {
- // test
- mDatastore.put(ConsentConstants.IS_AD_ID_ENABLED, isAdIdEnabled);
- }
-
- /** Set the AdultAccount bit to storage. */
- @Override
- public void setAdultAccount(boolean isAdultAccount) throws IOException {
- mDatastore.put(ConsentConstants.IS_ADULT_ACCOUNT, isAdultAccount);
- }
-
/**
* Sets the consent for this user ID for this API type in AppSearch. If we do not get
- * confirmation that the write was successful, then we throw an exception so that user does not
+ * confirmation that to write was successful, then we throw an exception so that user does not
* incorrectly think that the consent is updated.
*
* @throws IOException if the operation fails
@@ -361,7 +394,7 @@ public class AppConsentStorageManager implements IConsentStorage {
@Override
public boolean setConsentForAppIfNew(String packageName, boolean isConsentRevoked)
throws IllegalArgumentException {
- // test
+ // TODO(b/317595641) clean up setConsentForAppIfNew logic
try {
return mAppConsentDao.setConsentForAppIfNew(packageName, isConsentRevoked);
} catch (IOException exception) {
@@ -370,15 +403,6 @@ public class AppConsentStorageManager implements IConsentStorage {
}
}
- /** Set the current privacy sandbox feature. */
- @Override
- public void setCurrentPrivacySandboxFeature(PrivacySandboxFeatureType featureType)
- throws IOException {
- for (PrivacySandboxFeatureType currentFeatureType : PrivacySandboxFeatureType.values()) {
- mDatastore.put(currentFeatureType.name(), currentFeatureType == featureType);
- }
- }
-
/** Set the current enrollment channel to storage. */
@Override
public void setEnrollmentChannel(
@@ -386,18 +410,6 @@ public class AppConsentStorageManager implements IConsentStorage {
mUxStatesDao.setEnrollmentChannel(ux, channel);
}
- /** Set the EntryPointEnabled bit to storage . */
- @Override
- public void setEntryPointEnabled(boolean isEntryPointEnabled) throws IOException {
- mDatastore.put(ConsentConstants.IS_ENTRY_POINT_ENABLED, isEntryPointEnabled);
- }
-
- /** Set the U18Account bit to storage. */
- @Override
- public void setU18Account(boolean isU18Account) throws IOException {
- mDatastore.put(ConsentConstants.IS_U18_ACCOUNT, isU18Account);
- }
-
/** Set the U18NotificationDisplayed bit to storage. */
@Override
public void setU18NotificationDisplayed(boolean wasU18NotificationDisplayed)
@@ -406,12 +418,6 @@ public class AppConsentStorageManager implements IConsentStorage {
ConsentConstants.WAS_U18_NOTIFICATION_DISPLAYED, wasU18NotificationDisplayed);
}
- /** Set the current UX to storage. */
- @Override
- public void setUx(PrivacySandboxUxCollection ux) {
- mUxStatesDao.setUx(ux);
- }
-
/**
* Retrieves if GA UX notification has been displayed.
*
@@ -419,7 +425,8 @@ public class AppConsentStorageManager implements IConsentStorage {
*/
@Override
public boolean wasGaUxNotificationDisplayed() {
- return mDatastore.get(ConsentConstants.GA_UX_NOTIFICATION_DISPLAYED_ONCE);
+ return Objects.requireNonNullElse(
+ mDatastore.get(ConsentConstants.GA_UX_NOTIFICATION_DISPLAYED_ONCE), false);
}
/**
@@ -429,12 +436,16 @@ public class AppConsentStorageManager implements IConsentStorage {
*/
@Override
public boolean wasNotificationDisplayed() {
- return mDatastore.get(ConsentConstants.NOTIFICATION_DISPLAYED_ONCE);
+ return Objects.requireNonNullElse(
+ mDatastore.get(ConsentConstants.NOTIFICATION_DISPLAYED_ONCE), false);
}
/** Returns whether the wasU18NotificationDisplayed bit is true. */
@Override
public boolean wasU18NotificationDisplayed() {
- return mDatastore.get(ConsentConstants.WAS_U18_NOTIFICATION_DISPLAYED);
+ return Objects.requireNonNullElse(
+ mDatastore.get(ConsentConstants.WAS_U18_NOTIFICATION_DISPLAYED), false);
}
+
+
}
diff --git a/adservices/service-core/java/com/android/adservices/service/consent/ConsentCompositeStorage.java b/adservices/service-core/java/com/android/adservices/service/consent/ConsentCompositeStorage.java
index 730b0788ba..75229cb1ce 100644
--- a/adservices/service-core/java/com/android/adservices/service/consent/ConsentCompositeStorage.java
+++ b/adservices/service-core/java/com/android/adservices/service/consent/ConsentCompositeStorage.java
@@ -22,13 +22,18 @@ import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICE
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_WHILE_GET_CONSENT;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PRIVACY_SANDBOX_SAVE_FAILURE;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX;
+import static com.android.adservices.service.ui.ux.collection.PrivacySandboxUxCollection.U18_UX;
import android.annotation.NonNull;
import android.annotation.SuppressLint;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
import com.android.adservices.LogUtil;
import com.android.adservices.errorlogging.ErrorLogUtil;
import com.android.adservices.service.common.feature.PrivacySandboxFeatureType;
+import com.android.adservices.service.exception.ConsentStorageDeferException;
import com.android.adservices.service.ui.enrollment.collection.PrivacySandboxEnrollmentChannelCollection;
import com.android.adservices.service.ui.ux.collection.PrivacySandboxUxCollection;
import com.android.internal.annotations.VisibleForTesting;
@@ -36,14 +41,22 @@ import com.android.internal.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
-import java.util.List;
/**
* CompositeStorage to handle read/write user's to multiple source of truth
*
* <p>Every source of truth should have its own dedicated storage class that implements the
* IConsentStorage interface, and pass in the instances to ConsentCompositeStorage.
+ *
+ * <p>By default, when caller set value to the storage, CompositeStorage will iterate through every
+ * instance in mConsentStorageList and call the corresponding method. For getter, CompositeStorage
+ * will only return the first one.
+ *
+ * <p>If the method not available in some implementation, the implementation class should throw
+ * {code ConsentStorageDeferException}, CompositeStorage will try to get the result for next
+ * instance.
*/
+@RequiresApi(Build.VERSION_CODES.S)
public class ConsentCompositeStorage implements IConsentStorage {
private static final int UNKNOWN = 0;
private final ImmutableList<IConsentStorage> mConsentStorageList;
@@ -54,7 +67,6 @@ public class ConsentCompositeStorage implements IConsentStorage {
* @param consentStorageList storage implementation instance list.
*/
public ConsentCompositeStorage(ImmutableList<IConsentStorage> consentStorageList) {
- assert (consentStorageList.size() > 0);
if (consentStorageList == null || consentStorageList.isEmpty()) {
throw new IllegalArgumentException("consent storage list can not be empty!");
}
@@ -67,9 +79,21 @@ public class ConsentCompositeStorage implements IConsentStorage {
* <p>This should be called when the Privacy Sandbox has been disabled.
*/
@Override
- public void clearAllAppConsentData() throws IOException {
+ public void clearAllAppConsentData() {
for (IConsentStorage storage : getConsentStorageList()) {
- storage.clearAllAppConsentData();
+ try {
+ storage.clearAllAppConsentData();
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ ErrorLogUtil.e(
+ e,
+ AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_WHILE_GET_CONSENT,
+ AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
+ throw new RuntimeException(e);
+ }
}
}
@@ -80,12 +104,19 @@ public class ConsentCompositeStorage implements IConsentStorage {
*/
@Override
public void clearConsentForUninstalledApp(@NonNull String packageName) {
- try {
- for (IConsentStorage storage : getConsentStorageList()) {
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
storage.clearConsentForUninstalledApp(packageName);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ ErrorLogUtil.e(
+ e,
+ AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_WHILE_GET_CONSENT,
+ AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
}
- } catch (IOException e) {
- throw new RuntimeException(e);
}
}
@@ -97,12 +128,18 @@ public class ConsentCompositeStorage implements IConsentStorage {
*/
@Override
public void clearConsentForUninstalledApp(String packageName, int packageUid) {
-
for (IConsentStorage storage : getConsentStorageList()) {
try {
storage.clearConsentForUninstalledApp(packageName, packageUid);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
} catch (IOException e) {
- LogUtil.e(getClass().getSimpleName() + " failed. " + e.getMessage());
+ ErrorLogUtil.e(
+ e,
+ AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_WHILE_GET_CONSENT,
+ AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
}
}
}
@@ -117,8 +154,15 @@ public class ConsentCompositeStorage implements IConsentStorage {
for (IConsentStorage storage : getConsentStorageList()) {
try {
storage.clearKnownAppsWithConsent();
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
} catch (IOException e) {
- throw new RuntimeException(e);
+ ErrorLogUtil.e(
+ e,
+ AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_WHILE_GET_CONSENT,
+ AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
}
}
}
@@ -126,15 +170,24 @@ public class ConsentCompositeStorage implements IConsentStorage {
/**
* @return an {@link ImmutableList} of all known apps in the database that have had user consent
* revoked
- * @throws IOException if the operation fails
*/
@Override
public ImmutableList<String> getAppsWithRevokedConsent() {
- try {
- return getPrimaryStorage().getAppsWithRevokedConsent();
- } catch (IOException e) {
- throw new RuntimeException(e);
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.getAppsWithRevokedConsent();
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ ErrorLogUtil.e(
+ e,
+ AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_WHILE_GET_CONSENT,
+ AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
+ }
}
+ return ImmutableList.of();
}
/**
@@ -147,15 +200,22 @@ public class ConsentCompositeStorage implements IConsentStorage {
*/
@Override
public AdServicesApiConsent getConsent(AdServicesApiType apiType) {
- try {
- return getPrimaryStorage().getConsent(apiType);
- } catch (IOException e) {
- ErrorLogUtil.e(
- e,
- AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_WHILE_GET_CONSENT,
- AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
- return AdServicesApiConsent.REVOKED;
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.getConsent(apiType);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException | RuntimeException e) {
+ ErrorLogUtil.e(
+ e,
+ AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_WHILE_GET_CONSENT,
+ AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
+ return AdServicesApiConsent.REVOKED;
+ }
}
+ return AdServicesApiConsent.REVOKED;
}
/**
@@ -178,15 +238,42 @@ public class ConsentCompositeStorage implements IConsentStorage {
*/
@Override
public PrivacySandboxFeatureType getCurrentPrivacySandboxFeature() {
- try {
- return getPrimaryStorage().getCurrentPrivacySandboxFeature();
- } catch (IOException e) {
- ErrorLogUtil.e(
- e,
- AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PRIVACY_SANDBOX_SAVE_FAILURE,
- AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
- LogUtil.e(getClass().getSimpleName() + " failed. " + e.getMessage());
- return PrivacySandboxFeatureType.PRIVACY_SANDBOX_UNSUPPORTED;
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.getCurrentPrivacySandboxFeature();
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException | RuntimeException e) {
+ ErrorLogUtil.e(
+ e,
+ AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PRIVACY_SANDBOX_SAVE_FAILURE,
+ AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
+ return PrivacySandboxFeatureType.PRIVACY_SANDBOX_UNSUPPORTED;
+ }
+ }
+ return PrivacySandboxFeatureType.PRIVACY_SANDBOX_UNSUPPORTED;
+ }
+
+ /** Sets the current privacy sandbox feature. */
+ @Override
+ public void setCurrentPrivacySandboxFeature(PrivacySandboxFeatureType featureType) {
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ storage.setCurrentPrivacySandboxFeature(featureType);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ ErrorLogUtil.e(
+ e,
+ AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PRIVACY_SANDBOX_SAVE_FAILURE,
+ AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
+ throw new RuntimeException(
+ getClass().getSimpleName() + " failed. " + e.getMessage());
+ }
}
}
@@ -197,7 +284,19 @@ public class ConsentCompositeStorage implements IConsentStorage {
*/
@Override
public boolean getDefaultAdIdState() {
- return getPrimaryStorage().getDefaultAdIdState();
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.getDefaultAdIdState();
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDatastoreWhileRecordingDefaultConsent(e);
+ return false;
+ }
+ }
+ return false;
}
/**
@@ -207,22 +306,37 @@ public class ConsentCompositeStorage implements IConsentStorage {
*/
@Override
public AdServicesApiConsent getDefaultConsent(AdServicesApiType apiType) {
- try {
- return getPrimaryStorage().getDefaultConsent(apiType);
- } catch (IOException e) {
- ErrorLogUtil.e(
- e,
- AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DATASTORE_EXCEPTION_WHILE_RECORDING_DEFAULT_CONSENT,
- AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
- return AdServicesApiConsent.REVOKED;
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.getDefaultConsent(apiType);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDatastoreWhileRecordingDefaultConsent(e);
+ return AdServicesApiConsent.REVOKED;
+ }
}
+ return AdServicesApiConsent.REVOKED;
}
/** Returns current enrollment channel. */
@Override
public PrivacySandboxEnrollmentChannelCollection getEnrollmentChannel(
PrivacySandboxUxCollection ux) {
- return getPrimaryStorage().getEnrollmentChannel(ux);
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.getEnrollmentChannel(ux);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDatastoreWhileRecordingDefaultConsent(e);
+ }
+ }
+ return null;
}
/**
@@ -233,26 +347,20 @@ public class ConsentCompositeStorage implements IConsentStorage {
*/
@Override
public ImmutableList<String> getKnownAppsWithConsent() {
- try {
- return getPrimaryStorage().getKnownAppsWithConsent();
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Gets first storage instance, for read operations, should always return from the first source
- * of truth.
- *
- * @return first instance of IConsentStorage.
- */
- @VisibleForTesting
- public IConsentStorage getPrimaryStorage() {
- List<IConsentStorage> storageList = getConsentStorageList();
- if (storageList.isEmpty()) {
- throw new IllegalStateException("Consent Storage List is empty.");
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.getKnownAppsWithConsent();
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDatastoreWhileRecordingDefaultConsent(e);
+ } catch (IllegalStateException e) {
+ LogUtil.i("IllegalStateException" + e);
+ }
}
- return storageList.get(0);
+ return ImmutableList.of();
}
/**
@@ -262,33 +370,121 @@ public class ConsentCompositeStorage implements IConsentStorage {
*/
@Override
public int getUserManualInteractionWithConsent() {
- try {
- return getPrimaryStorage().getUserManualInteractionWithConsent();
- } catch (IOException e) {
- ErrorLogUtil.e(
- e,
- AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DATASTORE_EXCEPTION_WHILE_RECORDING_MANUAL_CONSENT_INTERACTION,
- AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
- return UNKNOWN;
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.getUserManualInteractionWithConsent();
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDatastoreManualInteractionException(e);
+ return UNKNOWN;
+ }
}
+ return 0;
}
/** Returns current UX. */
@Override
public PrivacySandboxUxCollection getUx() {
- return getPrimaryStorage().getUx();
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.getUx();
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDatastoreManualInteractionException(e);
+ throw new RuntimeException(e);
+ }
+ }
+ return null;
+ }
+
+ /** Sets the current UX to storage. */
+ @Override
+ public void setUx(PrivacySandboxUxCollection ux) {
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ storage.setUx(ux);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDataStoreWhileRecordingException(e);
+ }
+ }
}
/** Returns whether the isAdIdEnabled bit is true. */
@Override
public boolean isAdIdEnabled() {
- return getPrimaryStorage().isAdIdEnabled();
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.isAdIdEnabled();
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDatastoreManualInteractionException(e);
+ throw new RuntimeException(e);
+ }
+ }
+ return false;
+ }
+
+ /** Set the AdIdEnabled bit to storage. */
+ @Override
+ public void setAdIdEnabled(boolean isAdIdEnabled) {
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ storage.setAdIdEnabled(isAdIdEnabled);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDataStoreWhileRecordingException(e);
+ }
+ }
}
/** Returns whether the isAdultAccount bit is true. */
@Override
public boolean isAdultAccount() {
- return getPrimaryStorage().isAdultAccount();
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.isAdultAccount();
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDatastoreManualInteractionException(e);
+ throw new RuntimeException(e);
+ }
+ }
+ return false;
+ }
+
+ /** Set the AdultAccount bit to storage. */
+ @Override
+ public void setAdultAccount(boolean isAdultAccount) {
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ storage.setAdultAccount(isAdultAccount);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDataStoreWhileRecordingException(e);
+ }
+ }
}
/**
@@ -300,23 +496,90 @@ public class ConsentCompositeStorage implements IConsentStorage {
*
* @throws IllegalArgumentException if the package name is invalid or not found as an installed
* application
- * @throws IOException if the operation fails
*/
@Override
public boolean isConsentRevokedForApp(String packageName) throws IllegalArgumentException {
- return getPrimaryStorage().isConsentRevokedForApp(packageName);
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.isConsentRevokedForApp(packageName);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDatastoreManualInteractionException(e);
+ throw new RuntimeException(e);
+ }
+ }
+ return false;
}
/** Returns whether the isEntryPointEnabled bit is true. */
@Override
public boolean isEntryPointEnabled() {
- return getPrimaryStorage().isEntryPointEnabled();
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.isEntryPointEnabled();
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDatastoreManualInteractionException(e);
+ throw new RuntimeException(e);
+ }
+ }
+ return false;
+ }
+
+ /** Sets the EntryPointEnabled bit to storage . */
+ @Override
+ public void setEntryPointEnabled(boolean isEntryPointEnabled) {
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ storage.setEntryPointEnabled(isEntryPointEnabled);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDataStoreWhileRecordingException(e);
+ }
+ }
}
/** Returns whether the isU18Account bit is true. */
@Override
public boolean isU18Account() {
- return getPrimaryStorage().isU18Account();
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.isU18Account();
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDatastoreManualInteractionException(e);
+ throw new RuntimeException(e);
+ }
+ }
+ return false;
+ }
+
+ /** Sets the U18Account bit to storage. */
+ @Override
+ public void setU18Account(boolean isU18Account) {
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ storage.setU18Account(isU18Account);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDataStoreWhileRecordingException(e);
+ }
+ }
}
/** Saves the default AdId state bit to data stores based on source of truth. */
@@ -325,7 +588,12 @@ public class ConsentCompositeStorage implements IConsentStorage {
for (IConsentStorage storage : getConsentStorageList()) {
try {
storage.recordDefaultAdIdState(defaultAdIdState);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
} catch (IOException e) {
+ logDatastoreManualInteractionException(e);
throw new RuntimeException(e);
}
}
@@ -337,7 +605,12 @@ public class ConsentCompositeStorage implements IConsentStorage {
for (IConsentStorage storage : getConsentStorageList()) {
try {
storage.recordDefaultConsent(apiType, defaultConsent);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
} catch (IOException e) {
+ logDatastoreManualInteractionException(e);
throw new RuntimeException(e);
}
}
@@ -352,7 +625,12 @@ public class ConsentCompositeStorage implements IConsentStorage {
for (IConsentStorage storage : getConsentStorageList()) {
try {
storage.recordGaUxNotificationDisplayed(wasGaUxDisplayed);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
} catch (IOException e) {
+ logDatastoreManualInteractionException(e);
throw new RuntimeException(e);
}
}
@@ -366,10 +644,13 @@ public class ConsentCompositeStorage implements IConsentStorage {
public void recordNotificationDisplayed(boolean wasNotificationDisplayed) {
for (IConsentStorage storage : getConsentStorageList()) {
try {
-
storage.recordNotificationDisplayed(wasNotificationDisplayed);
-
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
} catch (IOException e) {
+ logDatastoreManualInteractionException(e);
throw new RuntimeException(e);
}
}
@@ -381,53 +662,66 @@ public class ConsentCompositeStorage implements IConsentStorage {
for (IConsentStorage storage : getConsentStorageList()) {
try {
storage.recordUserManualInteractionWithConsent(interaction);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
} catch (IOException e) {
- ErrorLogUtil.e(
- e,
- AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DATASTORE_EXCEPTION_WHILE_RECORDING_MANUAL_CONSENT_INTERACTION,
- AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
+ logDatastoreManualInteractionException(e);
throw new RuntimeException(
getClass().getSimpleName() + " failed. " + e.getMessage());
}
}
}
- /** Set the AdIdEnabled bit to storage. */
+ /**
+ * Sets the consent for this user ID for this API type in AppSearch. If we do not get
+ * confirmation that the write was successful, then we throw an exception so that user does not
+ * incorrectly think that the consent is updated.
+ */
@Override
- public void setAdIdEnabled(boolean isAdIdEnabled) {
- for (IConsentStorage storage : getConsentStorageList()) {
- try {
- storage.setAdIdEnabled(isAdIdEnabled);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
+ public void setConsent(AdServicesApiType apiType, boolean isGiven) {
+ setConsentToApiType(apiType, isGiven);
+ if (apiType == AdServicesApiType.ALL_API) {
+ return;
}
+ setAggregatedConsent();
}
- /** Set the AdultAccount bit to storage. */
- @Override
- public void setAdultAccount(boolean isAdultAccount) {
+ private void setConsentToApiType(AdServicesApiType apiType, boolean isGiven) {
for (IConsentStorage storage : getConsentStorageList()) {
try {
- storage.setAdultAccount(isAdultAccount);
+ storage.setConsent(apiType, isGiven);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
} catch (IOException e) {
- throw new RuntimeException(e);
+ logDataStoreWhileRecordingException(e);
}
}
}
- /**
- * Sets the consent for this user ID for this API type in AppSearch. If we do not get
- * confirmation that the write was successful, then we throw an exception so that user does not
- * incorrectly think that the consent is updated.
- *
- * @throws IOException if the operation fails
- */
- @Override
- public void setConsent(AdServicesApiType apiType, boolean isGiven) throws IOException {
- for (IConsentStorage storage : getConsentStorageList()) {
- storage.setConsent(apiType, isGiven);
+ // Set the aggregated consent so that after the rollback of the module
+ // and the flag which controls the consent flow everything works as expected.
+ // The problematic edge case which is covered:
+ // T1: AdServices is installed in pre-GA UX version and the consent is given
+ // T2: AdServices got upgraded to GA UX binary and GA UX feature flag is enabled
+ // T3: Consent for the Topics API got revoked
+ // T4: AdServices got rolledback and the feature flags which controls consent flow
+ // (SYSTEM_SERVER_ONLY and DUAL_WRITE) also got rolledback
+ // T5: Restored consent should be revoked
+ @VisibleForTesting
+ void setAggregatedConsent() {
+ if (getUx() == U18_UX) {
+ // The edge case does not apply to U18 UX.
+ return;
}
+ setConsentToApiType(
+ AdServicesApiType.ALL_API,
+ getConsent(AdServicesApiType.TOPICS).isGiven()
+ && getConsent(AdServicesApiType.MEASUREMENTS).isGiven()
+ && getConsent(AdServicesApiType.FLEDGE).isGiven());
}
/**
@@ -441,7 +735,12 @@ public class ConsentCompositeStorage implements IConsentStorage {
for (IConsentStorage storage : getConsentStorageList()) {
try {
storage.setConsentForApp(packageName, isConsentRevoked);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
} catch (IOException e) {
+ logDataStoreWhileRecordingException(e);
throw new RuntimeException(e);
}
}
@@ -459,34 +758,25 @@ public class ConsentCompositeStorage implements IConsentStorage {
*/
@Override
public boolean setConsentForAppIfNew(String packageName, boolean isConsentRevoked) {
- boolean ret = false;
- for (IConsentStorage storage : getConsentStorageList()) {
- try {
- // Same as original logic, only return the last one.
- ret = storage.setConsentForAppIfNew(packageName, isConsentRevoked);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- // LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
- return ret;
- }
-
- /** Sets the current privacy sandbox feature. */
- @Override
- public void setCurrentPrivacySandboxFeature(PrivacySandboxFeatureType featureType) {
- for (IConsentStorage storage : getConsentStorageList()) {
+ for (IConsentStorage storage : getConsentStorageList().reverse()) {
try {
- storage.setCurrentPrivacySandboxFeature(featureType);
+ // Same as original logic, call the PPAPI first
+ // TODO(b/317595641): clean up the logic
+ boolean ret = storage.setConsentForAppIfNew(packageName, isConsentRevoked);
+ if (ret) {
+ return true;
+ }
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
} catch (IOException e) {
- ErrorLogUtil.e(
- e,
- AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PRIVACY_SANDBOX_SAVE_FAILURE,
- AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
- throw new RuntimeException(
- getClass().getSimpleName() + " failed. " + e.getMessage());
+ LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
+ logDataStoreWhileRecordingException(e);
+ return false;
}
}
+ return false;
}
/** Sets the current enrollment channel to storage. */
@@ -494,50 +784,35 @@ public class ConsentCompositeStorage implements IConsentStorage {
public void setEnrollmentChannel(
PrivacySandboxUxCollection ux, PrivacySandboxEnrollmentChannelCollection channel) {
for (IConsentStorage storage : getConsentStorageList()) {
- storage.setEnrollmentChannel(ux, channel);
- }
- }
-
- /** Sets the EntryPointEnabled bit to storage . */
- @Override
- public void setEntryPointEnabled(boolean isEntryPointEnabled) {
- for (IConsentStorage storage : getConsentStorageList()) {
try {
- storage.setEntryPointEnabled(isEntryPointEnabled);
+ storage.setEnrollmentChannel(ux, channel);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
} catch (IOException e) {
- throw new RuntimeException(e);
+ logDataStoreWhileRecordingException(e);
}
}
}
- /** Sets the U18Account bit to storage. */
- @Override
- public void setU18Account(boolean isU18Account) throws IOException {
- for (IConsentStorage storage : getConsentStorageList()) {
- storage.setU18Account(isU18Account);
- }
- }
-
/** Sets the U18NotificationDisplayed bit to storage. */
@Override
public void setU18NotificationDisplayed(boolean wasU18NotificationDisplayed) {
for (IConsentStorage storage : getConsentStorageList()) {
try {
storage.setU18NotificationDisplayed(wasU18NotificationDisplayed);
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
} catch (IOException e) {
+ logDataStoreWhileRecordingException(e);
throw new RuntimeException(e);
}
}
}
- /** Sets the current UX to storage. */
- @Override
- public void setUx(PrivacySandboxUxCollection ux) {
- for (IConsentStorage storage : getConsentStorageList()) {
- storage.setUx(ux);
- }
- }
-
/**
* Retrieves if GA UX notification has been displayed.
*
@@ -545,7 +820,19 @@ public class ConsentCompositeStorage implements IConsentStorage {
*/
@Override
public boolean wasGaUxNotificationDisplayed() {
- return getPrimaryStorage().wasGaUxNotificationDisplayed();
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.wasGaUxNotificationDisplayed();
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDataStoreWhileRecordingException(e);
+ return false;
+ }
+ }
+ return false;
}
/**
@@ -556,20 +843,57 @@ public class ConsentCompositeStorage implements IConsentStorage {
@SuppressLint("NameOfTheRuleToSuppress")
@Override
public boolean wasNotificationDisplayed() {
- try {
- return getPrimaryStorage().wasNotificationDisplayed();
- } catch (IOException e) {
- ErrorLogUtil.e(
- e,
- AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DATASTORE_EXCEPTION_WHILE_RECORDING_NOTIFICATION,
- AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
- return false;
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.wasNotificationDisplayed();
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDataStoreWhileRecordingException(e);
+ return false;
+ }
}
+ return false;
}
/** Returns whether the wasU18NotificationDisplayed bit is true. */
@Override
public boolean wasU18NotificationDisplayed() {
- return getPrimaryStorage().wasU18NotificationDisplayed();
+ for (IConsentStorage storage : getConsentStorageList()) {
+ try {
+ return storage.wasU18NotificationDisplayed();
+ } catch (ConsentStorageDeferException e) {
+ LogUtil.i(
+ "Skip current storage manager %s. Defer to next one",
+ storage.getClass().getSimpleName());
+ } catch (IOException e) {
+ logDataStoreWhileRecordingException(e);
+ return false;
+ }
+ }
+ return false;
+ }
+
+ private static void logDataStoreWhileRecordingException(IOException e) {
+ ErrorLogUtil.e(
+ e,
+ AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DATASTORE_EXCEPTION_WHILE_RECORDING_NOTIFICATION,
+ AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
+ }
+
+ private static void logDatastoreManualInteractionException(IOException e) {
+ ErrorLogUtil.e(
+ e,
+ AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DATASTORE_EXCEPTION_WHILE_RECORDING_MANUAL_CONSENT_INTERACTION,
+ AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
+ }
+
+ private static void logDatastoreWhileRecordingDefaultConsent(IOException e) {
+ ErrorLogUtil.e(
+ e,
+ AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DATASTORE_EXCEPTION_WHILE_RECORDING_DEFAULT_CONSENT,
+ AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
}
}
diff --git a/adservices/service-core/java/com/android/adservices/service/consent/ConsentConstants.java b/adservices/service-core/java/com/android/adservices/service/consent/ConsentConstants.java
index 8407ed08e4..582f2a1413 100644
--- a/adservices/service-core/java/com/android/adservices/service/consent/ConsentConstants.java
+++ b/adservices/service-core/java/com/android/adservices/service/consent/ConsentConstants.java
@@ -17,7 +17,6 @@
package com.android.adservices.service.consent;
import com.android.adservices.service.common.compat.FileCompatUtils;
-import com.android.internal.annotations.VisibleForTesting;
/** ConsentManager related Constants. */
public class ConsentConstants {
@@ -37,8 +36,7 @@ public class ConsentConstants {
public static final String DEFAULT_AD_ID_STATE = "DEFAULT_AD_ID_STATE";
- @VisibleForTesting
- static final String MANUAL_INTERACTION_WITH_CONSENT_RECORDED =
+ public static final String MANUAL_INTERACTION_WITH_CONSENT_RECORDED =
"MANUAL_INTERACTION_WITH_CONSENT_RECORDED";
public static final String CONSENT_KEY = "CONSENT";
diff --git a/adservices/service-core/java/com/android/adservices/service/consent/ConsentManager.java b/adservices/service-core/java/com/android/adservices/service/consent/ConsentManager.java
index 900ffd58f3..171970b3b6 100644
--- a/adservices/service-core/java/com/android/adservices/service/consent/ConsentManager.java
+++ b/adservices/service-core/java/com/android/adservices/service/consent/ConsentManager.java
@@ -39,6 +39,7 @@ import android.app.job.JobScheduler;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
+import android.os.Trace;
import androidx.annotation.RequiresApi;
@@ -206,6 +207,7 @@ public class ConsentManager {
public static ConsentManager getInstance(@NonNull Context context) {
Objects.requireNonNull(context);
+ Trace.beginSection("ConsentManager#Initialization");
if (sConsentManager == null) {
synchronized (LOCK) {
if (sConsentManager == null) {
@@ -253,7 +255,8 @@ public class ConsentManager {
context,
datastore,
appSearchConsentManager,
- adServicesExtDataManager);
+ adServicesExtDataManager,
+ statsdAdServicesLogger);
}
}
@@ -288,6 +291,7 @@ public class ConsentManager {
}
}
}
+ Trace.endSection();
return sConsentManager;
}
@@ -2154,6 +2158,7 @@ public class ConsentManager {
for back compat. */
ThrowableSetter ppapiAndAdExtDataServiceSetter,
ErrorLogger errorLogger) {
+ Trace.beginSection("ConsentManager#WriteOperation");
mReadWriteLock.writeLock().lock();
try {
switch (mConsentSourceOfTruth) {
@@ -2185,11 +2190,12 @@ public class ConsentManager {
if (errorLogger != null) {
errorLogger.apply(e);
}
- throw new RuntimeException(getClass().getSimpleName() + " failed. " + e.getMessage());
+ throw new RuntimeException(
+ getClass().getSimpleName() + " failed. " + e.getMessage(), e);
} finally {
mReadWriteLock.writeLock().unlock();
+ Trace.endSection();
}
-
}
@FunctionalInterface
@@ -2218,6 +2224,7 @@ public class ConsentManager {
for back compat. */
ThrowableGetter<T> ppapiAndAdExtDataServiceGetter,
ErrorLogger errorLogger) {
+ Trace.beginSection("ConsentManager#ReadOperation");
mReadWriteLock.readLock().lock();
try {
switch (mConsentSourceOfTruth) {
@@ -2248,6 +2255,7 @@ public class ConsentManager {
LogUtil.e(getClass().getSimpleName() + " failed. " + e.getMessage());
} finally {
mReadWriteLock.readLock().unlock();
+ Trace.endSection();
}
return defaultReturn;
@@ -2260,8 +2268,20 @@ public class ConsentManager {
: AD_SERVICES_SETTINGS_USAGE_REPORTED__REGION__ROW;
}
- /* Returns an object of ConsentMigrationStats */
- private static ConsentMigrationStats getConsentManagerStatsForLogging(
+ /***
+ * Returns an object of ConsentMigrationStats for logging
+ *
+ * @param appConsents AppConsents consents per API (fledge, msmt, topics, default)
+ * @param migrationStatus Status of migration ( FAILURE, SUCCESS_WITH_SHARED_PREF_UPDATED,
+ * SUCCESS_WITH_SHARED_PREF_NOT_UPDATED)
+ * @param migrationType Type of migration ( PPAPI_TO_SYSTEM_SERVICE,
+ * APPSEARCH_TO_SYSTEM_SERVICE,
+ * ADEXT_SERVICE_TO_SYSTEM_SERVICE,
+ * ADEXT_SERVICE_TO_APPSEARCH)
+ * @param context Context of the application
+ * @return consentMigrationStats returns ConsentMigrationStats for logging
+ */
+ public static ConsentMigrationStats getConsentManagerStatsForLogging(
AppConsents appConsents,
ConsentMigrationStats.MigrationStatus migrationStatus,
ConsentMigrationStats.MigrationType migrationType,
@@ -2269,7 +2289,6 @@ public class ConsentManager {
ConsentMigrationStats consentMigrationStats =
ConsentMigrationStats.builder()
.setMigrationType(migrationType)
- .setMigrationStatus(migrationStatus)
// When appConsents is null we log it as a failure
.setMigrationStatus(
appConsents != null
diff --git a/adservices/service-core/java/com/android/adservices/service/consent/ConsentManagerV2.java b/adservices/service-core/java/com/android/adservices/service/consent/ConsentManagerV2.java
new file mode 100644
index 0000000000..b321c6754c
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/service/consent/ConsentManagerV2.java
@@ -0,0 +1,1445 @@
+/*
+ * 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.adservices.service.consent;
+
+import static com.android.adservices.AdServicesCommon.ADEXTSERVICES_PACKAGE_NAME_SUFFIX;
+import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__APP_SEARCH_DATA_MIGRATION_FAILURE;
+import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SHARED_PREF_RESET_FAILURE;
+import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SHARED_PREF_UPDATE_FAILURE;
+import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX;
+import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_MEASUREMENT_WIPEOUT;
+import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_SETTINGS_USAGE_REPORTED__REGION__EU;
+import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_SETTINGS_USAGE_REPORTED__REGION__ROW;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.app.adservices.AdServicesManager;
+import android.app.job.JobScheduler;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.adservices.LogUtil;
+import com.android.adservices.concurrency.AdServicesExecutors;
+import com.android.adservices.data.adselection.AppInstallDao;
+import com.android.adservices.data.adselection.FrequencyCapDao;
+import com.android.adservices.data.adselection.SharedStorageDatabase;
+import com.android.adservices.data.common.BooleanFileDatastore;
+import com.android.adservices.data.consent.AppConsentDao;
+import com.android.adservices.data.customaudience.CustomAudienceDao;
+import com.android.adservices.data.customaudience.CustomAudienceDatabase;
+import com.android.adservices.data.enrollment.EnrollmentDao;
+import com.android.adservices.data.topics.Topic;
+import com.android.adservices.data.topics.TopicsTables;
+import com.android.adservices.errorlogging.ErrorLogUtil;
+import com.android.adservices.service.Flags;
+import com.android.adservices.service.FlagsFactory;
+import com.android.adservices.service.appsearch.AppSearchConsentStorageManager;
+import com.android.adservices.service.common.BackgroundJobsManager;
+import com.android.adservices.service.common.UserProfileIdManager;
+import com.android.adservices.service.common.compat.FileCompatUtils;
+import com.android.adservices.service.common.feature.PrivacySandboxFeatureType;
+import com.android.adservices.service.extdata.AdServicesExtDataStorageServiceManager;
+import com.android.adservices.service.measurement.MeasurementImpl;
+import com.android.adservices.service.measurement.WipeoutStatus;
+import com.android.adservices.service.stats.AdServicesLoggerImpl;
+import com.android.adservices.service.stats.ConsentMigrationStats;
+import com.android.adservices.service.stats.MeasurementWipeoutStats;
+import com.android.adservices.service.stats.StatsdAdServicesLogger;
+import com.android.adservices.service.stats.UiStatsLogger;
+import com.android.adservices.service.topics.TopicsWorker;
+import com.android.adservices.service.ui.data.UxStatesDao;
+import com.android.adservices.service.ui.enrollment.collection.PrivacySandboxEnrollmentChannelCollection;
+import com.android.adservices.service.ui.ux.collection.PrivacySandboxUxCollection;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
+
+import com.google.common.collect.ImmutableList;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * Manager all critical user data such as per API consent.
+ *
+ * <p>For Beta the consent is given for all {@link AdServicesApiType} or for none.
+ *
+ * <p>Currently there are three types of source of truth to store consent data,
+ *
+ * <ul>
+ * <li>SYSTEM_SERVER_ONLY: Write and read consent from system server only.
+ * <li>PPAPI_ONLY: Write and read consent from PPAPI only.
+ * <li>PPAPI_AND_SYSTEM_SERVER: Write consent to both PPAPI and system server. Read consent from
+ * system server only.
+ * <li>APPSEARCH_ONLY: Write and read consent from appSearch only for back compat.
+ * <li>PPAPI_AND_ADEXT_SERVICE: Write and read consent from PPAPI and AdExt service..
+ * </ul>
+ */
+// TODO(b/279042385): move UI logs to UI.
+@RequiresApi(Build.VERSION_CODES.S)
+public class ConsentManagerV2 {
+ private static volatile ConsentManagerV2 sConsentManager;
+
+ @IntDef(value = {NO_MANUAL_INTERACTIONS_RECORDED, UNKNOWN, MANUAL_INTERACTIONS_RECORDED})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface UserManualInteraction {}
+
+ public static final int NO_MANUAL_INTERACTIONS_RECORDED = -1;
+ public static final int UNKNOWN = 0;
+ public static final int MANUAL_INTERACTIONS_RECORDED = 1;
+
+ private final Flags mFlags;
+ private final TopicsWorker mTopicsWorker;
+ private final BooleanFileDatastore mDatastore;
+ private final EnrollmentDao mEnrollmentDao;
+ private final MeasurementImpl mMeasurementImpl;
+ private final CustomAudienceDao mCustomAudienceDao;
+ private final AppInstallDao mAppInstallDao;
+ private final FrequencyCapDao mFrequencyCapDao;
+ private final AdServicesStorageManager mAdServicesStorageManager;
+ private final AppSearchConsentStorageManager mAppSearchConsentStorageManager;
+ private final UserProfileIdManager mUserProfileIdManager;
+
+ private final AppConsentForRStorageManager mAppConsentForRStorageManager;
+
+ private static final Object LOCK = new Object();
+
+ private ConsentCompositeStorage mConsentCompositeStorage;
+
+ private AppConsentStorageManager mAppConsentStorageManager;
+
+ ConsentManagerV2(
+ @NonNull TopicsWorker topicsWorker,
+ @NonNull AppConsentDao appConsentDao,
+ @NonNull EnrollmentDao enrollmentDao,
+ @NonNull MeasurementImpl measurementImpl,
+ @NonNull CustomAudienceDao customAudienceDao,
+ @NonNull AppConsentStorageManager appConsentStorageManager,
+ @NonNull AppInstallDao appInstallDao,
+ @NonNull FrequencyCapDao frequencyCapDao,
+ @NonNull AdServicesStorageManager adServicesStorageManager,
+ @NonNull BooleanFileDatastore booleanFileDatastore,
+ @NonNull AppSearchConsentStorageManager appSearchConsentStorageManager,
+ @NonNull UserProfileIdManager userProfileIdManager,
+ @NonNull AppConsentForRStorageManager appConsentForRStorageManager,
+ @NonNull Flags flags,
+ @Flags.ConsentSourceOfTruth int consentSourceOfTruth,
+ boolean enableAppsearchConsentData,
+ boolean enableAdExtServiceConsentData) {
+ Objects.requireNonNull(topicsWorker);
+ Objects.requireNonNull(appConsentDao);
+ Objects.requireNonNull(measurementImpl);
+ Objects.requireNonNull(customAudienceDao);
+ Objects.requireNonNull(appInstallDao);
+ Objects.requireNonNull(frequencyCapDao);
+ Objects.requireNonNull(booleanFileDatastore);
+ Objects.requireNonNull(userProfileIdManager);
+
+ if (consentSourceOfTruth != Flags.PPAPI_ONLY
+ && consentSourceOfTruth != Flags.APPSEARCH_ONLY) {
+ Objects.requireNonNull(adServicesStorageManager);
+ }
+
+ if (enableAppsearchConsentData) {
+ Objects.requireNonNull(appSearchConsentStorageManager);
+ }
+
+ if (enableAdExtServiceConsentData) {
+ Objects.requireNonNull(appConsentForRStorageManager);
+ }
+
+ mAdServicesStorageManager = adServicesStorageManager;
+ mTopicsWorker = topicsWorker;
+ mDatastore = booleanFileDatastore;
+ mEnrollmentDao = enrollmentDao;
+ mMeasurementImpl = measurementImpl;
+ mCustomAudienceDao = customAudienceDao;
+ mAppInstallDao = appInstallDao;
+ mFrequencyCapDao = frequencyCapDao;
+
+ mAppSearchConsentStorageManager = appSearchConsentStorageManager;
+ mUserProfileIdManager = userProfileIdManager;
+
+ mFlags = flags;
+ mAppConsentStorageManager = appConsentStorageManager;
+ mAppConsentForRStorageManager = appConsentForRStorageManager;
+
+ mConsentCompositeStorage =
+ new ConsentCompositeStorage(getStorageListBySourceOfTruth(consentSourceOfTruth));
+ }
+
+ private ImmutableList<IConsentStorage> getStorageListBySourceOfTruth(
+ @Flags.ConsentSourceOfTruth int consentSourceOfTruth) {
+ switch (consentSourceOfTruth) {
+ case Flags.PPAPI_ONLY:
+ return ImmutableList.of(mAppConsentStorageManager);
+ case Flags.SYSTEM_SERVER_ONLY:
+ return ImmutableList.of(mAdServicesStorageManager);
+ case Flags.PPAPI_AND_SYSTEM_SERVER:
+ // System storage has higher priority
+ return ImmutableList.of(mAdServicesStorageManager, mAppConsentStorageManager);
+ case Flags.APPSEARCH_ONLY:
+ return ImmutableList.of(mAppSearchConsentStorageManager);
+ case Flags.PPAPI_AND_ADEXT_SERVICE:
+ return ImmutableList.of(mAppConsentForRStorageManager);
+ default:
+ LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
+ return ImmutableList.of();
+ }
+ }
+
+ /**
+ * Gets an instance of {@link ConsentManagerV2} to be used.
+ *
+ * <p>If no instance has been initialized yet, a new one will be created. Otherwise, the
+ * existing instance will be returned.
+ */
+ @NonNull
+ public static ConsentManagerV2 getInstance(@NonNull Context context) {
+ Objects.requireNonNull(context);
+
+ if (sConsentManager == null) {
+ synchronized (LOCK) {
+ if (sConsentManager == null) {
+ // Execute one-time consent migration if needed.
+ int consentSourceOfTruth = FlagsFactory.getFlags().getConsentSourceOfTruth();
+ BooleanFileDatastore datastore = createAndInitializeDataStore(context);
+ AdServicesStorageManager adServicesManager =
+ AdServicesStorageManager.getInstance(
+ AdServicesManager.getInstance(context));
+ AppConsentDao appConsentDao = AppConsentDao.getInstance(context);
+
+ // It is possible that the old value of the flag lingers after OTA until the
+ // first PH sync. In that case, we should not use the stale value, but use the
+ // default instead. The next PH sync will restore the T+ value.
+ if (SdkLevel.isAtLeastT() && consentSourceOfTruth == Flags.APPSEARCH_ONLY) {
+ consentSourceOfTruth = Flags.DEFAULT_CONSENT_SOURCE_OF_TRUTH;
+ }
+ AppSearchConsentStorageManager appSearchConsentStorageManager = null;
+ StatsdAdServicesLogger statsdAdServicesLogger =
+ StatsdAdServicesLogger.getInstance();
+ // Flag enable_appsearch_consent_data is true on S- and T+ only when we want to
+ // use AppSearch to write to or read from.
+ boolean enableAppsearchConsentData =
+ FlagsFactory.getFlags().getEnableAppsearchConsentData();
+ if (enableAppsearchConsentData) {
+ appSearchConsentStorageManager =
+ AppSearchConsentStorageManager.getInstance();
+ handleConsentMigrationFromAppSearchIfNeeded(
+ context,
+ datastore,
+ appConsentDao,
+ appSearchConsentStorageManager,
+ adServicesManager,
+ statsdAdServicesLogger);
+ }
+ UxStatesDao uxStatesDao = UxStatesDao.getInstance(context);
+ AppConsentForRStorageManager mAppConsentForRStorageManager = null;
+ // Flag enable_adext_service_consent_data is true on R and S+ only when
+ // we want to use AdServicesExtDataStorageService to write to or read from.
+ boolean enableAdExtServiceConsentData =
+ FlagsFactory.getFlags().getEnableAdExtServiceConsentData();
+ if (enableAdExtServiceConsentData) {
+ AdServicesExtDataStorageServiceManager adServicesExtDataManager =
+ AdServicesExtDataStorageServiceManager.getInstance(context);
+ if (FlagsFactory.getFlags().getEnableAdExtServiceToAppSearchMigration()) {
+ ConsentMigrationUtils.handleConsentMigrationToAppSearchIfNeededV2(
+ context,
+ datastore,
+ appSearchConsentStorageManager,
+ adServicesExtDataManager);
+ }
+ mAppConsentForRStorageManager =
+ new AppConsentForRStorageManager(
+ datastore,
+ appConsentDao,
+ uxStatesDao,
+ adServicesExtDataManager);
+ }
+
+ // Attempt to migrate consent data from PPAPI to System server if needed.
+ handleConsentMigrationIfNeeded(
+ context,
+ datastore,
+ adServicesManager,
+ statsdAdServicesLogger,
+ consentSourceOfTruth);
+
+ AppConsentStorageManager appConsentStorageManager =
+ new AppConsentStorageManager(datastore, appConsentDao, uxStatesDao);
+ sConsentManager =
+ new ConsentManagerV2(
+ TopicsWorker.getInstance(context),
+ appConsentDao,
+ EnrollmentDao.getInstance(context),
+ MeasurementImpl.getInstance(context),
+ CustomAudienceDatabase.getInstance(context).customAudienceDao(),
+ appConsentStorageManager,
+ SharedStorageDatabase.getInstance(context).appInstallDao(),
+ SharedStorageDatabase.getInstance(context).frequencyCapDao(),
+ adServicesManager,
+ datastore,
+ appSearchConsentStorageManager,
+ UserProfileIdManager.getInstance(context),
+ // TODO(b/260601944): Remove Flag Instance.
+ mAppConsentForRStorageManager,
+ FlagsFactory.getFlags(),
+ consentSourceOfTruth,
+ enableAppsearchConsentData,
+ enableAdExtServiceConsentData);
+ }
+ }
+ }
+ return sConsentManager;
+ }
+
+ /**
+ * Enables all PP API services. It gives consent to Topics, Fledge and Measurements services.
+ *
+ * <p>To write consent to PPAPI if consent source of truth is PPAPI_ONLY or dual sources. To
+ * write to system server consent if source of truth is system server or dual sources.
+ */
+ public void enable(@NonNull Context context) {
+ Objects.requireNonNull(context);
+
+ // Check current value, if it is already enabled, skip this enable process. so that the Api
+ // won't be reset. Only add this logic to "enable" not "disable", since if it already
+ // disabled, there is no harm to reset the api again.
+ if (mFlags.getConsentManagerLazyEnableMode() && getConsentFromSourceOfTruth()) {
+ LogUtil.d("CONSENT_KEY already enable. Skipping enable process.");
+ return;
+ }
+ UiStatsLogger.logOptInSelected();
+
+ BackgroundJobsManager.scheduleAllBackgroundJobs(context);
+ try {
+ // reset all state data which should be removed
+ resetTopicsAndBlockedTopics();
+ clearAllAppConsentData();
+ resetMeasurement();
+ resetUserProfileId();
+ mUserProfileIdManager.getOrCreateId();
+ } catch (IOException e) {
+ throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_WHILE_SET_CONTENT, e);
+ }
+ setConsentToSourceOfTruth(/* isGiven */ true);
+ }
+
+ /**
+ * Disables all PP API services. It revokes consent to Topics, Fledge and Measurements services.
+ *
+ * <p>To write consent to PPAPI if consent source of truth is PPAPI_ONLY or dual sources. To
+ * write to system server consent if source of truth is system server or dual sources.
+ */
+ public void disable(@NonNull Context context) {
+ Objects.requireNonNull(context);
+ UiStatsLogger.logOptOutSelected();
+ // Disable all the APIs
+ try {
+ // reset all data
+ resetTopicsAndBlockedTopics();
+ clearAllAppConsentData();
+ resetMeasurement();
+ resetEnrollment();
+ resetUserProfileId();
+
+ BackgroundJobsManager.unscheduleAllBackgroundJobs(
+ context.getSystemService(JobScheduler.class));
+ } catch (IOException e) {
+ throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_WHILE_SET_CONTENT, e);
+ }
+ setConsentToSourceOfTruth(/* isGiven */ false);
+ }
+
+ /**
+ * Enables the {@code apiType} PP API service. It gives consent to an API which is provided in
+ * the parameter.
+ *
+ * <p>To write consent to PPAPI if consent source of truth is PPAPI_ONLY or dual sources. To
+ * write to system server consent if source of truth is system server or dual sources.
+ *
+ * @param context Context of the application.
+ * @param apiType Type of the API (Topics, Fledge, Measurement) which should be enabled.
+ */
+ public void enable(@NonNull Context context, AdServicesApiType apiType) {
+ Objects.requireNonNull(context);
+ // Check current value, if it is already enabled, skip this enable process. so that the Api
+ // won't be reset.
+ if (mFlags.getConsentManagerLazyEnableMode()
+ && getPerApiConsentFromSourceOfTruth(apiType)) {
+ LogUtil.d(
+ "ApiType: is %s already enable. Skipping enable process.",
+ apiType.toPpApiDatastoreKey());
+ return;
+ }
+
+ UiStatsLogger.logOptInSelected(apiType);
+
+ BackgroundJobsManager.scheduleJobsPerApi(context, apiType);
+
+ try {
+ // reset all state data which should be removed
+ resetByApi(apiType);
+
+ if (AdServicesApiType.FLEDGE == apiType) {
+ mUserProfileIdManager.getOrCreateId();
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_WHILE_SET_CONTENT, e);
+ }
+
+ setPerApiConsentToSourceOfTruth(/* isGiven */ true, apiType);
+ }
+
+ /**
+ * Disables {@code apiType} PP API service. It revokes consent to an API which is provided in
+ * the parameter.
+ *
+ * <p>To write consent to PPAPI if consent source of truth is PPAPI_ONLY or dual sources. To
+ * write to system server consent if source of truth is system server or dual sources.
+ */
+ public void disable(@NonNull Context context, AdServicesApiType apiType) {
+ Objects.requireNonNull(context);
+
+ UiStatsLogger.logOptOutSelected(apiType);
+
+ try {
+ resetByApi(apiType);
+ BackgroundJobsManager.unscheduleJobsPerApi(
+ context.getSystemService(JobScheduler.class), apiType);
+ } catch (IOException e) {
+ throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_WHILE_SET_CONTENT, e);
+ }
+
+ setPerApiConsentToSourceOfTruth(/* isGiven */ false, apiType);
+
+ if (areAllApisDisabled()) {
+ BackgroundJobsManager.unscheduleAllBackgroundJobs(
+ context.getSystemService(JobScheduler.class));
+ }
+ }
+
+ private boolean areAllApisDisabled() {
+ if (getConsent(AdServicesApiType.TOPICS).isGiven()
+ || getConsent(AdServicesApiType.MEASUREMENTS).isGiven()
+ || getConsent(AdServicesApiType.FLEDGE).isGiven()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Retrieves the consent for all PP API services.
+ *
+ * <p>To read from PPAPI consent if source of truth is PPAPI. To read from system server consent
+ * if source of truth is system server or dual sources.
+ *
+ * @return AdServicesApiConsent the consent
+ */
+ public AdServicesApiConsent getConsent() {
+ if (mFlags.getConsentManagerDebugMode()) {
+ return AdServicesApiConsent.GIVEN;
+ }
+ return mConsentCompositeStorage.getConsent(AdServicesApiType.ALL_API);
+ }
+
+ /**
+ * Retrieves the consent per API.
+ *
+ * @param apiType apiType for which the consent should be provided
+ * @return {@link AdServicesApiConsent} providing information whether the consent was given or
+ * revoked.
+ */
+ public AdServicesApiConsent getConsent(AdServicesApiType apiType) {
+ if (mFlags.getConsentManagerDebugMode()) {
+ return AdServicesApiConsent.GIVEN;
+ }
+ return mConsentCompositeStorage.getConsent(apiType);
+ }
+
+ /**
+ * Returns whether the user is adult user who OTA from R.
+ *
+ * @return true if user is adult user who OTA from R, otherwise false.
+ */
+ public boolean isOtaAdultUserFromRvc() {
+ if (mFlags.getConsentManagerOTADebugMode()) {
+ return true;
+ }
+ // TODO(313672368) clean up getRvcPostOtaNotifAgeCheck flag after u18 is qualified on R/S
+ return mAppConsentForRStorageManager != null
+ && mAppConsentForRStorageManager.wasU18NotificationDisplayed()
+ && (mFlags.getRvcPostOtaNotifAgeCheck()
+ ? !mAppConsentForRStorageManager.isU18Account()
+ && mAppConsentForRStorageManager.isAdultAccount()
+ : true);
+ }
+
+ /**
+ * Proxy call to {@link TopicsWorker} to get {@link ImmutableList} of {@link Topic}s which could
+ * be returned to the {@link TopicsWorker} clients.
+ *
+ * @return {@link ImmutableList} of {@link Topic}s.
+ */
+ @NonNull
+ public ImmutableList<Topic> getKnownTopicsWithConsent() {
+ return mTopicsWorker.getKnownTopicsWithConsent();
+ }
+
+ /**
+ * Proxy call to {@link TopicsWorker} to get {@link ImmutableList} of {@link Topic}s which were
+ * blocked by the user.
+ *
+ * @return {@link ImmutableList} of blocked {@link Topic}s.
+ */
+ @NonNull
+ public ImmutableList<Topic> getTopicsWithRevokedConsent() {
+ return mTopicsWorker.getTopicsWithRevokedConsent();
+ }
+
+ /**
+ * Proxy call to {@link TopicsWorker} to revoke consent for provided {@link Topic} (block
+ * topic).
+ *
+ * @param topic {@link Topic} to block.
+ */
+ @NonNull
+ public void revokeConsentForTopic(@NonNull Topic topic) {
+ mTopicsWorker.revokeConsentForTopic(topic);
+ }
+
+ /**
+ * Proxy call to {@link TopicsWorker} to restore consent for provided {@link Topic} (unblock the
+ * topic).
+ *
+ * @param topic {@link Topic} to restore consent for.
+ */
+ @NonNull
+ public void restoreConsentForTopic(@NonNull Topic topic) {
+ mTopicsWorker.restoreConsentForTopic(topic);
+ }
+
+ /** Wipes out all the data gathered by Topics API but blocked topics. */
+ public void resetTopics() {
+ ArrayList<String> tablesToBlock = new ArrayList<>();
+ tablesToBlock.add(TopicsTables.BlockedTopicsContract.TABLE);
+ mTopicsWorker.clearAllTopicsData(tablesToBlock);
+ }
+
+ /** Wipes out all the data gathered by Topics API. */
+ public void resetTopicsAndBlockedTopics() {
+ mTopicsWorker.clearAllTopicsData(new ArrayList<>());
+ }
+
+ /**
+ * @return an {@link ImmutableList} of all known apps in the database that have not had user
+ * consent revoked
+ */
+ public ImmutableList<App> getKnownAppsWithConsent() {
+ return ImmutableList.copyOf(
+ mConsentCompositeStorage.getKnownAppsWithConsent().stream()
+ .map(App::create)
+ .collect(Collectors.toList()));
+ }
+
+ /**
+ * @return an {@link ImmutableList} of all known apps in the database that have had user consent
+ * revoked
+ */
+ public ImmutableList<App> getAppsWithRevokedConsent() {
+ return ImmutableList.copyOf(
+ mConsentCompositeStorage.getAppsWithRevokedConsent().stream()
+ .map(App::create)
+ .collect(Collectors.toList()));
+ }
+
+ /**
+ * Proxy call to {@link AppConsentDao} to revoke consent for provided {@link App}.
+ *
+ * <p>Also clears all app data related to the provided {@link App}.
+ *
+ * @param app {@link App} to block.
+ * @throws IOException if the operation fails
+ */
+ public void revokeConsentForApp(@NonNull App app) throws IOException {
+ mConsentCompositeStorage.setConsentForApp(app.getPackageName(), true);
+
+ asyncExecute(
+ () -> mCustomAudienceDao.deleteCustomAudienceDataByOwner(app.getPackageName()));
+ if (mFlags.getFledgeAdSelectionFilteringEnabled()) {
+ asyncExecute(() -> mAppInstallDao.deleteByPackageName(app.getPackageName()));
+ asyncExecute(
+ () -> mFrequencyCapDao.deleteHistogramDataBySourceApp(app.getPackageName()));
+ }
+ }
+
+ /**
+ * Proxy call to {@link AppConsentDao} to restore consent for provided {@link App}.
+ *
+ * @param app {@link App} to restore consent for.
+ * @throws IOException if the operation fails
+ */
+ public void restoreConsentForApp(@NonNull App app) throws IOException {
+ mConsentCompositeStorage.setConsentForApp(app.getPackageName(), false);
+ }
+
+ /**
+ * Deletes all app consent data and all app data gathered or generated by the Privacy Sandbox.
+ *
+ * <p>This should be called when the Privacy Sandbox has been disabled.
+ *
+ * @throws IOException if the operation fails
+ */
+ public void clearAllAppConsentData() throws IOException {
+ mConsentCompositeStorage.clearAllAppConsentData();
+
+ asyncExecute(mCustomAudienceDao::deleteAllCustomAudienceData);
+ if (mFlags.getFledgeAdSelectionFilteringEnabled()) {
+ asyncExecute(mAppInstallDao::deleteAllAppInstallData);
+ asyncExecute(mFrequencyCapDao::deleteAllHistogramData);
+ }
+ }
+
+ /**
+ * Deletes the list of known allowed apps as well as all app data from the Privacy Sandbox.
+ *
+ * <p>The list of blocked apps is not reset.
+ *
+ * @throws IOException if the operation fails
+ */
+ public void clearKnownAppsWithConsent() throws IOException {
+ mConsentCompositeStorage.clearKnownAppsWithConsent();
+ asyncExecute(mCustomAudienceDao::deleteAllCustomAudienceData);
+ if (mFlags.getFledgeAdSelectionFilteringEnabled()) {
+ asyncExecute(mAppInstallDao::deleteAllAppInstallData);
+ asyncExecute(mFrequencyCapDao::deleteAllHistogramData);
+ }
+ }
+
+ /**
+ * Checks whether a single given installed application (identified by its package name) has had
+ * user consent to use the FLEDGE APIs revoked.
+ *
+ * <p>This method also checks whether a user has opted out of the FLEDGE Privacy Sandbox
+ * initiative.
+ *
+ * @param packageName String package name that uniquely identifies an installed application to
+ * check
+ * @return {@code true} if either the FLEDGE Privacy Sandbox initiative has been opted out or if
+ * the user has revoked consent for the given application to use the FLEDGE APIs
+ * @throws IllegalArgumentException if the package name is invalid or not found as an installed
+ * application
+ */
+ public boolean isFledgeConsentRevokedForApp(@NonNull String packageName)
+ throws IllegalArgumentException {
+ AdServicesApiConsent consent = getConsent(AdServicesApiType.FLEDGE);
+
+ if (!consent.isGiven()) {
+ return true;
+ }
+
+ return mConsentCompositeStorage.isConsentRevokedForApp(packageName);
+ }
+
+ /**
+ * Persists the use of a FLEDGE API by a single given installed application (identified by its
+ * package name) if the app has not already had its consent revoked.
+ *
+ * <p>This method also checks whether a user has opted out of the FLEDGE Privacy Sandbox
+ * initiative.
+ *
+ * <p>This is only meant to be called by the FLEDGE APIs.
+ *
+ * @param packageName String package name that uniquely identifies an installed application that
+ * has used a FLEDGE API
+ * @return {@code true} if user consent has been revoked for the application or API, {@code
+ * false} otherwise
+ * @throws IllegalArgumentException if the package name is invalid or not found as an installed
+ * application
+ */
+ public boolean isFledgeConsentRevokedForAppAfterSettingFledgeUse(@NonNull String packageName)
+ throws IllegalArgumentException {
+ AdServicesApiConsent consent = getConsent(AdServicesApiType.FLEDGE);
+
+ if (!consent.isGiven()) {
+ return true;
+ }
+
+ return mConsentCompositeStorage.setConsentForAppIfNew(packageName, false);
+ }
+
+ /**
+ * Clear consent data after an app was uninstalled.
+ *
+ * @param packageName the package name that had been uninstalled.
+ */
+ public void clearConsentForUninstalledApp(String packageName, int packageUid) {
+ mConsentCompositeStorage.clearConsentForUninstalledApp(packageName, packageUid);
+ }
+
+ /**
+ * Clear consent data after an app was uninstalled, but the package Uid is unavailable. This
+ * could happen because the INTERACT_ACROSS_USERS_FULL permission is not available on Android
+ * versions prior to T.
+ *
+ * <p><strong>This method should only be used for R/S back-compat scenarios.</strong>
+ *
+ * @param packageName the package name that had been uninstalled.
+ */
+ public void clearConsentForUninstalledApp(@NonNull String packageName) {
+ mConsentCompositeStorage.clearConsentForUninstalledApp(packageName);
+ }
+
+ /** Wipes out all the data gathered by Measurement API. */
+ public void resetMeasurement() {
+ mMeasurementImpl.deleteAllMeasurementData(List.of());
+ // Log wipeout event triggered by consent flip to delete data of package
+ WipeoutStatus wipeoutStatus = new WipeoutStatus();
+ wipeoutStatus.setWipeoutType(WipeoutStatus.WipeoutType.CONSENT_FLIP);
+ logWipeoutStats(wipeoutStatus);
+ }
+
+ /** Wipes out all the Enrollment data */
+ @VisibleForTesting
+ void resetEnrollment() {
+ mEnrollmentDao.deleteAll();
+ }
+
+ /**
+ * Saves information to the storage that notification was displayed for the first time to the
+ * user.
+ */
+ public void recordNotificationDisplayed(boolean wasNotificationDisplayed) {
+ mConsentCompositeStorage.recordNotificationDisplayed(wasNotificationDisplayed);
+ }
+
+ /**
+ * Retrieves if notification has been displayed.
+ *
+ * @return true if Consent Notification was displayed, otherwise false.
+ */
+ public Boolean wasNotificationDisplayed() {
+ return mConsentCompositeStorage.wasNotificationDisplayed();
+ }
+
+ /**
+ * Saves information to the storage that GA UX notification was displayed for the first time to
+ * the user.
+ */
+ public void recordGaUxNotificationDisplayed(boolean wasGaUxDisplayed) {
+ mConsentCompositeStorage.recordGaUxNotificationDisplayed(wasGaUxDisplayed);
+ }
+
+ /**
+ * Retrieves if GA UX notification has been displayed.
+ *
+ * @return true if GA UX Consent Notification was displayed, otherwise false.
+ */
+ public Boolean wasGaUxNotificationDisplayed() {
+ return mConsentCompositeStorage.wasGaUxNotificationDisplayed();
+ }
+
+ /**
+ * Retrieves the PP API default consent.
+ *
+ * @return true if the topics default consent is true, false otherwise.
+ */
+ public Boolean getDefaultConsent() {
+ return mConsentCompositeStorage.getDefaultConsent(AdServicesApiType.ALL_API).isGiven();
+ }
+
+ /**
+ * Retrieves the topics default consent.
+ *
+ * @return true if the topics default consent is true, false otherwise.
+ */
+ public Boolean getTopicsDefaultConsent() {
+ return mConsentCompositeStorage.getDefaultConsent(AdServicesApiType.TOPICS).isGiven();
+ }
+
+ /**
+ * Retrieves the FLEDGE default consent.
+ *
+ * @return true if the FLEDGE default consent is true, false otherwise.
+ */
+ public Boolean getFledgeDefaultConsent() {
+ return mConsentCompositeStorage.getDefaultConsent(AdServicesApiType.FLEDGE).isGiven();
+ }
+
+ /**
+ * Retrieves the measurement default consent.
+ *
+ * @return true if the measurement default consent is true, false otherwise.
+ */
+ public Boolean getMeasurementDefaultConsent() {
+ return mConsentCompositeStorage.getDefaultConsent(AdServicesApiType.MEASUREMENTS).isGiven();
+ }
+
+ /**
+ * Retrieves the default AdId state.
+ *
+ * @return true if the AdId is enabled by default, false otherwise.
+ */
+ public Boolean getDefaultAdIdState() {
+ return mConsentCompositeStorage.getDefaultAdIdState();
+ }
+
+ /** Saves the default consent bit to data stores based on source of truth. */
+ public void recordDefaultConsent(boolean defaultConsent) {
+ mConsentCompositeStorage.recordDefaultConsent(AdServicesApiType.ALL_API, defaultConsent);
+ }
+
+ /** Saves the topics default consent bit to data stores based on source of truth. */
+ public void recordTopicsDefaultConsent(boolean defaultConsent) {
+ mConsentCompositeStorage.recordDefaultConsent(AdServicesApiType.TOPICS, defaultConsent);
+ }
+
+ /** Saves the FLEDGE default consent bit to data stores based on source of truth. */
+ public void recordFledgeDefaultConsent(boolean defaultConsent) {
+ mConsentCompositeStorage.recordDefaultConsent(AdServicesApiType.FLEDGE, defaultConsent);
+ }
+
+ /** Saves the measurement default consent bit to data stores based on source of truth. */
+ public void recordMeasurementDefaultConsent(boolean defaultConsent) {
+ mConsentCompositeStorage.recordDefaultConsent(
+ AdServicesApiType.MEASUREMENTS, defaultConsent);
+ }
+
+ /** Saves the default AdId state bit to data stores based on source of truth. */
+ public void recordDefaultAdIdState(boolean defaultAdIdState) {
+ mConsentCompositeStorage.recordDefaultAdIdState(defaultAdIdState);
+ }
+
+ /** Set the current privacy sandbox feature. */
+ public void setCurrentPrivacySandboxFeature(PrivacySandboxFeatureType currentFeatureType) {
+ mConsentCompositeStorage.setCurrentPrivacySandboxFeature(currentFeatureType);
+ }
+
+ /** Saves information to the storage that user interacted with consent manually. */
+ public void recordUserManualInteractionWithConsent(@UserManualInteraction int interaction) {
+ mConsentCompositeStorage.recordUserManualInteractionWithConsent(interaction);
+ }
+
+ /**
+ * Get the current privacy sandbox feature.
+ *
+ * <p>To write to PPAPI if consent source of truth is PPAPI_ONLY or dual sources. To write to
+ * system server if consent source of truth is SYSTEM_SERVER_ONLY or dual sources.
+ */
+ public PrivacySandboxFeatureType getCurrentPrivacySandboxFeature() {
+ return mConsentCompositeStorage.getCurrentPrivacySandboxFeature();
+ }
+
+ /**
+ * Returns information whether user interacted with consent manually.
+ *
+ * @return true if the user interacted with the consent manually, otherwise false.
+ */
+ public @UserManualInteraction int getUserManualInteractionWithConsent() {
+ return mConsentCompositeStorage.getUserManualInteractionWithConsent();
+ }
+
+ @VisibleForTesting
+ static BooleanFileDatastore createAndInitializeDataStore(@NonNull Context context) {
+ BooleanFileDatastore booleanFileDatastore =
+ new BooleanFileDatastore(
+ context,
+ ConsentConstants.STORAGE_XML_IDENTIFIER,
+ ConsentConstants.STORAGE_VERSION);
+
+ try {
+ booleanFileDatastore.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 (booleanFileDatastore.get(ConsentConstants.NOTIFICATION_DISPLAYED_ONCE) == null) {
+ booleanFileDatastore.put(ConsentConstants.NOTIFICATION_DISPLAYED_ONCE, false);
+ }
+ if (booleanFileDatastore.get(ConsentConstants.GA_UX_NOTIFICATION_DISPLAYED_ONCE)
+ == null) {
+ booleanFileDatastore.put(ConsentConstants.GA_UX_NOTIFICATION_DISPLAYED_ONCE, false);
+ }
+ } catch (IOException | IllegalArgumentException | NullPointerException e) {
+ throw new RuntimeException("Failed to initialize the File Datastore!", e);
+ }
+
+ return booleanFileDatastore;
+ }
+
+ // Handle different migration requests based on current consent source of Truth
+ // PPAPI_ONLY: reset the shared preference to reset status of migrating consent from PPAPI to
+ // system server.
+ // PPAPI_AND_SYSTEM_SERVER: migrate consent from PPAPI to system server.
+ // SYSTEM_SERVER_ONLY: migrate consent from PPAPI to system server and clear PPAPI consent
+ @VisibleForTesting
+ static void handleConsentMigrationIfNeeded(
+ @NonNull Context context,
+ @NonNull BooleanFileDatastore datastore,
+ AdServicesStorageManager adServicesManager,
+ @NonNull StatsdAdServicesLogger statsdAdServicesLogger,
+ @Flags.ConsentSourceOfTruth int consentSourceOfTruth) {
+ Objects.requireNonNull(context);
+ // On R/S, handleConsentMigrationIfNeeded should never be executed.
+ // It is a T+ feature. On T+, this function should only execute if it's within the
+ // AdServices
+ // APK and not ExtServices. So check if it's within ExtServices, and bail out if that's the
+ // case on any platform.
+ String packageName = context.getPackageName();
+ if (packageName != null && packageName.endsWith(ADEXTSERVICES_PACKAGE_NAME_SUFFIX)) {
+ LogUtil.d("Aborting attempt to migrate consent in ExtServices");
+ return;
+ }
+ Objects.requireNonNull(datastore);
+ if (consentSourceOfTruth == Flags.PPAPI_AND_SYSTEM_SERVER
+ || consentSourceOfTruth == Flags.SYSTEM_SERVER_ONLY) {
+ Objects.requireNonNull(adServicesManager);
+ }
+
+ switch (consentSourceOfTruth) {
+ case Flags.PPAPI_ONLY:
+ // Technically we only need to reset the SHARED_PREFS_KEY_HAS_MIGRATED bit once.
+ // What we need is clearIfSet operation which is not available in SP. So here we
+ // always reset the bit since otherwise we need to read the SP to read the value and
+ // the clear the value.
+ // The only flow we would do are:
+ // Case 1: DUAL-> PPAPI if there is a bug in System Server
+ // Case 2: DUAL -> SYSTEM_SERVER_ONLY: if everything goes smoothly.
+ resetSharedPreference(context, ConsentConstants.SHARED_PREFS_KEY_HAS_MIGRATED);
+ break;
+ case Flags.PPAPI_AND_SYSTEM_SERVER:
+ migratePpApiConsentToSystemService(
+ context, datastore, adServicesManager, statsdAdServicesLogger);
+ break;
+ case Flags.SYSTEM_SERVER_ONLY:
+ migratePpApiConsentToSystemService(
+ context, datastore, adServicesManager, statsdAdServicesLogger);
+ clearPpApiConsent(context, datastore);
+ break;
+ case Flags.APPSEARCH_ONLY:
+ // If this is an S- device, the consent source of truth is always APPSEARCH_ONLY.
+ break;
+ default:
+ break;
+ }
+ }
+
+ // Reset data for the specific AdServicesApiType
+ @VisibleForTesting
+ void resetByApi(AdServicesApiType apiType) throws IOException {
+ switch (apiType) {
+ case TOPICS:
+ resetTopicsAndBlockedTopics();
+ break;
+ case FLEDGE:
+ clearAllAppConsentData();
+ resetUserProfileId();
+ break;
+ case MEASUREMENTS:
+ resetMeasurement();
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void resetUserProfileId() {
+ mUserProfileIdManager.deleteId();
+ }
+
+ // Perform a one-time migration to migrate existing PPAPI Consent
+ @VisibleForTesting
+ // Suppress lint warning for context.getUser in R since this code is unused in R
+ @SuppressWarnings("NewApi")
+ static void migratePpApiConsentToSystemService(
+ @NonNull Context context,
+ @NonNull BooleanFileDatastore datastore,
+ @NonNull AdServicesStorageManager adServicesManager,
+ @NonNull StatsdAdServicesLogger statsdAdServicesLogger) {
+ Objects.requireNonNull(context);
+ Objects.requireNonNull(datastore);
+ Objects.requireNonNull(adServicesManager);
+
+ AppConsents appConsents = null;
+ try {
+ // Exit if migration has happened.
+ SharedPreferences sharedPreferences =
+ FileCompatUtils.getSharedPreferencesHelper(
+ context, ConsentConstants.SHARED_PREFS_CONSENT, Context.MODE_PRIVATE);
+ // If we migrated data to system server either from PPAPI or from AppSearch, do not
+ // attempt another migration of data to system server.
+ boolean shouldSkipMigration =
+ sharedPreferences.getBoolean(
+ ConsentConstants.SHARED_PREFS_KEY_APPSEARCH_HAS_MIGRATED,
+ /* default= */ false)
+ || sharedPreferences.getBoolean(
+ ConsentConstants.SHARED_PREFS_KEY_HAS_MIGRATED,
+ /* default= */ false);
+ if (shouldSkipMigration) {
+ LogUtil.v(
+ "Consent migration has happened to user %d, skip...",
+ context.getUser().getIdentifier());
+ return;
+ }
+ LogUtil.d("Started migrating Consent from PPAPI to System Service");
+
+ Boolean consentKey = Boolean.TRUE.equals(datastore.get(ConsentConstants.CONSENT_KEY));
+
+ // Migrate Consent and Notification Displayed to System Service.
+ // Set consent enabled only when value is TRUE. FALSE and null are regarded as disabled.
+ adServicesManager.setConsent(AdServicesApiType.ALL_API, consentKey);
+ // Set notification displayed only when value is TRUE. FALSE and null are regarded as
+ // not displayed.
+ if (Boolean.TRUE.equals(datastore.get(ConsentConstants.NOTIFICATION_DISPLAYED_ONCE))) {
+ adServicesManager.recordNotificationDisplayed(true);
+ }
+
+ Boolean manualInteractionRecorded =
+ datastore.get(ConsentConstants.MANUAL_INTERACTION_WITH_CONSENT_RECORDED);
+ if (manualInteractionRecorded != null) {
+ adServicesManager.recordUserManualInteractionWithConsent(
+ manualInteractionRecorded ? 1 : -1);
+ }
+
+ // Save migration has happened into shared preferences.
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putBoolean(ConsentConstants.SHARED_PREFS_KEY_HAS_MIGRATED, true);
+ appConsents =
+ AppConsents.builder()
+ .setDefaultConsent(consentKey)
+ .setMsmtConsent(consentKey)
+ .setFledgeConsent(consentKey)
+ .setTopicsConsent(consentKey)
+ .build();
+
+ if (editor.commit()) {
+ LogUtil.d("Finished migrating Consent from PPAPI to System Service");
+ statsdAdServicesLogger.logConsentMigrationStats(
+ getConsentManagerStatsForLogging(
+ appConsents,
+ ConsentMigrationStats.MigrationStatus
+ .SUCCESS_WITH_SHARED_PREF_UPDATED,
+ ConsentMigrationStats.MigrationType.PPAPI_TO_SYSTEM_SERVICE,
+ context));
+ } else {
+ LogUtil.e(
+ "Finished migrating Consent from PPAPI to System Service but shared"
+ + " preference is not updated.");
+ ErrorLogUtil.e(
+ AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SHARED_PREF_UPDATE_FAILURE,
+ AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
+ statsdAdServicesLogger.logConsentMigrationStats(
+ getConsentManagerStatsForLogging(
+ appConsents,
+ ConsentMigrationStats.MigrationStatus
+ .SUCCESS_WITH_SHARED_PREF_NOT_UPDATED,
+ ConsentMigrationStats.MigrationType.PPAPI_TO_SYSTEM_SERVICE,
+ context));
+ }
+ } catch (Exception e) {
+ LogUtil.e("PPAPI consent data migration failed: ", e);
+ statsdAdServicesLogger.logConsentMigrationStats(
+ getConsentManagerStatsForLogging(
+ appConsents,
+ ConsentMigrationStats.MigrationStatus.FAILURE,
+ ConsentMigrationStats.MigrationType.PPAPI_TO_SYSTEM_SERVICE,
+ context));
+ }
+ }
+
+ // Clear PPAPI Consent if fully migrated to use system server consent. This is because system
+ // consent cannot be migrated back to PPAPI. This data clearing should only happen once.
+ @VisibleForTesting
+ static void clearPpApiConsent(
+ @NonNull Context context, @NonNull BooleanFileDatastore datastore) {
+ // Exit if PPAPI consent has cleared.
+ SharedPreferences sharedPreferences =
+ FileCompatUtils.getSharedPreferencesHelper(
+ context, ConsentConstants.SHARED_PREFS_CONSENT, Context.MODE_PRIVATE);
+ if (sharedPreferences.getBoolean(
+ ConsentConstants.SHARED_PREFS_KEY_PPAPI_HAS_CLEARED, /* defValue */ false)) {
+ return;
+ }
+
+ LogUtil.d("Started clearing Consent in PPAPI.");
+
+ try {
+ datastore.clear();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to clear PPAPI Consent", e);
+ }
+
+ // Save that PPAPI consent has cleared into shared preferences.
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putBoolean(ConsentConstants.SHARED_PREFS_KEY_PPAPI_HAS_CLEARED, true);
+
+ if (editor.commit()) {
+ LogUtil.d("Finished clearing Consent in PPAPI.");
+ } else {
+ LogUtil.e("Finished clearing Consent in PPAPI but shared preference is not updated.");
+ ErrorLogUtil.e(
+ AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SHARED_PREF_UPDATE_FAILURE,
+ AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
+ }
+ }
+
+ // Set the shared preference to false for given key.
+ @VisibleForTesting
+ static void resetSharedPreference(
+ @NonNull Context context, @NonNull String sharedPreferenceKey) {
+ Objects.requireNonNull(context);
+ Objects.requireNonNull(sharedPreferenceKey);
+
+ SharedPreferences sharedPreferences =
+ FileCompatUtils.getSharedPreferencesHelper(
+ context, ConsentConstants.SHARED_PREFS_CONSENT, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putBoolean(sharedPreferenceKey, false);
+
+ if (editor.commit()) {
+ LogUtil.d("Finished resetting shared preference for " + sharedPreferenceKey);
+ } else {
+ LogUtil.e("Failed to reset shared preference for " + sharedPreferenceKey);
+ ErrorLogUtil.e(
+ AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SHARED_PREF_RESET_FAILURE,
+ AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
+ }
+ }
+
+ // To write to PPAPI if consent source of truth is PPAPI_ONLY or dual sources.
+ // To write to system server if consent source of truth is SYSTEM_SERVER_ONLY or dual sources.
+ @VisibleForTesting
+ void setConsentToSourceOfTruth(boolean isGiven) {
+ mConsentCompositeStorage.setConsent(AdServicesApiType.ALL_API, isGiven);
+ }
+
+ @VisibleForTesting
+ boolean getConsentFromSourceOfTruth() {
+ return mConsentCompositeStorage.getConsent(AdServicesApiType.ALL_API).isGiven();
+ }
+
+ @VisibleForTesting
+ boolean getPerApiConsentFromSourceOfTruth(AdServicesApiType apiType) {
+ return mConsentCompositeStorage.getConsent(apiType).isGiven();
+ }
+
+ @VisibleForTesting
+ void setPerApiConsentToSourceOfTruth(boolean isGiven, AdServicesApiType apiType) {
+ mConsentCompositeStorage.setConsent(apiType, isGiven);
+ }
+
+ private static void storeUserManualInteractionToPpApi(
+ @ConsentManagerV2.UserManualInteraction int interaction, BooleanFileDatastore datastore)
+ throws IOException {
+ switch (interaction) {
+ case NO_MANUAL_INTERACTIONS_RECORDED:
+ datastore.put(ConsentConstants.MANUAL_INTERACTION_WITH_CONSENT_RECORDED, false);
+ break;
+ case UNKNOWN:
+ datastore.remove(ConsentConstants.MANUAL_INTERACTION_WITH_CONSENT_RECORDED);
+ break;
+ case MANUAL_INTERACTIONS_RECORDED:
+ datastore.put(ConsentConstants.MANUAL_INTERACTION_WITH_CONSENT_RECORDED, true);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ String.format("InteractionId < %d > can not be handled.", interaction));
+ }
+ }
+
+ /**
+ * This method handles migration of consent data from AppSearch to AdServices. Consent data is
+ * written to AppSearch on S- and ported to AdServices after OTA to T. If any new data is
+ * written for consent, we need to make sure it is migrated correctly post-OTA in this method.
+ */
+ @VisibleForTesting
+ static void handleConsentMigrationFromAppSearchIfNeeded(
+ @NonNull Context context,
+ @NonNull BooleanFileDatastore datastore,
+ @NonNull AppConsentDao appConsentDao,
+ @NonNull AppSearchConsentStorageManager appSearchConsentStorageManager,
+ @NonNull AdServicesStorageManager adServicesStorageManager,
+ @NonNull StatsdAdServicesLogger statsdAdServicesLogger) {
+ Objects.requireNonNull(context);
+ Objects.requireNonNull(appSearchConsentStorageManager);
+ LogUtil.d("Check migrating Consent from AppSearch to PPAPI and System Service");
+
+ // On R/S, this function should never be executed because AppSearch to PPAPI and
+ // System Server migration is a T+ feature. On T+, this function should only execute
+ // if it's within the AdServices APK and not ExtServices. So check if it's within
+ // ExtServices, and bail out if that's the case on any platform.
+ String packageName = context.getPackageName();
+ if (packageName != null && packageName.endsWith(ADEXTSERVICES_PACKAGE_NAME_SUFFIX)) {
+ LogUtil.d(
+ "Aborting attempt to migrate AppSearch to PPAPI and System Service in"
+ + " ExtServices");
+ return;
+ }
+
+ AppConsents appConsents = null;
+ try {
+ // This should be called only once after OTA (if flag is enabled). If we did not record
+ // showing the notification on T+ yet and we have shown the notification on S- (as
+ // recorded
+ // in AppSearch), initialize T+ consent data so that we don't show notification twice
+ // (after
+ // OTA upgrade).
+ SharedPreferences sharedPreferences =
+ FileCompatUtils.getSharedPreferencesHelper(
+ context, ConsentConstants.SHARED_PREFS_CONSENT, Context.MODE_PRIVATE);
+ // If we did not migrate notification data, we should not attempt to migrate anything.
+ if (!appSearchConsentStorageManager.migrateConsentDataIfNeeded(
+ sharedPreferences, datastore, adServicesStorageManager, appConsentDao)) {
+ LogUtil.d("Skipping consent migration from AppSearch");
+ return;
+ }
+ // Migrate Consent for all APIs and per API to PP API and System Service.
+ appConsents =
+ migrateAppSearchConsents(
+ appSearchConsentStorageManager, adServicesStorageManager, datastore);
+ // Record interactions data only if we recorded an interaction in AppSearch.
+ int manualInteractionRecorded =
+ appSearchConsentStorageManager.getUserManualInteractionWithConsent();
+ if (manualInteractionRecorded == MANUAL_INTERACTIONS_RECORDED) {
+ // Initialize PP API datastore.
+ storeUserManualInteractionToPpApi(MANUAL_INTERACTIONS_RECORDED, datastore);
+ // Initialize system service.
+ adServicesStorageManager.recordUserManualInteractionWithConsent(
+ manualInteractionRecorded);
+ }
+
+ // Record that we migrated consent data from AppSearch. We write the notification data
+ // to system server and perform migration only if system server did not record any
+ // notification having been displayed.
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putBoolean(ConsentConstants.SHARED_PREFS_KEY_APPSEARCH_HAS_MIGRATED, true);
+ if (editor.commit()) {
+ LogUtil.d("Finished migrating Consent from AppSearch to PPAPI + System Service");
+ statsdAdServicesLogger.logConsentMigrationStats(
+ getConsentManagerStatsForLogging(
+ appConsents,
+ ConsentMigrationStats.MigrationStatus
+ .SUCCESS_WITH_SHARED_PREF_UPDATED,
+ ConsentMigrationStats.MigrationType.APPSEARCH_TO_SYSTEM_SERVICE,
+ context));
+ } else {
+ LogUtil.e(
+ "Finished migrating Consent from AppSearch to PPAPI + System Service "
+ + "but shared preference is not updated.");
+ statsdAdServicesLogger.logConsentMigrationStats(
+ getConsentManagerStatsForLogging(
+ appConsents,
+ ConsentMigrationStats.MigrationStatus
+ .SUCCESS_WITH_SHARED_PREF_NOT_UPDATED,
+ ConsentMigrationStats.MigrationType.APPSEARCH_TO_SYSTEM_SERVICE,
+ context));
+ }
+ } catch (IOException e) {
+ LogUtil.e("AppSearch consent data migration failed: ", e);
+ ErrorLogUtil.e(
+ e,
+ AD_SERVICES_ERROR_REPORTED__ERROR_CODE__APP_SEARCH_DATA_MIGRATION_FAILURE,
+ AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__UX);
+ statsdAdServicesLogger.logConsentMigrationStats(
+ getConsentManagerStatsForLogging(
+ appConsents,
+ ConsentMigrationStats.MigrationStatus.FAILURE,
+ ConsentMigrationStats.MigrationType.APPSEARCH_TO_SYSTEM_SERVICE,
+ context));
+ }
+ }
+
+ /**
+ * This method returns and migrates the consent states (opt in/out) for all PPAPIs, each API and
+ * their default consent values.
+ */
+ @VisibleForTesting
+ static AppConsents migrateAppSearchConsents(
+ AppSearchConsentStorageManager appSearchConsentManager,
+ AdServicesStorageManager adServicesManager,
+ BooleanFileDatastore datastore)
+ throws IOException {
+ Map<String, Boolean> consentMap = new HashMap<>();
+ for (AdServicesApiType apiType : AdServicesApiType.values()) {
+ if (apiType == AdServicesApiType.UNKNOWN) {
+ continue;
+ }
+ boolean consented = appSearchConsentManager.getConsent(apiType).isGiven();
+ datastore.put(apiType.toPpApiDatastoreKey(), consented);
+ adServicesManager.setConsent(apiType, consented);
+ boolean defaultConsent = appSearchConsentManager.getDefaultConsent(apiType).isGiven();
+ datastore.put(apiType.toDefaultConsentDatastoreKey(), defaultConsent);
+ adServicesManager.recordDefaultConsent(apiType, defaultConsent);
+ consentMap.put(apiType.toPpApiDatastoreKey(), consented);
+ consentMap.put(apiType.toDefaultConsentDatastoreKey(), defaultConsent);
+ }
+ return AppConsents.builder()
+ .setMsmtConsent(
+ consentMap.get(AdServicesApiType.MEASUREMENTS.toPpApiDatastoreKey()))
+ .setTopicsConsent(consentMap.get(AdServicesApiType.TOPICS.toPpApiDatastoreKey()))
+ .setFledgeConsent(consentMap.get(AdServicesApiType.FLEDGE.toPpApiDatastoreKey()))
+ .setDefaultConsent(
+ consentMap.get(AdServicesApiType.ALL_API.toDefaultConsentDatastoreKey()))
+ .build();
+ }
+
+ /**
+ * Represents revoked consent as internally determined by the PP APIs.
+ *
+ * <p>This is an internal-only exception and is not meant to be returned to external callers.
+ */
+ public static class RevokedConsentException extends IllegalStateException {
+ public static final String REVOKED_CONSENT_ERROR_MESSAGE =
+ "Error caused by revoked user consent";
+
+ /** Creates an instance of a {@link RevokedConsentException}. */
+ public RevokedConsentException() {
+ super(REVOKED_CONSENT_ERROR_MESSAGE);
+ }
+ }
+
+ private void asyncExecute(Runnable runnable) {
+ AdServicesExecutors.getBackgroundExecutor().execute(runnable);
+ }
+
+ private void logWipeoutStats(WipeoutStatus wipeoutStatus) {
+ AdServicesLoggerImpl.getInstance()
+ .logMeasurementWipeoutStats(
+ new MeasurementWipeoutStats.Builder()
+ .setCode(AD_SERVICES_MEASUREMENT_WIPEOUT)
+ .setWipeoutType(wipeoutStatus.getWipeoutType().getValue())
+ .build());
+ }
+
+ /** Returns whether the isAdIdEnabled bit is true based on consent_source_of_truth. */
+ public Boolean isAdIdEnabled() {
+ return mConsentCompositeStorage.isAdIdEnabled();
+ }
+
+ /** Set the AdIdEnabled bit to storage based on consent_source_of_truth. */
+ public void setAdIdEnabled(boolean isAdIdEnabled) {
+ mConsentCompositeStorage.setAdIdEnabled(isAdIdEnabled);
+ }
+
+ /** Returns whether the isU18Account bit is true based on consent_source_of_truth. */
+ public Boolean isU18Account() {
+ return mConsentCompositeStorage.isU18Account();
+ }
+
+ /** Set the U18Account bit to storage based on consent_source_of_truth. */
+ public void setU18Account(boolean isU18Account) {
+ mConsentCompositeStorage.setU18Account(isU18Account);
+ }
+
+ /** Returns whether the isEntryPointEnabled bit is true based on consent_source_of_truth. */
+ public Boolean isEntryPointEnabled() {
+ return mConsentCompositeStorage.isEntryPointEnabled();
+ }
+
+ /** Set the EntryPointEnabled bit to storage based on consent_source_of_truth. */
+ public void setEntryPointEnabled(boolean isEntryPointEnabled) {
+ mConsentCompositeStorage.setEntryPointEnabled(isEntryPointEnabled);
+ }
+
+ /** Returns whether the isAdultAccount bit is true based on consent_source_of_truth. */
+ public Boolean isAdultAccount() {
+ return mConsentCompositeStorage.isAdultAccount();
+ }
+
+ /** Set the AdultAccount bit to storage based on consent_source_of_truth. */
+ public void setAdultAccount(boolean isAdultAccount) {
+ mConsentCompositeStorage.setAdultAccount(isAdultAccount);
+ }
+
+ /**
+ * Returns whether the wasU18NotificationDisplayed bit is true based on consent_source_of_truth.
+ */
+ public Boolean wasU18NotificationDisplayed() {
+ return mConsentCompositeStorage.wasU18NotificationDisplayed();
+ }
+
+ /** Set the U18NotificationDisplayed bit to storage based on consent_source_of_truth. */
+ public void setU18NotificationDisplayed(boolean wasU18NotificationDisplayed) {
+ mConsentCompositeStorage.setU18NotificationDisplayed(wasU18NotificationDisplayed);
+ }
+
+ /** Returns current UX based on consent_source_of_truth. */
+ public PrivacySandboxUxCollection getUx() {
+ return mConsentCompositeStorage.getUx();
+ }
+
+ /** Set the current UX to storage based on consent_source_of_truth. */
+ public void setUx(PrivacySandboxUxCollection ux) {
+ mConsentCompositeStorage.setUx(ux);
+ }
+
+ /** Returns current enrollment channel based on consent_source_of_truth. */
+ public PrivacySandboxEnrollmentChannelCollection getEnrollmentChannel(
+ PrivacySandboxUxCollection ux) {
+ return mConsentCompositeStorage.getEnrollmentChannel(ux);
+ }
+
+ /** Set the current enrollment channel to storage based on consent_source_of_truth. */
+ public void setEnrollmentChannel(
+ PrivacySandboxUxCollection ux, PrivacySandboxEnrollmentChannelCollection channel) {
+ mConsentCompositeStorage.setEnrollmentChannel(ux, channel);
+ }
+
+ @VisibleForTesting
+ void setConsentToPpApi(boolean isGiven) throws IOException {
+ mDatastore.put(ConsentConstants.CONSENT_KEY, isGiven);
+ }
+
+ /* Returns the region od the device */
+ private static int getConsentRegion(@NonNull Context context) {
+ return DeviceRegionProvider.isEuDevice(context)
+ ? AD_SERVICES_SETTINGS_USAGE_REPORTED__REGION__EU
+ : AD_SERVICES_SETTINGS_USAGE_REPORTED__REGION__ROW;
+ }
+
+ /* Returns an object of ConsentMigrationStats */
+ private static ConsentMigrationStats getConsentManagerStatsForLogging(
+ AppConsents appConsents,
+ ConsentMigrationStats.MigrationStatus migrationStatus,
+ ConsentMigrationStats.MigrationType migrationType,
+ Context context) {
+ ConsentMigrationStats consentMigrationStats =
+ ConsentMigrationStats.builder()
+ .setMigrationType(migrationType)
+ .setMigrationStatus(migrationStatus)
+ // When appConsents is null we log it as a failure
+ .setMigrationStatus(
+ appConsents != null
+ ? migrationStatus
+ : ConsentMigrationStats.MigrationStatus.FAILURE)
+ .setMsmtConsent(appConsents == null || appConsents.getMsmtConsent())
+ .setTopicsConsent(appConsents == null || appConsents.getTopicsConsent())
+ .setFledgeConsent(appConsents == null || appConsents.getFledgeConsent())
+ .setDefaultConsent(appConsents == null || appConsents.getDefaultConsent())
+ .setRegion(getConsentRegion(context))
+ .build();
+ return consentMigrationStats;
+ }
+}
diff --git a/adservices/service-core/java/com/android/adservices/service/consent/ConsentMigrationUtils.java b/adservices/service-core/java/com/android/adservices/service/consent/ConsentMigrationUtils.java
index d5ccb28f37..9bd7afd617 100644
--- a/adservices/service-core/java/com/android/adservices/service/consent/ConsentMigrationUtils.java
+++ b/adservices/service-core/java/com/android/adservices/service/consent/ConsentMigrationUtils.java
@@ -20,6 +20,8 @@ import static android.adservices.extdata.AdServicesExtDataParams.BOOLEAN_TRUE;
import static android.adservices.extdata.AdServicesExtDataParams.BOOLEAN_UNKNOWN;
import static android.adservices.extdata.AdServicesExtDataParams.STATE_MANUAL_INTERACTIONS_RECORDED;
+import static com.android.adservices.service.consent.ConsentManager.getConsentManagerStatsForLogging;
+
import android.adservices.extdata.AdServicesExtDataParams;
import android.annotation.NonNull;
import android.annotation.TargetApi;
@@ -32,8 +34,11 @@ import androidx.annotation.Nullable;
import com.android.adservices.LogUtil;
import com.android.adservices.data.common.BooleanFileDatastore;
import com.android.adservices.service.appsearch.AppSearchConsentManager;
+import com.android.adservices.service.appsearch.AppSearchConsentStorageManager;
import com.android.adservices.service.common.compat.FileCompatUtils;
import com.android.adservices.service.extdata.AdServicesExtDataStorageServiceManager;
+import com.android.adservices.service.stats.ConsentMigrationStats;
+import com.android.adservices.service.stats.StatsdAdServicesLogger;
import com.android.modules.utils.build.SdkLevel;
import java.util.Objects;
@@ -50,10 +55,77 @@ public final class ConsentMigrationUtils {
* as it's the new consent source of truth. If any new data is written for consent, we need to
* make sure it is migrated correctly post-OTA in this method.
*/
+ @TargetApi(Build.VERSION_CODES.S)
public static void handleConsentMigrationToAppSearchIfNeeded(
@NonNull Context context,
@NonNull BooleanFileDatastore datastore,
@Nullable AppSearchConsentManager appSearchConsentManager,
+ @Nullable AdServicesExtDataStorageServiceManager adExtDataManager,
+ @Nullable StatsdAdServicesLogger statsdAdServicesLogger) {
+ Objects.requireNonNull(context);
+ Objects.requireNonNull(datastore);
+ Objects.requireNonNull(statsdAdServicesLogger);
+ LogUtil.d("Check if consent migration to AppSearch is needed.");
+ AppConsents appConsents = null;
+ try {
+ SharedPreferences sharedPreferences =
+ FileCompatUtils.getSharedPreferencesHelper(
+ context, ConsentConstants.SHARED_PREFS_CONSENT, Context.MODE_PRIVATE);
+
+ if (!isMigrationToAppSearchNeeded(
+ context, sharedPreferences, appSearchConsentManager, adExtDataManager)) {
+ LogUtil.d("Skipping consent migration to AppSearch");
+ return;
+ }
+
+ // Reduce number of read calls by fetching all the AdExt data at once.
+ AdServicesExtDataParams dataFromR = adExtDataManager.getAdServicesExtData();
+ if (dataFromR.getIsNotificationDisplayed() != BOOLEAN_TRUE) {
+ LogUtil.d("Skipping consent migration to AppSearch; notification not shown on R");
+ return;
+ }
+
+ appConsents = migrateDataToAppSearch(appSearchConsentManager, dataFromR, datastore);
+
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putBoolean(ConsentConstants.SHARED_PREFS_KEY_HAS_MIGRATED_TO_APP_SEARCH, true);
+ if (editor.commit()) {
+ LogUtil.d("Finished migrating consent to AppSearch.");
+ logMigrationToAppSearch(
+ statsdAdServicesLogger,
+ appConsents,
+ ConsentMigrationStats.MigrationStatus.SUCCESS_WITH_SHARED_PREF_UPDATED,
+ context);
+ } else {
+ LogUtil.e("Finished migrating consent to AppSearch. Shared prefs not updated.");
+ logMigrationToAppSearch(
+ statsdAdServicesLogger,
+ appConsents,
+ ConsentMigrationStats.MigrationStatus.SUCCESS_WITH_SHARED_PREF_NOT_UPDATED,
+ context);
+ }
+ // No longer need access to Android R data. Safe to clear here.
+ adExtDataManager.clearDataOnOtaAsync();
+ } catch (Exception e) {
+ LogUtil.e("Consent migration to AppSearch failed: ", e);
+ logMigrationToAppSearch(
+ statsdAdServicesLogger,
+ appConsents,
+ ConsentMigrationStats.MigrationStatus.FAILURE,
+ context);
+ }
+ }
+
+ /**
+ * This method handles migration of consent data to AppSearch post-OTA R -> S. Consent data is
+ * written to AdServicesExtDataStorageService on R and ported over to AppSearch after OTA to S
+ * as it's the new consent source of truth. If any new data is written for consent, we need to
+ * make sure it is migrated correctly post-OTA in this method.
+ */
+ public static void handleConsentMigrationToAppSearchIfNeededV2(
+ @NonNull Context context,
+ @NonNull BooleanFileDatastore datastore,
+ @Nullable AppSearchConsentStorageManager appSearchConsentManager,
@Nullable AdServicesExtDataStorageServiceManager adExtDataManager) {
Objects.requireNonNull(context);
Objects.requireNonNull(datastore);
@@ -65,7 +137,7 @@ public final class ConsentMigrationUtils {
FileCompatUtils.getSharedPreferencesHelper(
context, ConsentConstants.SHARED_PREFS_CONSENT, Context.MODE_PRIVATE);
- if (!isMigrationToAppSearchNeeded(
+ if (!isMigrationToAppSearchNeededV2(
context, sharedPreferences, appSearchConsentManager, adExtDataManager)) {
LogUtil.d("Skipping consent migration to AppSearch");
return;
@@ -78,7 +150,7 @@ public final class ConsentMigrationUtils {
return;
}
- migrateDataToAppSearch(appSearchConsentManager, dataFromR, datastore);
+ migrateDataToAppSearchV2(appSearchConsentManager, dataFromR, datastore);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(ConsentConstants.SHARED_PREFS_KEY_HAS_MIGRATED_TO_APP_SEARCH, true);
@@ -143,8 +215,56 @@ public final class ConsentMigrationUtils {
return !isNotificationDisplayedOnS;
}
+ private static boolean isMigrationToAppSearchNeededV2(
+ Context context,
+ SharedPreferences sharedPreferences,
+ AppSearchConsentStorageManager appSearchConsentManager,
+ AdServicesExtDataStorageServiceManager adExtDataManager) {
+ if (SdkLevel.isAtLeastT() || !SdkLevel.isAtLeastS()) {
+ LogUtil.d("Not S device. Consent migration to AppSearch not needed");
+ return false;
+ }
+
+ // Cannot be null on S since the consent source of truth has to be APPSEARCH_ONLY.
+ Objects.requireNonNull(appSearchConsentManager);
+
+ // There could be a case where we may need to ramp down enable_adext_service_consent_data
+ // flag on S, in which case we should gracefully handle consent migration by skipping.
+ if (adExtDataManager == null) {
+ LogUtil.d("AdExtDataManager is null. Consent migration to AppSearch not needed");
+ return false;
+ }
+
+ boolean isMigrationToAppSearchDone =
+ sharedPreferences.getBoolean(
+ ConsentConstants.SHARED_PREFS_KEY_HAS_MIGRATED_TO_APP_SEARCH,
+ /* defValue= */ false);
+ if (isMigrationToAppSearchDone) {
+ LogUtil.d(
+ "Consent migration to AppSearch is already done for user %d.",
+ context.getUser().getIdentifier());
+ return false;
+ }
+
+ // Just in case, check all notification types to ensure notification is not shown. We do not
+ // want to override consent if notification is already shown.
+ boolean isNotificationDisplayedOnS =
+ appSearchConsentManager.wasU18NotificationDisplayed()
+ || appSearchConsentManager.wasNotificationDisplayed()
+ || appSearchConsentManager.wasGaUxNotificationDisplayed();
+ LogUtil.d(
+ "Notification shown status on S for migrating consent to AppSearch: "
+ + isNotificationDisplayedOnS);
+
+ // If notification is not shown, we will need to perform another check to ensure
+ // notification was shown on R before performing migration. This check will be performed
+ // later in order to reduce number of calls to AdExtDataService in the consent migration
+ // process.
+ return !isNotificationDisplayedOnS;
+ }
+
@TargetApi(Build.VERSION_CODES.S)
- private static void migrateDataToAppSearch(
+ private static AppConsents migrateDataToAppSearch(
AppSearchConsentManager appSearchConsentManager,
AdServicesExtDataParams dataFromR,
BooleanFileDatastore datastore) {
@@ -178,5 +298,72 @@ public final class ConsentMigrationUtils {
if (dataFromR.getIsAdultAccount() != BOOLEAN_UNKNOWN) {
appSearchConsentManager.setAdultAccount(dataFromR.getIsAdultAccount() == BOOLEAN_TRUE);
}
+
+ // Logging false for fledge and topics consent by default because only measurement is
+ // supported on R.
+ AppConsents appConsents =
+ AppConsents.builder()
+ .setDefaultConsent(
+ measurementDefaultConsent != null
+ ? measurementDefaultConsent
+ : false)
+ .setMsmtConsent(isMeasurementConsented)
+ .setFledgeConsent(false)
+ .setTopicsConsent(false)
+ .build();
+ return appConsents;
+ }
+
+ @TargetApi(Build.VERSION_CODES.S)
+ private static void logMigrationToAppSearch(
+ StatsdAdServicesLogger statsdAdServicesLogger,
+ AppConsents appConsents,
+ ConsentMigrationStats.MigrationStatus migrationStatus,
+ Context context) {
+ statsdAdServicesLogger.logConsentMigrationStats(
+ getConsentManagerStatsForLogging(
+ appConsents,
+ migrationStatus,
+ ConsentMigrationStats.MigrationType.ADEXT_SERVICE_TO_APPSEARCH,
+ context));
+ }
+
+ @TargetApi(Build.VERSION_CODES.S)
+ private static void migrateDataToAppSearchV2(
+ AppSearchConsentStorageManager appSearchConsentStorageManager,
+ AdServicesExtDataParams dataFromR,
+ BooleanFileDatastore datastore) {
+ // Default measurement consent is stored using PPAPI_ONLY source on R.
+ Boolean measurementDefaultConsent =
+ datastore.get(ConsentConstants.MEASUREMENT_DEFAULT_CONSENT);
+ if (measurementDefaultConsent != null) {
+ appSearchConsentStorageManager.recordDefaultConsent(
+ AdServicesApiType.MEASUREMENTS, measurementDefaultConsent);
+ }
+
+ boolean isMeasurementConsented = dataFromR.getIsMeasurementConsented() == BOOLEAN_TRUE;
+ appSearchConsentStorageManager.setConsent(
+ AdServicesApiType.MEASUREMENTS, isMeasurementConsented);
+
+ appSearchConsentStorageManager.setU18NotificationDisplayed(
+ dataFromR.getIsNotificationDisplayed() == BOOLEAN_TRUE);
+
+ // Record interaction data only if we recorded an interaction in
+ // AdServicesExtDataStorageService.
+ int manualInteractionRecorded = dataFromR.getManualInteractionWithConsentStatus();
+ if (manualInteractionRecorded == STATE_MANUAL_INTERACTIONS_RECORDED) {
+ appSearchConsentStorageManager.recordUserManualInteractionWithConsent(
+ manualInteractionRecorded);
+ }
+
+ if (dataFromR.getIsU18Account() != BOOLEAN_UNKNOWN) {
+ appSearchConsentStorageManager.setU18Account(
+ dataFromR.getIsU18Account() == BOOLEAN_TRUE);
+ }
+
+ if (dataFromR.getIsAdultAccount() != BOOLEAN_UNKNOWN) {
+ appSearchConsentStorageManager.setAdultAccount(
+ dataFromR.getIsAdultAccount() == BOOLEAN_TRUE);
+ }
}
}
diff --git a/adservices/service-core/java/com/android/adservices/service/consent/IConsentStorage.java b/adservices/service-core/java/com/android/adservices/service/consent/IConsentStorage.java
index 0c21645d7b..ebd47b8da1 100644
--- a/adservices/service-core/java/com/android/adservices/service/consent/IConsentStorage.java
+++ b/adservices/service-core/java/com/android/adservices/service/consent/IConsentStorage.java
@@ -117,7 +117,7 @@ public interface IConsentStorage {
*
* @return true if the AdId is enabled by default, false otherwise.
*/
- boolean getDefaultAdIdState();
+ boolean getDefaultAdIdState() throws IOException;
/**
* Retrieves the default consent.
@@ -130,7 +130,7 @@ public interface IConsentStorage {
/** Returns current enrollment channel. */
@NonNull
PrivacySandboxEnrollmentChannelCollection getEnrollmentChannel(
- @NonNull PrivacySandboxUxCollection ux);
+ @NonNull PrivacySandboxUxCollection ux) throws IOException;
/**
* @return an {@link ImmutableList} of all known apps in the database that have not had user
@@ -148,13 +148,13 @@ public interface IConsentStorage {
/** Returns current UX. */
@NonNull
- PrivacySandboxUxCollection getUx();
+ PrivacySandboxUxCollection getUx() throws IOException;
/** Returns whether the isAdIdEnabled bit is true. */
- boolean isAdIdEnabled();
+ boolean isAdIdEnabled() throws IOException;
/** Returns whether the isAdultAccount bit is true. */
- boolean isAdultAccount();
+ boolean isAdultAccount() throws IOException;
/**
* Returns whether a given application (identified by package name) has had user consent
@@ -167,13 +167,14 @@ public interface IConsentStorage {
* application
* @throws IOException if the operation fails
*/
- boolean isConsentRevokedForApp(@NonNull String packageName) throws IllegalArgumentException;
+ boolean isConsentRevokedForApp(@NonNull String packageName)
+ throws IllegalArgumentException, IOException;
/** Returns whether the isEntryPointEnabled bit is true. */
- boolean isEntryPointEnabled();
+ boolean isEntryPointEnabled() throws IOException;
/** Returns whether the isU18Account bit is true. */
- boolean isU18Account();
+ boolean isU18Account() throws IOException;
/** Saves the default AdId state bit to data stores based on source of truth. */
void recordDefaultAdIdState(boolean defaultAdIdState) throws IOException;
@@ -242,7 +243,8 @@ public interface IConsentStorage {
/** Sets the current enrollment channel to storage. */
void setEnrollmentChannel(
@NonNull PrivacySandboxUxCollection ux,
- @NonNull PrivacySandboxEnrollmentChannelCollection channel);
+ @NonNull PrivacySandboxEnrollmentChannelCollection channel)
+ throws IOException;
/** Sets the EntryPointEnabled bit to storage . */
void setEntryPointEnabled(boolean isEntryPointEnabled) throws IOException;
@@ -254,14 +256,14 @@ public interface IConsentStorage {
void setU18NotificationDisplayed(boolean wasU18NotificationDisplayed) throws IOException;
/** Sets the current UX to storage. */
- void setUx(PrivacySandboxUxCollection ux);
+ void setUx(PrivacySandboxUxCollection ux) throws IOException;
/**
* Retrieves if GA UX notification has been displayed.
*
* @return true if GA UX Consent Notification was displayed, otherwise false.
*/
- boolean wasGaUxNotificationDisplayed();
+ boolean wasGaUxNotificationDisplayed() throws IOException;
/**
* Retrieves if notification has been displayed.
@@ -271,5 +273,5 @@ public interface IConsentStorage {
boolean wasNotificationDisplayed() throws IOException;
/** Returns whether the wasU18NotificationDisplayed bit is true. */
- boolean wasU18NotificationDisplayed();
+ boolean wasU18NotificationDisplayed() throws IOException;
}