diff options
Diffstat (limited to 'adservices/service-core/java/com/android/adservices/service/consent')
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; } |