diff options
Diffstat (limited to 'adservices/service-core/java/com/android/adservices/service')
73 files changed, 3881 insertions, 1349 deletions
diff --git a/adservices/service-core/java/com/android/adservices/service/Flags.java b/adservices/service-core/java/com/android/adservices/service/Flags.java index 200ccec1f5..774504370c 100644 --- a/adservices/service-core/java/com/android/adservices/service/Flags.java +++ b/adservices/service-core/java/com/android/adservices/service/Flags.java @@ -117,6 +117,18 @@ public interface Flags extends CommonFlags { } /** + * Flag to override base64 public key used for encryption testing. + * + * <p>Note: Default value for this flag should not be changed from empty. + */ + String TOPICS_TEST_ENCRYPTION_PUBLIC_KEY = ""; + + /** Returns test public key used for encrypting topics for testing. */ + default String getTopicsTestEncryptionPublicKey() { + return TOPICS_TEST_ENCRYPTION_PUBLIC_KEY; + } + + /** * Returns the number of epochs to look back when deciding if a caller has observed a topic * before. */ @@ -697,7 +709,7 @@ public interface Flags extends CommonFlags { return MEASUREMENT_FLEX_API_MAX_INFORMATION_GAIN_EVENT; } - float MEASUREMENT_FLEX_API_MAX_INFORMATION_GAIN_NAVIGATION = 11.46173F; + float MEASUREMENT_FLEX_API_MAX_INFORMATION_GAIN_NAVIGATION = 11.5F; /** Returns max information gain in Flexible Event API for Navigation sources */ default float getMeasurementFlexApiMaxInformationGainNavigation() { @@ -3419,14 +3431,6 @@ public interface Flags extends CommonFlags { return DEFAULT_ADSERVICES_VERSION_MAPPINGS; } - /** Default Determines whether EU notification flow change is enabled. */ - boolean DEFAULT_EU_NOTIF_FLOW_CHANGE_ENABLED = true; - - /** Determines whether EU notification flow change is enabled. */ - default boolean getEuNotifFlowChangeEnabled() { - return DEFAULT_EU_NOTIF_FLOW_CHANGE_ENABLED; - } - /** Default value for Measurement flexible event reporting API */ boolean MEASUREMENT_FLEXIBLE_EVENT_REPORTING_API_ENABLED = false; @@ -3518,14 +3522,6 @@ public interface Flags extends CommonFlags { return MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS; } - /** Disable early reporting windows configurability by default. */ - boolean MEASUREMENT_ENABLE_CONFIGURABLE_EVENT_REPORTING_WINDOWS = false; - - /** Returns true if event reporting windows configurability is enabled, false otherwise. */ - default boolean getMeasurementEnableConfigurableEventReportingWindows() { - return MEASUREMENT_ENABLE_CONFIGURABLE_EVENT_REPORTING_WINDOWS; - } - /** * Default early reporting windows for VTC type source. Derived from {@link * com.android.adservices.service.measurement.PrivacyParams#EVENT_EARLY_REPORTING_WINDOW_MILLISECONDS}. @@ -3585,18 +3581,8 @@ public interface Flags extends CommonFlags { return MEASUREMENT_AGGREGATE_REPORT_DELAY_CONFIG; } - /** Disable conversions configurability by default. */ - boolean DEFAULT_MEASUREMENT_ENABLE_VTC_CONFIGURABLE_MAX_EVENT_REPORTS = false; - - /** - * Returns true, if event reports max conversions configurability is enabled, false otherwise. - */ - default boolean getMeasurementEnableVtcConfigurableMaxEventReports() { - return DEFAULT_MEASUREMENT_ENABLE_VTC_CONFIGURABLE_MAX_EVENT_REPORTS; - } - - /** Disable conversions configurability by default. */ - int DEFAULT_MEASUREMENT_VTC_CONFIGURABLE_MAX_EVENT_REPORTS_COUNT = 2; + /** Default max allowed number of event reports. */ + int DEFAULT_MEASUREMENT_VTC_CONFIGURABLE_MAX_EVENT_REPORTS_COUNT = 1; /** Returns the default max allowed number of event reports. */ default int getMeasurementVtcConfigurableMaxEventReportsCount() { @@ -3979,11 +3965,11 @@ public interface Flags extends CommonFlags { } /** Default RVC NOTIFICATION feature flag.. */ - boolean DEFAULT_RVC_NOTIFICATION_ENABLED = false; + boolean DEFAULT_RVC_POST_OTA_NOTIFICATION_ENABLED = false; /** RVC Notification feature flag.. */ - default boolean getEnableRvcNotification() { - return DEFAULT_RVC_NOTIFICATION_ENABLED; + default boolean getEnableRvcPostOtaNotification() { + return DEFAULT_RVC_POST_OTA_NOTIFICATION_ENABLED; } /** Default enableAdServices system API feature flag.. */ @@ -4100,76 +4086,6 @@ public interface Flags extends CommonFlags { return MEASUREMENT_MIN_REPORTING_ORIGIN_UPDATE_WINDOW; } - float MEASUREMENT_INSTALL_ATTR_DUAL_DESTINATION_EVENT_NOISE_PROBABILITY = 0.0000208f; - - /** - * {@link Source} Noise probability for 'Event' when both destinations (app and web) are - * available on the source and supports install attribution. - */ - default float getMeasurementInstallAttrDualDestinationEventNoiseProbability() { - return MEASUREMENT_INSTALL_ATTR_DUAL_DESTINATION_EVENT_NOISE_PROBABILITY; - } - - float MEASUREMENT_DUAL_DESTINATION_NAVIGATION_NOISE_PROBABILITY = 0.0170218f; - - /** - * {@link Source} Noise probability for 'Navigation' when both destinations (app and web) are - * available on the source. - */ - default float getMeasurementDualDestinationNavigationNoiseProbability() { - return MEASUREMENT_DUAL_DESTINATION_NAVIGATION_NOISE_PROBABILITY; - } - - float MEASUREMENT_INSTALL_ATTR_DUAL_DESTINATION_NAVIGATION_NOISE_PROBABILITY = - MEASUREMENT_DUAL_DESTINATION_NAVIGATION_NOISE_PROBABILITY; - - /** - * {@link Source} Noise probability for 'Navigation' when both destinations (app and web) are - * available on the source and supports install attribution. - */ - default float getMeasurementInstallAttrDualDestinationNavigationNoiseProbability() { - return MEASUREMENT_INSTALL_ATTR_DUAL_DESTINATION_NAVIGATION_NOISE_PROBABILITY; - } - - float MEASUREMENT_DUAL_DESTINATION_EVENT_NOISE_PROBABILITY = 0.0000042f; - - /** - * {@link Source} Noise probability for 'Event' when both destinations (app and web) are - * available on the source. - */ - default float getMeasurementDualDestinationEventNoiseProbability() { - return MEASUREMENT_DUAL_DESTINATION_EVENT_NOISE_PROBABILITY; - } - - float MEASUREMENT_INSTALL_ATTR_EVENT_NOISE_PROBABILITY = 0.0000125f; - - /** {@link Source} Noise probability for 'Event' which supports install attribution. */ - default float getMeasurementInstallAttrEventNoiseProbability() { - return MEASUREMENT_INSTALL_ATTR_EVENT_NOISE_PROBABILITY; - } - - float MEASUREMENT_EVENT_NOISE_PROBABILITY = 0.0000025f; - - /** {@link Source} Noise probability for 'Event'. */ - default float getMeasurementEventNoiseProbability() { - return MEASUREMENT_EVENT_NOISE_PROBABILITY; - } - - float MEASUREMENT_NAVIGATION_NOISE_PROBABILITY = 0.0024263f; - - /** {@link Source} Noise probability for 'Navigation'. */ - default float getMeasurementNavigationNoiseProbability() { - return MEASUREMENT_NAVIGATION_NOISE_PROBABILITY; - } - - float MEASUREMENT_INSTALL_ATTR_NAVIGATION_NOISE_PROBABILITY = - MEASUREMENT_NAVIGATION_NOISE_PROBABILITY; - - /** {@link Source} Noise probability for 'Navigation' which supports install attribution. */ - default float getMeasurementInstallAttrNavigationNoiseProbability() { - return MEASUREMENT_INSTALL_ATTR_NAVIGATION_NOISE_PROBABILITY; - } - boolean MEASUREMENT_ENABLE_PREINSTALL_CHECK = false; /** Returns true when pre-install check is enabled. */ @@ -4281,6 +4197,16 @@ public interface Flags extends CommonFlags { } /** + * Flag to control whether redirect registration urls should be modified to prefix the path + * string with .well-known + */ + boolean MEASUREMENT_ENABLE_REDIRECT_TO_WELL_KNOWN_PATH = false; + + default boolean getMeasurementEnableRedirectToWellKnownPath() { + return MEASUREMENT_ENABLE_REDIRECT_TO_WELL_KNOWN_PATH; + } + + /** * Default whether to limit logging for enrollment metrics to avoid performance issues. This * includes not logging data that requires database queries and downloading MDD files. */ @@ -4499,4 +4425,28 @@ public interface Flags extends CommonFlags { default int getBackgroundJobSamplingLoggingRate() { return DEFAULT_BACKGROUND_JOB_SAMPLING_LOGGING_RATE; } + + /** Default value of the timeout for AppSearch write operations */ + int DEFAULT_APPSEARCH_WRITE_TIMEOUT_MS = 3000; + + /** + * Gets the value of the timeout for AppSearch write operations, in milliseconds. + * + * @return the timeout, in milliseconds, for AppSearch write operations + */ + default int getAppSearchWriteTimeout() { + return DEFAULT_APPSEARCH_WRITE_TIMEOUT_MS; + } + + /** Default value of the timeout for AppSearch read operations */ + int DEFAULT_APPSEARCH_READ_TIMEOUT_MS = 500; + + /** + * Gets the value of the timeout for AppSearch read operations, in milliseconds. + * + * @return the timeout, in milliseconds, for AppSearch read operations + */ + default int getAppSearchReadTimeout() { + return DEFAULT_APPSEARCH_READ_TIMEOUT_MS; + } } diff --git a/adservices/service-core/java/com/android/adservices/service/FlagsConstants.java b/adservices/service-core/java/com/android/adservices/service/FlagsConstants.java index 56ca5efe24..cb408cefc4 100644 --- a/adservices/service-core/java/com/android/adservices/service/FlagsConstants.java +++ b/adservices/service-core/java/com/android/adservices/service/FlagsConstants.java @@ -94,6 +94,8 @@ public final class FlagsConstants { public static final String KEY_TOPICS_ENCRYPTION_ENABLED = "topics_encryption_enabled"; public static final String KEY_TOPICS_DISABLE_PLAINTEXT_RESPONSE = "topics_disable_plaintext_response"; + public static final String KEY_TOPICS_TEST_ENCRYPTION_PUBLIC_KEY = + "topics_test_encryption_public_key"; // Topics classifier keys public static final String KEY_CLASSIFIER_TYPE = "classifier_type"; @@ -284,9 +286,6 @@ public final class FlagsConstants { public static final String KEY_MEASUREMENT_ENABLE_COARSE_EVENT_REPORT_DESTINATIONS = "measurement_enable_coarse_event_report_destinations"; - public static final String KEY_MEASUREMENT_ENABLE_VTC_CONFIGURABLE_MAX_EVENT_REPORTS = - "measurement_enable_vtc_configurable_max_event_reports_count"; - public static final String KEY_MEASUREMENT_VTC_CONFIGURABLE_MAX_EVENT_REPORTS_COUNT = "measurement_vtc_configurable_max_event_reports_count"; @@ -811,6 +810,8 @@ public final class FlagsConstants { public static final String KEY_PPAPI_APP_SIGNATURE_ALLOW_LIST = "ppapi_app_signature_allow_list"; + public static final String KEY_APPSEARCH_WRITE_TIMEOUT_MS = "appsearch_write_timeout_ms"; + public static final String KEY_APPSEARCH_READ_TIMEOUT_MS = "appsearch_read_timeout_ms"; public static final String KEY_APPSEARCH_WRITER_ALLOW_LIST_OVERRIDE = "appsearch_writer_allow_list_override"; @@ -1069,9 +1070,6 @@ public final class FlagsConstants { public static final String KEY_MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS = "measurement_min_event_report_delay_millis"; - public static final String KEY_MEASUREMENT_ENABLE_CONFIGURABLE_EVENT_REPORTING_WINDOWS = - "measurement_enable_configurable_event_reporting_windows"; - public static final String KEY_MEASUREMENT_EVENT_REPORTS_VTC_EARLY_REPORTING_WINDOWS = "measurement_event_reports_vtc_early_reporting_windows"; @@ -1134,31 +1132,8 @@ public final class FlagsConstants { public static final String KEY_MEASUREMENT_ENABLE_API_STATUS_ALLOW_LIST_CHECK = "measurement_enable_api_status_allow_list_check"; - public static final String - KEY_MEASUREMENT_INSTALL_ATTR_DUAL_DESTINATION_EVENT_NOISE_PROBABILITY = - "measurement_install_attr_dual_destination_event_noise_probability"; - - public static final String KEY_MEASUREMENT_DUAL_DESTINATION_NAVIGATION_NOISE_PROBABILITY = - "measurement_dual_destination_navigation_noise_probability"; - - public static final String - KEY_MEASUREMENT_INSTALL_ATTR_DUAL_DESTINATION_NAVIGATION_NOISE_PROBABILITY = - "measurement_install_attr_dual_destination_navigation_noise_probability"; - - public static final String KEY_MEASUREMENT_DUAL_DESTINATION_EVENT_NOISE_PROBABILITY = - "measurement_dual_destination_event_noise_probability"; - - public static final String KEY_MEASUREMENT_INSTALL_ATTR_EVENT_NOISE_PROBABILITY = - "measurement_install_attr_event_noise_probability"; - - public static final String KEY_MEASUREMENT_INSTALL_ATTR_NAVIGATION_NOISE_PROBABILITY = - "measurement_install_attr_navigation_noise_probability"; - - public static final String KEY_MEASUREMENT_EVENT_NOISE_PROBABILITY = - "measurement_event_noise_probability"; - - public static final String KEY_MEASUREMENT_NAVIGATION_NOISE_PROBABILITY = - "measurement_navigation_noise_probability"; + public static final String KEY_MEASUREMENT_ENABLE_REDIRECT_TO_WELL_KNOWN_PATH = + "measurement_enable_redirect_to_well_known_path"; // Database Schema Version Flags public static final String KEY_ENABLE_DATABASE_SCHEMA_VERSION_8 = @@ -1166,8 +1141,6 @@ public final class FlagsConstants { public static final String KEY_ENABLE_DATABASE_SCHEMA_VERSION_9 = "enable_database_schema_version_9"; - public static final String KEY_EU_NOTIF_FLOW_CHANGE_ENABLED = "eu_notif_flow_change_enabled"; - public static final String KEY_NOTIFICATION_DISMISSED_ON_CLICK = "notification_dmsmissed_on_click"; @@ -1175,7 +1148,8 @@ public final class FlagsConstants { public static final String KEY_RVC_UX_ENABLED = "rvc_ux_enabled"; - public static final String KEY_RVC_NOTIFICATION_ENABLED = "rvc_notification_enabled"; + public static final String KEY_RVC_POST_OTA_NOTIFICATION_ENABLED = + "rvc_post_ota_notification_enabled"; public static final String KEY_ENABLE_AD_SERVICES_SYSTEM_API = "enable_ad_services_system_api"; diff --git a/adservices/service-core/java/com/android/adservices/service/PhFlags.java b/adservices/service-core/java/com/android/adservices/service/PhFlags.java index 28b5720929..70948e5ddc 100644 --- a/adservices/service-core/java/com/android/adservices/service/PhFlags.java +++ b/adservices/service-core/java/com/android/adservices/service/PhFlags.java @@ -16,6 +16,8 @@ package com.android.adservices.service; +import static com.android.adservices.service.FlagsConstants.KEY_APPSEARCH_READ_TIMEOUT_MS; +import static com.android.adservices.service.FlagsConstants.KEY_APPSEARCH_WRITE_TIMEOUT_MS; import static com.android.adservices.service.FlagsConstants.KEY_ENCRYPTION_KEY_JOB_PERIOD_MS; import static com.android.adservices.service.FlagsConstants.KEY_ENCRYPTION_KEY_JOB_REQUIRED_NETWORK_TYPE; import static com.android.adservices.service.FlagsConstants.KEY_FLEDGE_MEASUREMENT_REPORT_AND_REGISTER_EVENT_API_ENABLED; @@ -245,6 +247,15 @@ public final class PhFlags extends CommonPhFlags implements Flags { } @Override + public String getTopicsTestEncryptionPublicKey() { + // The priority of applying the flag values: PH (DeviceConfig) and then hard-coded value. + return DeviceConfig.getString( + FlagsConstants.NAMESPACE_ADSERVICES, + /* flagName */ FlagsConstants.KEY_TOPICS_TEST_ENCRYPTION_PUBLIC_KEY, + /* defaultValue */ TOPICS_TEST_ENCRYPTION_PUBLIC_KEY); + } + + @Override public int getClassifierType() { // The priority of applying the flag values: SystemProperties, PH (DeviceConfig), then // hard-coded value. @@ -3472,16 +3483,6 @@ public final class PhFlags extends CommonPhFlags implements Flags { } @Override - public boolean getMeasurementEnableConfigurableEventReportingWindows() { - // The priority of applying the flag values: PH (DeviceConfig) and then hard-coded value. - return DeviceConfig.getBoolean( - FlagsConstants.NAMESPACE_ADSERVICES, - /* flagName */ FlagsConstants - .KEY_MEASUREMENT_ENABLE_CONFIGURABLE_EVENT_REPORTING_WINDOWS, - /* defaultValue */ MEASUREMENT_ENABLE_CONFIGURABLE_EVENT_REPORTING_WINDOWS); - } - - @Override public String getMeasurementEventReportsVtcEarlyReportingWindows() { // The priority of applying the flag values: PH (DeviceConfig) and then hard-coded value. return DeviceConfig.getString( @@ -3567,6 +3568,14 @@ public final class PhFlags extends CommonPhFlags implements Flags { } @Override + public boolean getMeasurementEnableRedirectToWellKnownPath() { + return DeviceConfig.getBoolean( + FlagsConstants.NAMESPACE_ADSERVICES, + /* flagName */ FlagsConstants.KEY_MEASUREMENT_ENABLE_REDIRECT_TO_WELL_KNOWN_PATH, + /* defaultValue */ MEASUREMENT_ENABLE_REDIRECT_TO_WELL_KNOWN_PATH); + } + + @Override public boolean getFledgeMeasurementReportAndRegisterEventApiEnabled() { return DeviceConfig.getBoolean( FlagsConstants.NAMESPACE_ADSERVICES, @@ -4281,49 +4290,6 @@ public final class PhFlags extends CommonPhFlags implements Flags { + getMeasurementMinReportingOriginUpdateWindow()); writer.println( "\t" - + FlagsConstants - .KEY_MEASUREMENT_DUAL_DESTINATION_NAVIGATION_NOISE_PROBABILITY - + " = " - + getMeasurementDualDestinationNavigationNoiseProbability()); - writer.println( - "\t" - + FlagsConstants - .KEY_MEASUREMENT_INSTALL_ATTR_DUAL_DESTINATION_EVENT_NOISE_PROBABILITY - + " = " - + getMeasurementInstallAttrDualDestinationEventNoiseProbability()); - writer.println( - "\t" - + FlagsConstants - .KEY_MEASUREMENT_INSTALL_ATTR_DUAL_DESTINATION_NAVIGATION_NOISE_PROBABILITY - + " = " - + getMeasurementInstallAttrDualDestinationNavigationNoiseProbability()); - writer.println( - "\t" - + FlagsConstants.KEY_MEASUREMENT_DUAL_DESTINATION_EVENT_NOISE_PROBABILITY - + " = " - + getMeasurementDualDestinationEventNoiseProbability()); - writer.println( - "\t" - + FlagsConstants.KEY_MEASUREMENT_INSTALL_ATTR_EVENT_NOISE_PROBABILITY - + " = " - + getMeasurementInstallAttrEventNoiseProbability()); - writer.println( - "\t" - + FlagsConstants.KEY_MEASUREMENT_INSTALL_ATTR_NAVIGATION_NOISE_PROBABILITY - + " = " - + getMeasurementInstallAttrNavigationNoiseProbability()); - writer.println( - "\t" - + FlagsConstants.KEY_MEASUREMENT_EVENT_NOISE_PROBABILITY - + " = " - + getMeasurementEventNoiseProbability()); - writer.println( - "\t" - + FlagsConstants.KEY_MEASUREMENT_NAVIGATION_NOISE_PROBABILITY - + " = " - + getMeasurementNavigationNoiseProbability()); - writer.println( - "\t" + FlagsConstants.KEY_MEASUREMENT_REGISTRATION_JOB_QUEUE_KILL_SWITCH + " = " + getAsyncRegistrationJobQueueKillSwitch()); @@ -4369,11 +4335,6 @@ public final class PhFlags extends CommonPhFlags implements Flags { + getMeasurementMinEventReportDelayMillis()); writer.println( "\t" - + FlagsConstants.KEY_MEASUREMENT_ENABLE_CONFIGURABLE_EVENT_REPORTING_WINDOWS - + " = " - + getMeasurementEnableConfigurableEventReportingWindows()); - writer.println( - "\t" + FlagsConstants.KEY_MEASUREMENT_EVENT_REPORTS_VTC_EARLY_REPORTING_WINDOWS + " = " + getMeasurementEventReportsVtcEarlyReportingWindows()); @@ -5154,11 +5115,6 @@ public final class PhFlags extends CommonPhFlags implements Flags { writer.println("==== AdServices PH Flags Dump UI Related Flags ===="); writer.println( "\t" - + FlagsConstants.KEY_EU_NOTIF_FLOW_CHANGE_ENABLED - + " = " - + getEuNotifFlowChangeEnabled()); - writer.println( - "\t" + FlagsConstants.KEY_UI_FEATURE_TYPE_LOGGING_ENABLED + " = " + isUiFeatureTypeLoggingEnabled()); @@ -5500,6 +5456,8 @@ public final class PhFlags extends CommonPhFlags implements Flags { + KEY_MEASUREMENT_ENABLE_SESSION_STABLE_KILL_SWITCHES + " = " + getMeasurementEnableSessionStableKillSwitches()); + writer.println("\t" + KEY_APPSEARCH_WRITE_TIMEOUT_MS + " = " + getAppSearchWriteTimeout()); + writer.println("\t" + KEY_APPSEARCH_READ_TIMEOUT_MS + " = " + getAppSearchReadTimeout()); } @VisibleForTesting @@ -5667,14 +5625,6 @@ public final class PhFlags extends CommonPhFlags implements Flags { } @Override - public boolean getEuNotifFlowChangeEnabled() { - return DeviceConfig.getBoolean( - FlagsConstants.NAMESPACE_ADSERVICES, - /* flagName */ FlagsConstants.KEY_EU_NOTIF_FLOW_CHANGE_ENABLED, - /* defaultValue */ DEFAULT_EU_NOTIF_FLOW_CHANGE_ENABLED); - } - - @Override public boolean getNotificationDismissedOnClick() { return DeviceConfig.getBoolean( FlagsConstants.NAMESPACE_ADSERVICES, @@ -5701,12 +5651,12 @@ public final class PhFlags extends CommonPhFlags implements Flags { } @Override - public boolean getEnableRvcNotification() { + public boolean getEnableRvcPostOtaNotification() { return getEnableAdServicesSystemApi() && DeviceConfig.getBoolean( FlagsConstants.NAMESPACE_ADSERVICES, - /* flagName */ FlagsConstants.KEY_RVC_NOTIFICATION_ENABLED, - /* defaultValue */ DEFAULT_RVC_NOTIFICATION_ENABLED); + /* flagName */ FlagsConstants.KEY_RVC_POST_OTA_NOTIFICATION_ENABLED, + /* defaultValue */ DEFAULT_RVC_POST_OTA_NOTIFICATION_ENABLED); } @Override @@ -5729,7 +5679,9 @@ public final class PhFlags extends CommonPhFlags implements Flags { getRecordManualInteractionEnabled()); uxMap.put(FlagsConstants.KEY_GA_UX_FEATURE_ENABLED, getGaUxFeatureEnabled()); uxMap.put(FlagsConstants.KEY_RVC_UX_ENABLED, getEnableRvcUx()); - uxMap.put(FlagsConstants.KEY_RVC_NOTIFICATION_ENABLED, getEnableRvcNotification()); + uxMap.put( + FlagsConstants.KEY_RVC_POST_OTA_NOTIFICATION_ENABLED, + getEnableRvcPostOtaNotification()); uxMap.put( FlagsConstants.KEY_UI_OTA_STRINGS_FEATURE_ENABLED, getUiOtaStringsFeatureEnabled()); uxMap.put( @@ -5742,7 +5694,6 @@ public final class PhFlags extends CommonPhFlags implements Flags { uxMap.put( FlagsConstants.KEY_CONSENT_NOTIFICATION_ACTIVITY_DEBUG_MODE, getConsentNotificationActivityDebugMode()); - uxMap.put(FlagsConstants.KEY_EU_NOTIF_FLOW_CHANGE_ENABLED, getEuNotifFlowChangeEnabled()); uxMap.put(FlagsConstants.KEY_U18_UX_ENABLED, getU18UxEnabled()); uxMap.put( FlagsConstants.KEY_NOTIFICATION_DISMISSED_ON_CLICK, @@ -5863,83 +5814,6 @@ public final class PhFlags extends CommonPhFlags implements Flags { } @Override - public float getMeasurementInstallAttrDualDestinationEventNoiseProbability() { - // The priority of applying the flag values: PH (DeviceConfig) and then hard-coded value. - return DeviceConfig.getFloat( - FlagsConstants.NAMESPACE_ADSERVICES, - /* flagName */ FlagsConstants - .KEY_MEASUREMENT_INSTALL_ATTR_DUAL_DESTINATION_EVENT_NOISE_PROBABILITY, - /* defaultValue */ MEASUREMENT_INSTALL_ATTR_DUAL_DESTINATION_EVENT_NOISE_PROBABILITY); - } - - @Override - public float getMeasurementDualDestinationNavigationNoiseProbability() { - // The priority of applying the flag values: PH (DeviceConfig) and then hard-coded value. - return DeviceConfig.getFloat( - FlagsConstants.NAMESPACE_ADSERVICES, - /* flagName */ FlagsConstants - .KEY_MEASUREMENT_DUAL_DESTINATION_NAVIGATION_NOISE_PROBABILITY, - /* defaultValue */ MEASUREMENT_DUAL_DESTINATION_NAVIGATION_NOISE_PROBABILITY); - } - - @Override - public float getMeasurementInstallAttrDualDestinationNavigationNoiseProbability() { - // The priority of applying the flag values: PH (DeviceConfig) and then hard-coded value. - return DeviceConfig.getFloat( - FlagsConstants.NAMESPACE_ADSERVICES, - /* flagName */ FlagsConstants - .KEY_MEASUREMENT_INSTALL_ATTR_DUAL_DESTINATION_NAVIGATION_NOISE_PROBABILITY, - /* defaultValue */ MEASUREMENT_INSTALL_ATTR_DUAL_DESTINATION_NAVIGATION_NOISE_PROBABILITY); - } - - @Override - public float getMeasurementDualDestinationEventNoiseProbability() { - // The priority of applying the flag values: PH (DeviceConfig) and then hard-coded value. - return DeviceConfig.getFloat( - FlagsConstants.NAMESPACE_ADSERVICES, - /* flagName */ FlagsConstants - .KEY_MEASUREMENT_DUAL_DESTINATION_EVENT_NOISE_PROBABILITY, - /* defaultValue */ MEASUREMENT_DUAL_DESTINATION_EVENT_NOISE_PROBABILITY); - } - - @Override - public float getMeasurementInstallAttrEventNoiseProbability() { - // The priority of applying the flag values: PH (DeviceConfig) and then hard-coded value. - return DeviceConfig.getFloat( - FlagsConstants.NAMESPACE_ADSERVICES, - /* flagName */ FlagsConstants.KEY_MEASUREMENT_INSTALL_ATTR_EVENT_NOISE_PROBABILITY, - /* defaultValue */ MEASUREMENT_INSTALL_ATTR_EVENT_NOISE_PROBABILITY); - } - - @Override - public float getMeasurementInstallAttrNavigationNoiseProbability() { - // The priority of applying the flag values: PH (DeviceConfig) and then hard-coded value. - return DeviceConfig.getFloat( - FlagsConstants.NAMESPACE_ADSERVICES, - /* flagName */ FlagsConstants - .KEY_MEASUREMENT_INSTALL_ATTR_NAVIGATION_NOISE_PROBABILITY, - /* defaultValue */ MEASUREMENT_INSTALL_ATTR_NAVIGATION_NOISE_PROBABILITY); - } - - @Override - public float getMeasurementEventNoiseProbability() { - // The priority of applying the flag values: PH (DeviceConfig) and then hard-coded value. - return DeviceConfig.getFloat( - FlagsConstants.NAMESPACE_ADSERVICES, - /* flagName */ FlagsConstants.KEY_MEASUREMENT_EVENT_NOISE_PROBABILITY, - /* defaultValue */ MEASUREMENT_EVENT_NOISE_PROBABILITY); - } - - @Override - public float getMeasurementNavigationNoiseProbability() { - // The priority of applying the flag values: PH (DeviceConfig) and then hard-coded value. - return DeviceConfig.getFloat( - FlagsConstants.NAMESPACE_ADSERVICES, - /* flagName */ FlagsConstants.KEY_MEASUREMENT_NAVIGATION_NOISE_PROBABILITY, - /* defaultValue */ MEASUREMENT_NAVIGATION_NOISE_PROBABILITY); - } - - @Override public boolean getMeasurementEnablePreinstallCheck() { // The priority of applying the flag values: PH (DeviceConfig) and then hard-coded value. return DeviceConfig.getBoolean( @@ -5949,16 +5823,6 @@ public final class PhFlags extends CommonPhFlags implements Flags { } @Override - public boolean getMeasurementEnableVtcConfigurableMaxEventReports() { - return DeviceConfig.getBoolean( - FlagsConstants.NAMESPACE_ADSERVICES, - /* flagName */ FlagsConstants - .KEY_MEASUREMENT_ENABLE_VTC_CONFIGURABLE_MAX_EVENT_REPORTS, - /* defaultValue */ - DEFAULT_MEASUREMENT_ENABLE_VTC_CONFIGURABLE_MAX_EVENT_REPORTS); - } - - @Override public int getMeasurementVtcConfigurableMaxEventReportsCount() { return DeviceConfig.getInt( FlagsConstants.NAMESPACE_ADSERVICES, @@ -6526,4 +6390,20 @@ public final class PhFlags extends CommonPhFlags implements Flags { return loggingRatio; } + + @Override + public int getAppSearchWriteTimeout() { + return DeviceConfig.getInt( + FlagsConstants.NAMESPACE_ADSERVICES, + /* name= */ FlagsConstants.KEY_APPSEARCH_WRITE_TIMEOUT_MS, + /* defaultValue= */ DEFAULT_APPSEARCH_WRITE_TIMEOUT_MS); + } + + @Override + public int getAppSearchReadTimeout() { + return DeviceConfig.getInt( + FlagsConstants.NAMESPACE_ADSERVICES, + /* name= */ FlagsConstants.KEY_APPSEARCH_READ_TIMEOUT_MS, + /* defaultValue= */ DEFAULT_APPSEARCH_READ_TIMEOUT_MS); + } } diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionRunner.java b/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionRunner.java index c337bf17ba..22eea4b056 100644 --- a/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionRunner.java +++ b/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionRunner.java @@ -49,7 +49,10 @@ import com.android.adservices.data.adselection.datahandlers.AdSelectionResultBid import com.android.adservices.data.adselection.datahandlers.WinningCustomAudience; import com.android.adservices.data.customaudience.CustomAudienceDao; import com.android.adservices.data.customaudience.DBCustomAudience; +import com.android.adservices.data.encryptionkey.EncryptionKeyDao; +import com.android.adservices.data.enrollment.EnrollmentDao; import com.android.adservices.service.Flags; +import com.android.adservices.service.adselection.signature.ProtectedAudienceSignatureManager; import com.android.adservices.service.common.AdSelectionServiceFilter; import com.android.adservices.service.common.FrequencyCapAdDataValidator; import com.android.adservices.service.common.Throttler; @@ -76,6 +79,7 @@ import com.google.common.util.concurrent.UncheckedTimeoutException; import java.time.Clock; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -123,8 +127,11 @@ public abstract class AdSelectionRunner { static final String ON_DEVICE_AUCTION_KILL_SWITCH_ENABLED = "On device auction kill switch enabled"; + @NonNull protected final Context mContext; @NonNull protected final CustomAudienceDao mCustomAudienceDao; @NonNull protected final AdSelectionEntryDao mAdSelectionEntryDao; + @NonNull protected final EncryptionKeyDao mEncryptionKeyDao; + @NonNull protected final EnrollmentDao mEnrollmentDao; @NonNull protected final ListeningExecutorService mLightweightExecutorService; @NonNull protected final ListeningExecutorService mBackgroundExecutorService; @NonNull protected final ScheduledThreadPoolExecutor mScheduledExecutor; @@ -157,6 +164,8 @@ public abstract class AdSelectionRunner { @NonNull final Context context, @NonNull final CustomAudienceDao customAudienceDao, @NonNull final AdSelectionEntryDao adSelectionEntryDao, + @NonNull final EncryptionKeyDao encryptionKeyDao, + @NonNull final EnrollmentDao enrollmentDao, @NonNull final ExecutorService lightweightExecutorService, @NonNull final ExecutorService backgroundExecutorService, @NonNull final ScheduledThreadPoolExecutor scheduledExecutor, @@ -173,6 +182,8 @@ public abstract class AdSelectionRunner { Objects.requireNonNull(context); Objects.requireNonNull(customAudienceDao); Objects.requireNonNull(adSelectionEntryDao); + Objects.requireNonNull(encryptionKeyDao); + Objects.requireNonNull(enrollmentDao); Objects.requireNonNull(lightweightExecutorService); Objects.requireNonNull(backgroundExecutorService); Objects.requireNonNull(adServicesLogger); @@ -184,8 +195,11 @@ public abstract class AdSelectionRunner { Objects.requireNonNull(debugReporting); Objects.requireNonNull(adSelectionExecutionLogger); + mContext = context; mCustomAudienceDao = customAudienceDao; mAdSelectionEntryDao = adSelectionEntryDao; + mEncryptionKeyDao = encryptionKeyDao; + mEnrollmentDao = enrollmentDao; mLightweightExecutorService = MoreExecutors.listeningDecorator(lightweightExecutorService); mBackgroundExecutorService = MoreExecutors.listeningDecorator(backgroundExecutorService); mScheduledExecutor = scheduledExecutor; @@ -209,6 +223,8 @@ public abstract class AdSelectionRunner { @NonNull final Context context, @NonNull final CustomAudienceDao customAudienceDao, @NonNull final AdSelectionEntryDao adSelectionEntryDao, + @NonNull final EncryptionKeyDao encryptionKeyDao, + @NonNull final EnrollmentDao enrollmentDao, @NonNull final ExecutorService lightweightExecutorService, @NonNull final ExecutorService backgroundExecutorService, @NonNull final ScheduledThreadPoolExecutor scheduledExecutor, @@ -227,6 +243,8 @@ public abstract class AdSelectionRunner { Objects.requireNonNull(context); Objects.requireNonNull(customAudienceDao); Objects.requireNonNull(adSelectionEntryDao); + Objects.requireNonNull(encryptionKeyDao); + Objects.requireNonNull(enrollmentDao); Objects.requireNonNull(lightweightExecutorService); Objects.requireNonNull(backgroundExecutorService); Objects.requireNonNull(scheduledExecutor); @@ -240,8 +258,11 @@ public abstract class AdSelectionRunner { Objects.requireNonNull(adCounterHistogramUpdater); Objects.requireNonNull(debugReporting); + mContext = context; mCustomAudienceDao = customAudienceDao; mAdSelectionEntryDao = adSelectionEntryDao; + mEncryptionKeyDao = encryptionKeyDao; + mEnrollmentDao = enrollmentDao; mLightweightExecutorService = MoreExecutors.listeningDecorator(lightweightExecutorService); mBackgroundExecutorService = MoreExecutors.listeningDecorator(backgroundExecutorService); mScheduledExecutor = scheduledExecutor; @@ -277,7 +298,6 @@ public abstract class AdSelectionRunner { final int traceCookie = Tracing.beginAsyncSection(Tracing.RUN_AD_SELECTION); Objects.requireNonNull(inputParams); Objects.requireNonNull(callback); - AdSelectionConfig adSelectionConfig = inputParams.getAdSelectionConfig(); try { ListenableFuture<Void> filterAndValidateRequestFuture = @@ -501,7 +521,7 @@ public abstract class AdSelectionRunner { @NonNull final String callerPackageName) { sLogger.v("Beginning Ad Selection Orchestration"); - AdSelectionConfig adSelectionConfigInput = adSelectionConfig; + AdSelectionConfig adSelectionConfigInput; if (!mFlags.getFledgeAdSelectionContextualAdsEnabled()) { // Empty all contextual ads if the feature is disabled sLogger.v("Contextual flow is disabled"); @@ -594,9 +614,7 @@ public abstract class AdSelectionRunner { private int countBuyersRequested(@NonNull AdSelectionConfig adSelectionConfig) { Objects.requireNonNull(adSelectionConfig); - return adSelectionConfig.getCustomAudienceBuyers().stream() - .collect(Collectors.toSet()) - .size(); + return new HashSet<>(adSelectionConfig.getCustomAudienceBuyers()).size(); } private int countBuyersFromCustomAudiences( @@ -728,10 +746,27 @@ public abstract class AdSelectionRunner { AdSelectionConfig adSelectionConfig) { Map<AdTechIdentifier, SignedContextualAds> filteredContextualAdsMap = new HashMap<>(); sLogger.v("Filtering contextual ads in Ad Selection Config"); + boolean isEnrollmentCheckEnabled = !mFlags.getDisableFledgeEnrollmentCheck(); + ProtectedAudienceSignatureManager signatureManager = + new ProtectedAudienceSignatureManager( + mEnrollmentDao, mEncryptionKeyDao, isEnrollmentCheckEnabled); for (Map.Entry<AdTechIdentifier, SignedContextualAds> entry : adSelectionConfig.getBuyerSignedContextualAds().entrySet()) { + if (!signatureManager.isVerified(entry.getKey(), entry.getValue())) { + sLogger.v( + "Contextual ads for buyer: '%s' have an invalid signature and will be" + + " removed from the auction", + entry.getKey()); + continue; + } filteredContextualAdsMap.put( entry.getKey(), mAdFilterer.filterContextualAds(entry.getValue())); + sLogger.v( + "Buyer '%s' has a valid signature. It's contextual ads filtered from " + + "%s ad(s) to %s ad(s)", + entry.getKey(), + entry.getValue().getAdsWithBid().size(), + filteredContextualAdsMap.get(entry.getKey()).getAdsWithBid().size()); } return adSelectionConfig .cloneToBuilder() diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionServiceImpl.java b/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionServiceImpl.java index 85bad0af02..9bff5dea67 100644 --- a/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionServiceImpl.java +++ b/adservices/service-core/java/com/android/adservices/service/adselection/AdSelectionServiceImpl.java @@ -71,12 +71,12 @@ import com.android.adservices.data.adselection.AdSelectionDebugReportingDatabase import com.android.adservices.data.adselection.AdSelectionEntryDao; import com.android.adservices.data.adselection.AdSelectionServerDatabase; import com.android.adservices.data.adselection.AppInstallDao; -import com.android.adservices.data.adselection.EncryptionContextDao; -import com.android.adservices.data.adselection.EncryptionKeyDao; import com.android.adservices.data.adselection.FrequencyCapDao; import com.android.adservices.data.adselection.SharedStorageDatabase; import com.android.adservices.data.customaudience.CustomAudienceDao; import com.android.adservices.data.customaudience.CustomAudienceDatabase; +import com.android.adservices.data.encryptionkey.EncryptionKeyDao; +import com.android.adservices.data.enrollment.EnrollmentDao; import com.android.adservices.data.signals.EncodedPayloadDao; import com.android.adservices.data.signals.ProtectedSignalsDatabase; import com.android.adservices.service.Flags; @@ -137,8 +137,8 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { @NonNull private final CustomAudienceDao mCustomAudienceDao; @NonNull private final EncodedPayloadDao mEncodedPayloadDao; @NonNull private final FrequencyCapDao mFrequencyCapDao; - @NonNull private final EncryptionContextDao mEncryptionContextDao; @NonNull private final EncryptionKeyDao mEncryptionKeyDao; + @NonNull private final EnrollmentDao mEnrollmentDao; @NonNull private final AdServicesHttpsClient mAdServicesHttpsClient; @NonNull private final ExecutorService mLightweightExecutor; @NonNull private final ExecutorService mBackgroundExecutor; @@ -168,8 +168,8 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { @NonNull CustomAudienceDao customAudienceDao, @NonNull EncodedPayloadDao encodedPayloadDao, @NonNull FrequencyCapDao frequencyCapDao, - @NonNull EncryptionContextDao encryptionContextDao, @NonNull EncryptionKeyDao encryptionKeyDao, + @NonNull EnrollmentDao enrollmentDao, @NonNull AdServicesHttpsClient adServicesHttpsClient, @NonNull DevContextFilter devContextFilter, @NonNull ExecutorService lightweightExecutorService, @@ -193,8 +193,8 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { Objects.requireNonNull(customAudienceDao); Objects.requireNonNull(encodedPayloadDao); Objects.requireNonNull(frequencyCapDao); - Objects.requireNonNull(encryptionContextDao); Objects.requireNonNull(encryptionKeyDao); + Objects.requireNonNull(enrollmentDao); Objects.requireNonNull(adServicesHttpsClient); Objects.requireNonNull(devContextFilter); Objects.requireNonNull(lightweightExecutorService); @@ -213,8 +213,8 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { mCustomAudienceDao = customAudienceDao; mEncodedPayloadDao = encodedPayloadDao; mFrequencyCapDao = frequencyCapDao; - mEncryptionContextDao = encryptionContextDao; mEncryptionKeyDao = encryptionKeyDao; + mEnrollmentDao = enrollmentDao; mAdServicesHttpsClient = adServicesHttpsClient; mDevContextFilter = devContextFilter; mLightweightExecutor = lightweightExecutorService; @@ -249,8 +249,8 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { CustomAudienceDatabase.getInstance(context).customAudienceDao(), ProtectedSignalsDatabase.getInstance(context).getEncodedPayloadDao(), SharedStorageDatabase.getInstance(context).frequencyCapDao(), - AdSelectionServerDatabase.getInstance(context).encryptionContextDao(), - AdSelectionServerDatabase.getInstance(context).encryptionKeyDao(), + EncryptionKeyDao.getInstance(context), + EnrollmentDao.getInstance(context), new AdServicesHttpsClient( AdServicesExecutors.getBlockingExecutor(), CacheProviderFactory.create(context, FlagsFactory.getFlags())), @@ -332,7 +332,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked in the binder thread, before anything else - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, inputParams.getCallerPackageName(), apiName); int callingUid = getCallingUid(apiName); @@ -370,7 +370,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked in the binder thread, before anything else - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, inputParams.getCallerPackageName(), apiName); int callingUid = getCallingUid(apiName); @@ -473,7 +473,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked in the binder thread, before anything else - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, inputParams.getCallerPackageName(), apiName); int callingUid = getCallingUid(apiName); @@ -674,6 +674,8 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { mContext, mCustomAudienceDao, mAdSelectionEntryDao, + mEncryptionKeyDao, + mEnrollmentDao, mAdServicesHttpsClient, mLightweightExecutor, mBackgroundExecutor, @@ -709,6 +711,8 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { mContext, mCustomAudienceDao, mAdSelectionEntryDao, + mEncryptionKeyDao, + mEnrollmentDao, mAdServicesHttpsClient, mLightweightExecutor, mBackgroundExecutor, @@ -758,7 +762,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked in the binder thread, before anything else - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, inputParams.getCallerPackageName(), apiName); int callingUid = getCallingUid(apiName); @@ -801,7 +805,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked in the binder thread, before anything else - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, requestParams.getCallerPackageName(), apiName); DevContext devContext = mDevContextFilter.createDevContext(); @@ -867,7 +871,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked in the binder thread, before anything else - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, inputParams.getCallerPackageName(), apiName); int callerUid = getCallingUid(apiName); @@ -924,7 +928,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked in the binder thread, before anything else - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, request.getCallerPackageName(), apiName); AppInstallAdvertisersSetter setter = @@ -956,7 +960,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked in the binder thread, before anything else - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, inputParams.getCallerPackageName(), apiName); final int callingUid = getCallingUid(apiName); @@ -1029,7 +1033,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked with a non-null callingAppPackageName - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, devContext.getCallingAppPackageName(), apiName); int callingUid = getCallingUid(apiName); @@ -1096,7 +1100,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked with a non-null callingAppPackageName - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, devContext.getCallingAppPackageName(), apiName); int callingUid = getCallingUid(apiName); @@ -1146,7 +1150,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked with a non-null callingAppPackageName - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, devContext.getCallingAppPackageName(), apiName); int callingUid = getCallingUid(apiName); @@ -1201,7 +1205,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked with a non-null callingAppPackageName - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, devContext.getCallingAppPackageName(), apiName); int callingUid = getCallingUid(apiName); @@ -1252,7 +1256,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked with a non-null callingAppPackageName - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, devContext.getCallingAppPackageName(), apiName); int callingUid = getCallingUid(apiName); @@ -1301,7 +1305,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked with a non-null callingAppPackageName - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, devContext.getCallingAppPackageName(), apiName); int callingUid = getCallingUid(apiName); @@ -1351,7 +1355,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked with a non-null callingAppPackageName - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, devContext.getCallingAppPackageName(), apiName); // TODO(b/265204820): Implement service @@ -1389,7 +1393,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked with a non-null callingAppPackageName - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, devContext.getCallingAppPackageName(), apiName); // TODO(b/265204820): Implement service @@ -1424,7 +1428,7 @@ public class AdSelectionServiceImpl extends AdSelectionService.Stub { } // Caller permissions must be checked with a non-null callingAppPackageName - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, devContext.getCallingAppPackageName(), apiName); // TODO(b/265204820): Implement service diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/OnDeviceAdSelectionRunner.java b/adservices/service-core/java/com/android/adservices/service/adselection/OnDeviceAdSelectionRunner.java index 4c5b280ddb..e2a93966fe 100644 --- a/adservices/service-core/java/com/android/adservices/service/adselection/OnDeviceAdSelectionRunner.java +++ b/adservices/service-core/java/com/android/adservices/service/adselection/OnDeviceAdSelectionRunner.java @@ -35,6 +35,8 @@ import com.android.adservices.data.adselection.AdSelectionEntryDao; import com.android.adservices.data.adselection.DBAdSelection; import com.android.adservices.data.customaudience.CustomAudienceDao; import com.android.adservices.data.customaudience.DBCustomAudience; +import com.android.adservices.data.encryptionkey.EncryptionKeyDao; +import com.android.adservices.data.enrollment.EnrollmentDao; import com.android.adservices.service.Flags; import com.android.adservices.service.common.AdSelectionServiceFilter; import com.android.adservices.service.common.BinderFlagReader; @@ -79,6 +81,8 @@ public class OnDeviceAdSelectionRunner extends AdSelectionRunner { @NonNull final Context context, @NonNull final CustomAudienceDao customAudienceDao, @NonNull final AdSelectionEntryDao adSelectionEntryDao, + @NonNull final EncryptionKeyDao encryptionKeyDao, + @NonNull final EnrollmentDao enrollmentDao, @NonNull final AdServicesHttpsClient adServicesHttpsClient, @NonNull final ExecutorService lightweightExecutorService, @NonNull final ExecutorService backgroundExecutorService, @@ -99,6 +103,8 @@ public class OnDeviceAdSelectionRunner extends AdSelectionRunner { context, customAudienceDao, adSelectionEntryDao, + encryptionKeyDao, + enrollmentDao, lightweightExecutorService, backgroundExecutorService, scheduledExecutor, @@ -172,6 +178,8 @@ public class OnDeviceAdSelectionRunner extends AdSelectionRunner { @NonNull final Context context, @NonNull final CustomAudienceDao customAudienceDao, @NonNull final AdSelectionEntryDao adSelectionEntryDao, + @NonNull final EncryptionKeyDao encryptionKeyDao, + @NonNull final EnrollmentDao enrollmentDao, @NonNull final AdServicesHttpsClient adServicesHttpsClient, @NonNull final ExecutorService lightweightExecutorService, @NonNull final ExecutorService backgroundExecutorService, @@ -195,6 +203,8 @@ public class OnDeviceAdSelectionRunner extends AdSelectionRunner { context, customAudienceDao, adSelectionEntryDao, + encryptionKeyDao, + enrollmentDao, lightweightExecutorService, backgroundExecutorService, scheduledExecutor, @@ -360,7 +370,10 @@ public class OnDeviceAdSelectionRunner extends AdSelectionRunner { .filter(Objects::nonNull) .collect(Collectors.toList())); } - + sLogger.v( + "Invoking score generator with %s bids and %s contextual ads.", + validBiddingOutcomes.size(), + adSelectionConfig.getBuyerSignedContextualAds().size()); return mAdsScoreGenerator .runAdScoring(validBiddingOutcomes, adSelectionConfig) .transform( diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/TrustedServerAdSelectionRunner.java b/adservices/service-core/java/com/android/adservices/service/adselection/TrustedServerAdSelectionRunner.java index 190ebef740..b372378b9c 100644 --- a/adservices/service-core/java/com/android/adservices/service/adselection/TrustedServerAdSelectionRunner.java +++ b/adservices/service-core/java/com/android/adservices/service/adselection/TrustedServerAdSelectionRunner.java @@ -34,6 +34,8 @@ import com.android.adservices.data.adselection.CustomAudienceSignals; import com.android.adservices.data.adselection.DBAdSelection; import com.android.adservices.data.customaudience.CustomAudienceDao; import com.android.adservices.data.customaudience.DBCustomAudience; +import com.android.adservices.data.encryptionkey.EncryptionKeyDao; +import com.android.adservices.data.enrollment.EnrollmentDao; import com.android.adservices.service.Flags; import com.android.adservices.service.common.AdRenderIdValidator; import com.android.adservices.service.common.AdSelectionServiceFilter; @@ -97,6 +99,8 @@ public class TrustedServerAdSelectionRunner extends AdSelectionRunner { @NonNull final Context context, @NonNull final CustomAudienceDao customAudienceDao, @NonNull final AdSelectionEntryDao adSelectionEntryDao, + @NonNull final EncryptionKeyDao encryptionKeyDao, + @NonNull final EnrollmentDao enrollmentDao, @NonNull final AdServicesHttpsClient adServicesHttpsClient, @NonNull final ExecutorService lightweightExecutorService, @NonNull final ExecutorService backgroundExecutorService, @@ -116,6 +120,8 @@ public class TrustedServerAdSelectionRunner extends AdSelectionRunner { context, customAudienceDao, adSelectionEntryDao, + encryptionKeyDao, + enrollmentDao, lightweightExecutorService, backgroundExecutorService, scheduledExecutor, @@ -146,6 +152,8 @@ public class TrustedServerAdSelectionRunner extends AdSelectionRunner { @NonNull final Context context, @NonNull final CustomAudienceDao customAudienceDao, @NonNull final AdSelectionEntryDao adSelectionEntryDao, + @NonNull final EncryptionKeyDao encryptionKeyDao, + @NonNull final EnrollmentDao enrollmentDao, @NonNull final ExecutorService lightweightExecutorService, @NonNull final ExecutorService backgroundExecutorService, @NonNull final ScheduledThreadPoolExecutor scheduledExecutor, @@ -165,6 +173,8 @@ public class TrustedServerAdSelectionRunner extends AdSelectionRunner { context, customAudienceDao, adSelectionEntryDao, + encryptionKeyDao, + enrollmentDao, lightweightExecutorService, backgroundExecutorService, scheduledExecutor, diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/signature/ProtectedAudienceSignatureManager.java b/adservices/service-core/java/com/android/adservices/service/adselection/signature/ProtectedAudienceSignatureManager.java index 63a3d405a9..ec5511f613 100644 --- a/adservices/service-core/java/com/android/adservices/service/adselection/signature/ProtectedAudienceSignatureManager.java +++ b/adservices/service-core/java/com/android/adservices/service/adselection/signature/ProtectedAudienceSignatureManager.java @@ -16,9 +16,9 @@ package com.android.adservices.service.adselection.signature; +import android.adservices.adselection.SignedContextualAds; import android.adservices.common.AdTechIdentifier; import android.annotation.NonNull; -import android.content.Context; import com.android.adservices.LoggerFactory; import com.android.adservices.data.encryptionkey.EncryptionKeyDao; @@ -28,6 +28,7 @@ import com.android.adservices.service.enrollment.EnrollmentData; import com.google.common.annotations.VisibleForTesting; +import java.util.Base64; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -41,32 +42,96 @@ import java.util.stream.Collectors; */ public class ProtectedAudienceSignatureManager { private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger(); - private final Context mContext; - private final EnrollmentDao mEnrollmentDao; - private final EncryptionKeyDao mEncryptionKeyDao; + /** + * This P-256 ECDSA key is used to verify signatures if {@link + * com.android.adservices.service.FlagsConstants #KEY_DISABLE_FLEDGE_ENROLLMENT_CHECK} is set to + * true. + * + * <p>This enables CTS and integration testing. + * + * <p>To test with this key, {@link SignedContextualAds} should be signed with {@link + * ProtectedAudienceSignatureManager#PRIVATE_TEST_KEY_STRING}. + */ + public static final String PUBLIC_TEST_KEY_STRING = + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE+Eyo0TOllW8as2pTTzxawQ57pXJiH16VERgHqcV1/YpADt3iq6" + + "9vbhwW8Ksi3M0GrxacOuge/AwiM7Uh6+V3PA=="; - public ProtectedAudienceSignatureManager(@NonNull Context context) { - Objects.requireNonNull(context); + /** + * Private key pair of the {@link ProtectedAudienceSignatureManager#PUBLIC_TEST_KEY_STRING} + * + * <p>See {@link ProtectedAudienceSignatureManager#PUBLIC_TEST_KEY_STRING} + */ + public static final String PRIVATE_TEST_KEY_STRING = + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgECetqRr9eE9DKKjILR+hP66Y1niEw/bqPD/MNx" + + "PTMvmhRANCAAT4TKjRM6WVbxqzalNPPFrBDnulcmIfXpURGAepxXX9ikAO3eKrr29uHBbwqyLczQ" + + "avFpw66B78DCIztSHr5Xc8"; - mContext = context; + @NonNull private final EnrollmentDao mEnrollmentDao; + @NonNull private final EncryptionKeyDao mEncryptionKeyDao; + private final boolean mIsEnrollmentCheckEnabled; - mEnrollmentDao = EnrollmentDao.getInstance(mContext); - mEncryptionKeyDao = EncryptionKeyDao.getInstance(mContext); + private final SignatureVerifier mSignatureVerifier; + + public ProtectedAudienceSignatureManager( + @NonNull EnrollmentDao enrollmentDao, + @NonNull EncryptionKeyDao encryptionKeyDao, + boolean isEnrollmentCheckEnabled) { + Objects.requireNonNull(enrollmentDao); + Objects.requireNonNull(encryptionKeyDao); + + mEnrollmentDao = enrollmentDao; + mEncryptionKeyDao = encryptionKeyDao; + mIsEnrollmentCheckEnabled = isEnrollmentCheckEnabled; + + mSignatureVerifier = new ECDSASignatureVerifier(); } @VisibleForTesting ProtectedAudienceSignatureManager( - @NonNull Context context, @NonNull EnrollmentDao enrollmentDao, - @NonNull EncryptionKeyDao encryptionKeyDao) { - mContext = context; + @NonNull EncryptionKeyDao encryptionKeyDao, + @NonNull SignatureVerifier signatureVerifier) { mEnrollmentDao = enrollmentDao; mEncryptionKeyDao = encryptionKeyDao; + mSignatureVerifier = signatureVerifier; + + mIsEnrollmentCheckEnabled = true; + } + + /** + * Returns whether is the given {@link SignedContextualAds} object is valid or not + * + * @param buyer Ad tech's identifier to resolve their public key + * @param signedContextualAds contextual ads object to verify + * @return true if the object is valid else false + */ + public boolean isVerified( + @NonNull AdTechIdentifier buyer, @NonNull SignedContextualAds signedContextualAds) { + Objects.requireNonNull(buyer); + Objects.requireNonNull(signedContextualAds); + + List<byte[]> publicKeys = fetchPublicKeyForAdTech(buyer); + boolean isVerified = false; + SignedContextualAdsHashUtil contextualAdsHashUtil; + for (byte[] publicKey : publicKeys) { + contextualAdsHashUtil = new SignedContextualAdsHashUtil(); + byte[] serialized = contextualAdsHashUtil.serialize(signedContextualAds); + isVerified = + mSignatureVerifier.verify( + publicKey, serialized, signedContextualAds.getSignature()); + } + return isVerified; } @VisibleForTesting - List<String> fetchPublicKeyForAdTech(AdTechIdentifier adTech) { + List<byte[]> fetchPublicKeyForAdTech(AdTechIdentifier adTech) { + Base64.Decoder decoder = Base64.getDecoder(); + if (!mIsEnrollmentCheckEnabled) { + sLogger.v("Enrollment check is disabled, returning the default key"); + return Collections.singletonList(decoder.decode(PUBLIC_TEST_KEY_STRING)); + } + sLogger.v("Fetching EnrollmentData for %s", adTech); EnrollmentData enrollmentData = mEnrollmentDao.getEnrollmentDataForFledgeByAdTechIdentifier(adTech); @@ -81,10 +146,10 @@ public class ProtectedAudienceSignatureManager { mEncryptionKeyDao.getEncryptionKeyFromEnrollmentIdAndKeyType( enrollmentData.getEnrollmentId(), EncryptionKey.KeyType.SIGNING); - sLogger.v("Received %s signing keys", encryptionKeys.size()); + sLogger.v("Received %s signing key(s)", encryptionKeys.size()); return encryptionKeys.stream() .sorted(Comparator.comparingLong(EncryptionKey::getExpiration)) - .map(EncryptionKey::getBody) + .map(key -> decoder.decode(key.getBody())) .collect(Collectors.toList()); } } diff --git a/adservices/service-core/java/com/android/adservices/service/adselection/signature/ThreadUnsafeByteArrayOutputStream.java b/adservices/service-core/java/com/android/adservices/service/adselection/signature/ThreadUnsafeByteArrayOutputStream.java index 4857768a62..a9918549b8 100644 --- a/adservices/service-core/java/com/android/adservices/service/adselection/signature/ThreadUnsafeByteArrayOutputStream.java +++ b/adservices/service-core/java/com/android/adservices/service/adselection/signature/ThreadUnsafeByteArrayOutputStream.java @@ -23,13 +23,13 @@ import java.util.Arrays; /** ByteArrayStream implementation that reduces amount of copy operation */ public class ThreadUnsafeByteArrayOutputStream { private static final int ONE_KILOBYTE = 1024; - private static final int INITIAL_CAPACITY = 100 * ONE_KILOBYTE; + private static final int DEFAULT_INITIAL_CAPACITY = 100 * ONE_KILOBYTE; private static final int SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8; private byte[] mBuffer; private int mCount; public ThreadUnsafeByteArrayOutputStream() { - this.mBuffer = new byte[INITIAL_CAPACITY]; + this.mBuffer = new byte[DEFAULT_INITIAL_CAPACITY]; this.mCount = 0; } diff --git a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentStorageManager.java b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentStorageManager.java index cbd867c0fe..04c6f60300 100644 --- a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentStorageManager.java +++ b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentStorageManager.java @@ -412,8 +412,7 @@ public class AppSearchConsentStorageManager implements IConsentStorage { /** Saves the default consent by apiType. */ @Override - public void recordDefaultConsent(AdServicesApiType apiType, boolean defaultConsent) - throws IOException { + public void recordDefaultConsent(AdServicesApiType apiType, boolean defaultConsent) { mAppSearchConsentWorker.setConsent(apiType.toDefaultConsentDatastoreKey(), defaultConsent); } @@ -436,8 +435,7 @@ public class AppSearchConsentStorageManager implements IConsentStorage { } /** Saves information to the storage that user interacted with consent manually. */ - public void recordUserManualInteractionWithConsent( - @ConsentManager.UserManualInteraction int interaction) { + public void recordUserManualInteractionWithConsent(int interaction) { mAppSearchConsentWorker.recordUserManualInteractionWithConsent(interaction); } diff --git a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentWorker.java b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentWorker.java index 42c5c1ed8b..bda9d17442 100644 --- a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentWorker.java +++ b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentWorker.java @@ -65,15 +65,11 @@ import java.util.stream.Collectors; * source of truth for S-. When a device upgrades from S- to T+, the consent is initialized from * AppSearch. */ -// TODO(b/269798827): Enable for R. @RequiresApi(Build.VERSION_CODES.S) class AppSearchConsentWorker { // At the worker level, we ensure that writes do not conflict with any other writes/reads. private static final ReadWriteLock READ_WRITE_LOCK = new ReentrantReadWriteLock(); - // Timeout for AppSearch write query in milliseconds. - private static final int TIMEOUT_MS = 2000; - private static final String CONSENT_DATABASE_NAME = "adservices_consent"; private static final String APP_CONSENT_DATABASE_NAME = "adservices_app_consent"; private static final String NOTIFICATION_DATABASE_NAME = "adservices_notification"; @@ -82,68 +78,69 @@ class AppSearchConsentWorker { private static final String UX_STATES_DATABASE_NAME = "adservices-ux-states"; // Required for allowing AdServices apk access to read consent written by ExtServices module. - private String mAdservicesPackageName; - private Context mContext; + private final String mAdservicesPackageName; - private ListenableFuture<AppSearchSession> mConsentSearchSession; - private ListenableFuture<AppSearchSession> mAppConsentSearchSession; - private ListenableFuture<AppSearchSession> mNotificationSearchSession; - private ListenableFuture<AppSearchSession> mInteractionsSearchSession; - private ListenableFuture<AppSearchSession> mTopicsSearchSession; - private ListenableFuture<AppSearchSession> mUxStatesSearchSession; + // Timeout for AppSearch write query in milliseconds. + private final int mTimeoutMs; + + private final ListenableFuture<AppSearchSession> mConsentSearchSession; + private final ListenableFuture<AppSearchSession> mAppConsentSearchSession; + private final ListenableFuture<AppSearchSession> mNotificationSearchSession; + private final ListenableFuture<AppSearchSession> mInteractionsSearchSession; + private final ListenableFuture<AppSearchSession> mTopicsSearchSession; + private final ListenableFuture<AppSearchSession> mUxStatesSearchSession; // When reading across APKs, a GlobalSearchSession is needed, hence we use it when reading. - private ListenableFuture<GlobalSearchSession> mGlobalSearchSession; - private Executor mExecutor = AdServicesExecutors.getBackgroundExecutor(); + private final ListenableFuture<GlobalSearchSession> mGlobalSearchSession; + private final Executor mExecutor = AdServicesExecutors.getBackgroundExecutor(); - private List<PackageIdentifier> mPackageIdentifiers = new ArrayList<>(); + private final List<PackageIdentifier> mPackageIdentifiers = new ArrayList<>(); // There is a single user ID for a given process, so this class would not be instantiated // across two user IDs. - private String mUid = getUserIdentifierFromBinderCallingUid(); + private final String mUid = getUserIdentifierFromBinderCallingUid(); private static final String SPLITTER = ","; private AppSearchConsentWorker(@NonNull Context context) { Objects.requireNonNull(context); - mContext = context; // We write with multiple schemas, so we need to initialize sessions per db. mConsentSearchSession = PlatformStorage.createSearchSessionAsync( - new PlatformStorage.SearchContext.Builder(mContext, CONSENT_DATABASE_NAME) + new PlatformStorage.SearchContext.Builder(context, CONSENT_DATABASE_NAME) .build()); mAppConsentSearchSession = PlatformStorage.createSearchSessionAsync( new PlatformStorage.SearchContext.Builder( - mContext, APP_CONSENT_DATABASE_NAME) + context, APP_CONSENT_DATABASE_NAME) .build()); mNotificationSearchSession = PlatformStorage.createSearchSessionAsync( new PlatformStorage.SearchContext.Builder( - mContext, NOTIFICATION_DATABASE_NAME) + context, NOTIFICATION_DATABASE_NAME) .build()); mInteractionsSearchSession = PlatformStorage.createSearchSessionAsync( new PlatformStorage.SearchContext.Builder( - mContext, INTERACTIONS_DATABASE_NAME) + context, INTERACTIONS_DATABASE_NAME) .build()); mTopicsSearchSession = PlatformStorage.createSearchSessionAsync( - new PlatformStorage.SearchContext.Builder(mContext, TOPICS_DATABASE_NAME) + new PlatformStorage.SearchContext.Builder(context, TOPICS_DATABASE_NAME) .build()); mUxStatesSearchSession = PlatformStorage.createSearchSessionAsync( - new PlatformStorage.SearchContext.Builder(mContext, UX_STATES_DATABASE_NAME) + new PlatformStorage.SearchContext.Builder(context, UX_STATES_DATABASE_NAME) .build()); // We use global session for reads since we may perform read on T+ AdServices package to // restore consent data post OTA. mGlobalSearchSession = PlatformStorage.createGlobalSearchSessionAsync( - new PlatformStorage.GlobalSearchContext.Builder(mContext).build()); + new PlatformStorage.GlobalSearchContext.Builder(context).build()); // The package identifier of the AdServices package on T+ should always have access to read // data written by AdExtServices package on S-. - mAdservicesPackageName = getAdServicesPackageName(mContext); + mAdservicesPackageName = getAdServicesPackageName(context); String shaCertsFlagValue = FlagsFactory.getFlags().getAdservicesApkShaCertificate(); for (String shaCert : shaCertsFlagValue.split(SPLITTER)) { @@ -151,6 +148,8 @@ class AppSearchConsentWorker { new PackageIdentifier( mAdservicesPackageName, new Signature(shaCert).toByteArray())); } + + mTimeoutMs = FlagsFactory.getFlags().getAppSearchWriteTimeout(); } /** Get an instance of AppSearchConsentWorker. */ @@ -175,8 +174,8 @@ class AppSearchConsentWorker { /** * 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. + * confirmation that the write operation was successful, then we throw an exception so that user + * does not incorrectly think that the consent is updated. */ void setConsent(@NonNull String apiType, @NonNull Boolean consented) { Objects.requireNonNull(apiType); @@ -193,11 +192,11 @@ class AppSearchConsentWorker { apiType, consented.toString()); dao.writeData(mConsentSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote consent data to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write consent to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write consent to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -235,10 +234,10 @@ class AppSearchConsentWorker { mExecutor, AppSearchAppConsentDao.getRowId(mUid, consentType), AppSearchAppConsentDao.NAMESPACE) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to delete consent to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to delete consent to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -280,11 +279,11 @@ class AppSearchConsentWorker { dao.setApps(apps); } dao.writeData(mAppConsentSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote app consent data to AppSearch (add): " + dao); return true; } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write consent to AppSearch ", e); + LogUtil.e(e, "Failed to write consent to AppSearch"); return false; } finally { READ_WRITE_LOCK.writeLock().unlock(); @@ -293,8 +292,8 @@ class AppSearchConsentWorker { /** * Removes an app from the list of apps with this consentType for this user. 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. + * confirmation that the write operation was successful, then we throw an exception so that user + * does not incorrectly think that the consent is updated. */ void removeAppWithConsent(@NonNull String consentType, @NonNull String app) { Objects.requireNonNull(consentType); @@ -320,11 +319,11 @@ class AppSearchConsentWorker { .filter(filterApp -> !filterApp.equals(app)) .collect(Collectors.toList())); dao.writeData(mAppConsentSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote app consent data to AppSearch (remove): " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write consent to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write consent to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.readLock().unlock(); } @@ -367,11 +366,11 @@ class AppSearchConsentWorker { /* wasNotificationDisplayed= */ wasNotificationDisplayed, /* wasGaUxNotificationDisplayed= */ wasGaUxNotificationDisplayed()); dao.writeData(mNotificationSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote notification data to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write notification data to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write notification data to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -389,11 +388,11 @@ class AppSearchConsentWorker { /* wasNotificationDisplayed= */ wasNotificationDisplayed(), /* wasGaUxNotificationDisplayed= */ wasNotificationDisplayed); dao.writeData(mNotificationSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote notification data to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write notification data to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write notification data to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -426,11 +425,11 @@ class AppSearchConsentWorker { apiType, currentFeatureType.ordinal()); dao.writeData(mInteractionsSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote feature type data to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write interactions data to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write interactions data to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -466,11 +465,11 @@ class AppSearchConsentWorker { apiType, interaction); dao.writeData(mInteractionsSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote interactions data to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write interactions data to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write interactions data to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -512,11 +511,11 @@ class AppSearchConsentWorker { dao.addBlockedTopic(topic); } dao.writeData(mTopicsSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote topics consent data to AppSearch (block): " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write consent to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write consent to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -539,11 +538,11 @@ class AppSearchConsentWorker { } dao.removeBlockedTopic(topic); dao.writeData(mTopicsSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote topics consent data to AppSearch (unblock): " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write consent to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write consent to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -554,7 +553,7 @@ class AppSearchConsentWorker { READ_WRITE_LOCK.writeLock().lock(); try { // We don't do {read, modify, write} here since the DAO has no other information besides - // blocked topics so we can rewrite it. + // blocked topics, so we can rewrite it. AppSearchTopicsConsentDao dao = new AppSearchTopicsConsentDao( mUid, @@ -564,11 +563,11 @@ class AppSearchConsentWorker { List.of(), List.of()); dao.writeData(mTopicsSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote topics consent data to AppSearch (clear): " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write consent to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write consent to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -606,7 +605,7 @@ class AppSearchConsentWorker { AdServicesCommon.ADEXTSERVICES_PACKAGE_NAME_SUFFIX, AdServicesCommon.ADSERVICES_APK_PACKAGE_NAME_SUFFIX); } - // If we don't know the AdServices package name, we can't do a write. + // If we don't know the AdServices package name, we can't write. throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); } @@ -637,11 +636,11 @@ class AppSearchConsentWorker { } dao.setAdIdEnabled(isAdIdEnabled); dao.writeData(mUxStatesSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote the isAdIdEnabled bit to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write the isAdIdEnabled to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write the isAdIdEnabled to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -674,11 +673,11 @@ class AppSearchConsentWorker { } dao.setU18Account(isU18Account); dao.writeData(mUxStatesSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote the isU18Account bit to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write the isU18Account to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write the isU18Account to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -711,11 +710,11 @@ class AppSearchConsentWorker { } dao.setEntryPointEnabled(isEntryPointEnabled); dao.writeData(mUxStatesSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote the isEntryPointEnabled bit to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write the isEntryPointEnabled to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write the isEntryPointEnabled to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -748,11 +747,11 @@ class AppSearchConsentWorker { } dao.setAdultAccount(isAdultAccount); dao.writeData(mUxStatesSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote the isAdultAccount bit to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write the isAdultAccount to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write the isAdultAccount to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -785,11 +784,11 @@ class AppSearchConsentWorker { } dao.setU18NotificationDisplayed(wasU18NotificationDisplayed); dao.writeData(mUxStatesSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote the wasU18NotificationDisplayed bit to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write the wasU18NotificationDisplayed to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write the wasU18NotificationDisplayed to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -822,11 +821,11 @@ class AppSearchConsentWorker { } dao.setUx(ux.toString()); dao.writeData(mUxStatesSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote PrivacySandboxUx to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write PrivacySandboxUx to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write PrivacySandboxUx to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } @@ -865,11 +864,11 @@ class AppSearchConsentWorker { } dao.setEnrollmentChannel(enrollmentChannel.toString()); dao.writeData(mUxStatesSearchSession, mPackageIdentifiers, mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote PrivacySandboxUx to AppSearch: " + dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { - LogUtil.e("Failed to write PrivacySandboxUx to AppSearch ", e); - throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE); + LogUtil.e(e, "Failed to write PrivacySandboxUx to AppSearch"); + throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE, e); } finally { READ_WRITE_LOCK.writeLock().unlock(); } diff --git a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchDao.java b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchDao.java index 4fdf36f675..cd52dbd72b 100644 --- a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchDao.java +++ b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchDao.java @@ -16,6 +16,8 @@ package com.android.adservices.service.appsearch; +import static com.android.adservices.service.consent.ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE; + import android.annotation.NonNull; import android.os.Build; @@ -31,6 +33,7 @@ import androidx.appsearch.app.RemoveByDocumentIdRequest; import androidx.appsearch.app.SearchResults; import androidx.appsearch.app.SearchSpec; import androidx.appsearch.app.SetSchemaRequest; +import androidx.appsearch.app.SetSchemaResponse.MigrationFailure; import androidx.appsearch.exceptions.AppSearchException; import com.android.adservices.AdServicesCommon; @@ -38,7 +41,6 @@ import com.android.adservices.LogUtil; import com.android.adservices.service.Flags; import com.android.adservices.service.FlagsFactory; import com.android.adservices.service.common.AllowLists; -import com.android.adservices.service.consent.ConsentConstants; import com.android.internal.annotations.VisibleForTesting; import com.google.common.util.concurrent.FluentFuture; @@ -58,12 +60,8 @@ import java.util.function.BiFunction; * Base class for all data access objects for AppSearch. This class handles the common logic for * reading from and writing to AppSearch. */ -// TODO(b/269798827): Enable for R. @RequiresApi(Build.VERSION_CODES.S) class AppSearchDao { - // Timeout for AppSearch search query in milliseconds. - private static final int TIMEOUT_MS = 500; - /** * Iterate over the search results returned for the search query by AppSearch. * @@ -88,7 +86,7 @@ class AppSearchDao { // Converts GenericDocument object to the type of object passed in cls. documentResult = genericDocument.toDocumentClass(cls); } catch (AppSearchException e) { - LogUtil.e("Failed to convert GenericDocument to " + cls.getName(), e); + LogUtil.e(e, "Failed to convert GenericDocument to %s", cls.getName()); } } @@ -203,9 +201,11 @@ class AppSearchDao { results -> iterateSearchResults(cls, results, executor), executor) .transform(result -> ((T) result), executor); - return future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + + int timeout = FlagsFactory.getFlags().getAppSearchReadTimeout(); + return future.get(timeout, TimeUnit.MILLISECONDS); } catch (ExecutionException | InterruptedException | TimeoutException e) { - LogUtil.e("getConsent() Appsearch lookup failed with: ", e); + LogUtil.e(e, "getConsent() AppSearch lookup failed"); } return null; } @@ -216,7 +216,7 @@ class AppSearchDao { * we specify the packageIdentifier as that of the T+ AdServices APK, which after OTA, needs * access to the data written before OTA. What is written is the subclass type of DAO. * - * @return the result of the write. + * @return the result of the write operation. */ FluentFuture<AppSearchBatchResult<String, Void>> writeData( @NonNull ListenableFuture<AppSearchSession> appSearchSession, @@ -245,14 +245,17 @@ class AppSearchDao { // If we get failures in schemaResponse then we cannot try // to write. if (!setSchemaResponse.getMigrationFailures().isEmpty()) { + MigrationFailure failure = + setSchemaResponse.getMigrationFailures().get(0); LogUtil.e( "SetSchemaResponse migration failure: " - + setSchemaResponse - .getMigrationFailures() - .get(0)); - throw new RuntimeException( - ConsentConstants - .ERROR_MESSAGE_APPSEARCH_FAILURE); + + failure); + String message = + String.format( + "%s Migration failure: %s", + ERROR_MESSAGE_APPSEARCH_FAILURE, + failure.getAppSearchResult()); + throw new RuntimeException(message); } // The database knows about this schemaType and write can // occur. @@ -264,17 +267,17 @@ class AppSearchDao { executor); return putFuture; } catch (AppSearchException e) { - LogUtil.e("Cannot instantiate AppSearch database: " + e.getMessage()); + LogUtil.e(e, "Cannot instantiate AppSearch database"); + return FluentFuture.from( + Futures.immediateFailedFuture( + new RuntimeException(ERROR_MESSAGE_APPSEARCH_FAILURE, e))); } - return FluentFuture.from( - Futures.immediateFailedFuture( - new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE))); } /** * Delete a row from the database. * - * @return the result of the delete. + * @return the result of the delete operation. */ protected static <T> FluentFuture<AppSearchBatchResult<String, Void>> deleteData( @NonNull Class<T> cls, @@ -308,8 +311,7 @@ class AppSearchDao { .getMigrationFailures() .get(0)); throw new RuntimeException( - ConsentConstants - .ERROR_MESSAGE_APPSEARCH_FAILURE); + ERROR_MESSAGE_APPSEARCH_FAILURE); } // The database knows about this schemaType and write can // occur. @@ -325,6 +327,6 @@ class AppSearchDao { } return FluentFuture.from( Futures.immediateFailedFuture( - new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE))); + new RuntimeException(ERROR_MESSAGE_APPSEARCH_FAILURE))); } } diff --git a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchMeasurementRollbackWorker.java b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchMeasurementRollbackWorker.java index f81566cc2c..b282cfefa1 100644 --- a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchMeasurementRollbackWorker.java +++ b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchMeasurementRollbackWorker.java @@ -28,6 +28,7 @@ import androidx.appsearch.platformstorage.PlatformStorage; import com.android.adservices.LogUtil; import com.android.adservices.concurrency.AdServicesExecutors; +import com.android.adservices.service.FlagsFactory; import com.android.adservices.service.common.compat.FileCompatUtils; import com.android.adservices.service.consent.ConsentConstants; import com.android.adservices.service.measurement.rollback.MeasurementRollbackWorker; @@ -59,7 +60,7 @@ public final class AppSearchMeasurementRollbackWorker implements MeasurementRoll private static final ReadWriteLock READ_WRITE_LOCK = new ReentrantReadWriteLock(); // Timeout for AppSearch write query in milliseconds. - private static final int TIMEOUT_MS = 2000; + private final int mTimeoutMs; private final String mUserId; private final String mAdServicesPackageName; @@ -75,6 +76,8 @@ public final class AppSearchMeasurementRollbackWorker implements MeasurementRoll mSearchSession = PlatformStorage.createSearchSessionAsync( new PlatformStorage.SearchContext.Builder(context, DATABASE_NAME).build()); + + mTimeoutMs = FlagsFactory.getFlags().getAppSearchWriteTimeout(); } /** Return an instance of {@link AppSearchMeasurementRollbackWorker} */ @@ -98,7 +101,7 @@ public final class AppSearchMeasurementRollbackWorker implements MeasurementRoll // don't need to share it with the T package. Thus, we can send an empty list for the // packageIdentifiers parameter. dao.writeData(mSearchSession, List.of(), mExecutor) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Wrote measurement rollback data to AppSearch: %s", dao); } catch (InterruptedException | TimeoutException | ExecutionException e) { LogUtil.e(e, "Failed to write measurement rollback to AppSearch"); @@ -130,7 +133,7 @@ public final class AppSearchMeasurementRollbackWorker implements MeasurementRoll mExecutor, storageIdentifier, AppSearchMeasurementRollbackDao.NAMESPACE) - .get(TIMEOUT_MS, TimeUnit.MILLISECONDS); + .get(mTimeoutMs, TimeUnit.MILLISECONDS); LogUtil.d("Deleted MeasurementRollback data from AppSearch for: %s", storageIdentifier); } catch (InterruptedException | TimeoutException | ExecutionException e) { LogUtil.e(e, "Failed to delete MeasurementRollback data in AppSearch"); diff --git a/adservices/service-core/java/com/android/adservices/service/common/AdServicesCommonServiceImpl.java b/adservices/service-core/java/com/android/adservices/service/common/AdServicesCommonServiceImpl.java index bf002effdc..ecb1e11151 100644 --- a/adservices/service-core/java/com/android/adservices/service/common/AdServicesCommonServiceImpl.java +++ b/adservices/service-core/java/com/android/adservices/service/common/AdServicesCommonServiceImpl.java @@ -58,6 +58,7 @@ import android.content.SharedPreferences; import android.os.Binder; import android.os.Build; import android.os.RemoteException; +import android.os.Trace; import androidx.annotation.RequiresApi; @@ -285,7 +286,9 @@ public class AdServicesCommonServiceImpl extends IAdServicesCommonService.Stub { @NonNull IEnableAdServicesCallback callback) { LogUtil.d(ENABLE_AD_SERVICES_API_CALLED_MESSAGE); + Trace.beginSection("AdServicesCommonService#EnableAdServices_PermissionCheck"); boolean authorizedCaller = PermissionHelper.hasModifyAdServicesStatePermission(mContext); + Trace.endSection(); sBackgroundExecutor.execute( () -> { @@ -308,7 +311,10 @@ public class AdServicesCommonServiceImpl extends IAdServicesCommonService.Stub { return; } + Trace.beginSection("AdServicesCommonService#EnableAdServices_UxEngineFlow"); mUxEngine.start(adServicesStates); + Trace.endSection(); + LogUtil.d("enableAdServices(): UxEngine started."); callback.onResult( diff --git a/adservices/service-core/java/com/android/adservices/service/common/AppManifestConfig.java b/adservices/service-core/java/com/android/adservices/service/common/AppManifestConfig.java index 5db3b171ea..672e443db4 100644 --- a/adservices/service-core/java/com/android/adservices/service/common/AppManifestConfig.java +++ b/adservices/service-core/java/com/android/adservices/service/common/AppManifestConfig.java @@ -16,6 +16,11 @@ package com.android.adservices.service.common; +import static com.android.adservices.service.common.AppManifestConfigCall.RESULT_ALLOWED_APP_ALLOWS_ALL; +import static com.android.adservices.service.common.AppManifestConfigCall.RESULT_ALLOWED_APP_ALLOWS_SPECIFIC_ID; +import static com.android.adservices.service.common.AppManifestConfigCall.RESULT_ALLOWED_BY_DEFAULT_APP_HAS_CONFIG_WITHOUT_API_SECTION; +import static com.android.adservices.service.common.AppManifestConfigCall.RESULT_DISALLOWED_APP_HAS_CONFIG_WITHOUT_API_SECTION; +import static com.android.adservices.service.common.AppManifestConfigCall.RESULT_DISALLOWED_BY_APP; import static com.android.adservices.service.common.AppManifestConfigParser.TAG_ADID; import static com.android.adservices.service.common.AppManifestConfigParser.TAG_APPSETID; import static com.android.adservices.service.common.AppManifestConfigParser.TAG_ATTRIBUTION; @@ -26,6 +31,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import com.android.adservices.LogUtil; +import com.android.adservices.service.common.AppManifestConfigCall.Result; import java.util.function.Supplier; @@ -96,9 +102,9 @@ public final class AppManifestConfig { * Returns if the ad partner is permitted to access Attribution API for config represented by * this object. * - * <p>If the tag is not found in the app manifest config, returns {@code false}. + * <p>See constants in {@link AppManifestConfigCall} for the returned value. */ - public boolean isAllowedAttributionAccess(@NonNull String enrollmentId) { + public @Result int isAllowedAttributionAccess(@NonNull String enrollmentId) { return isAllowedAccess(TAG_ATTRIBUTION, mAttributionConfig, enrollmentId); } @@ -119,9 +125,9 @@ public final class AppManifestConfig { * Returns {@code true} if an ad tech with the given enrollment ID is permitted to access Custom * Audience API for config represented by this object. * - * <p>If the tag is not found in the app manifest config, returns {@code false}. + * <p>See constants in {@link AppManifestConfigCall} for the returned value. */ - public boolean isAllowedCustomAudiencesAccess(@NonNull String enrollmentId) { + public @Result int isAllowedCustomAudiencesAccess(@NonNull String enrollmentId) { return isAllowedAccess(TAG_CUSTOM_AUDIENCES, mCustomAudiencesConfig, enrollmentId); } @@ -140,9 +146,9 @@ public final class AppManifestConfig { * Returns if the ad partner is permitted to access Topics API for config represented by this * object. * - * <p>If the tag is not found in the app manifest config, returns {@code false}. + * <p>See constants in {@link AppManifestConfigCall} for the returned value. */ - public boolean isAllowedTopicsAccess(@NonNull String enrollmentId) { + public @Result int isAllowedTopicsAccess(@NonNull String enrollmentId) { return isAllowedAccess(TAG_TOPICS, mTopicsConfig, enrollmentId); } @@ -159,9 +165,9 @@ public final class AppManifestConfig { /** * Returns if sdk is permitted to access AdId API for config represented by this object. * - * <p>If the tag is not found in the app manifest config, returns {@code false}. + * <p>See constants in {@link AppManifestConfigCall} for the returned value. */ - public boolean isAllowedAdIdAccess(@NonNull String sdk) { + public @Result int isAllowedAdIdAccess(@NonNull String sdk) { return isAllowedAccess(TAG_ADID, mAdIdConfig, sdk); } @@ -181,9 +187,9 @@ public final class AppManifestConfig { /** * Returns if sdk is permitted to access AppSetId API for config represented by this object. * - * <p>If the tag is not found in the app manifest config, returns {@code false}. + * <p>See constants in {@link AppManifestConfigCall} for the returned value. */ - public boolean isAllowedAppSetIdAccess(@NonNull String sdk) { + public @Result int isAllowedAppSetIdAccess(@NonNull String sdk) { return isAllowedAccess(TAG_APPSETID, mAppSetIdConfig, sdk); } @@ -200,15 +206,21 @@ public final class AppManifestConfig { return null; } - private boolean isAllowedAccess( + private @Result int isAllowedAccess( String tag, @Nullable AppManifestApiConfig config, String partnerId) { if (config == null) { LogUtil.v( "app manifest config tag '%s' not found, returning %b", tag, mEnabledByDefault); - return mEnabledByDefault; + return mEnabledByDefault + ? RESULT_ALLOWED_BY_DEFAULT_APP_HAS_CONFIG_WITHOUT_API_SECTION + : RESULT_DISALLOWED_APP_HAS_CONFIG_WITHOUT_API_SECTION; } - - return config.getAllowAllToAccess() - || config.getAllowAdPartnersToAccess().contains(partnerId); + if (config.getAllowAllToAccess()) { + return RESULT_ALLOWED_APP_ALLOWS_ALL; + } + if (config.getAllowAdPartnersToAccess().contains(partnerId)) { + return RESULT_ALLOWED_APP_ALLOWS_SPECIFIC_ID; + } + return RESULT_DISALLOWED_BY_APP; } } diff --git a/adservices/service-core/java/com/android/adservices/service/common/AppManifestConfigCall.java b/adservices/service-core/java/com/android/adservices/service/common/AppManifestConfigCall.java index 449a159c35..79c93bc55c 100644 --- a/adservices/service-core/java/com/android/adservices/service/common/AppManifestConfigCall.java +++ b/adservices/service-core/java/com/android/adservices/service/common/AppManifestConfigCall.java @@ -15,33 +15,162 @@ */ package com.android.adservices.service.common; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.annotation.IntDef; + +import com.android.internal.annotations.VisibleForTesting; + +import java.lang.annotation.Retention; import java.util.Objects; // TODO(b/310270746): make it package-protected when TopicsServiceImplTest is refactored /** Represents a call to a public {@link AppManifestConfigHelper} method. */ public final class AppManifestConfigCall { + + // TODO(b/306417555): use values from statsd atom for constants below + static final int API_UNSPECIFIED = 0; + static final int API_TOPICS = 1; + static final int API_CUSTOM_AUDIENCES = 2; + static final int API_ATTRIBUTION = 3; + + @IntDef({API_TOPICS, API_CUSTOM_AUDIENCES, API_ATTRIBUTION}) + @Retention(SOURCE) + public @interface ApiType {} + + // TODO(b/306417555): use values from statsd atom for constants below + static final int RESULT_UNSPECIFIED = 0; + static final int RESULT_ALLOWED_BY_DEFAULT_APP_DOES_NOT_HAVE_CONFIG = 1; + static final int RESULT_ALLOWED_BY_DEFAULT_APP_HAS_CONFIG_WITHOUT_API_SECTION = 2; + static final int RESULT_ALLOWED_APP_ALLOWS_ALL = 3; + static final int RESULT_ALLOWED_APP_ALLOWS_SPECIFIC_ID = 4; + static final int RESULT_DISALLOWED_APP_DOES_NOT_EXIST = 5; + static final int RESULT_DISALLOWED_APP_CONFIG_PARSING_ERROR = 6; + static final int RESULT_DISALLOWED_APP_DOES_NOT_HAVE_CONFIG = 7; + static final int RESULT_DISALLOWED_APP_HAS_CONFIG_WITHOUT_API_SECTION = 8; + static final int RESULT_DISALLOWED_BY_APP = 9; + static final int RESULT_DISALLOWED_GENERIC_ERROR = 10; + + @IntDef({ + RESULT_ALLOWED_BY_DEFAULT_APP_DOES_NOT_HAVE_CONFIG, + RESULT_ALLOWED_BY_DEFAULT_APP_HAS_CONFIG_WITHOUT_API_SECTION, + RESULT_ALLOWED_APP_ALLOWS_ALL, + RESULT_ALLOWED_APP_ALLOWS_SPECIFIC_ID, + RESULT_DISALLOWED_APP_DOES_NOT_EXIST, + RESULT_DISALLOWED_APP_CONFIG_PARSING_ERROR, + RESULT_DISALLOWED_APP_DOES_NOT_HAVE_CONFIG, + RESULT_DISALLOWED_APP_HAS_CONFIG_WITHOUT_API_SECTION, + RESULT_DISALLOWED_BY_APP, + RESULT_DISALLOWED_GENERIC_ERROR + }) + @Retention(SOURCE) + public @interface Result {} + + @VisibleForTesting static final String INVALID_API_TEMPLATE = "Invalid API: %d"; + public final String packageName; - public boolean appExists; - public boolean appHasConfig; - public boolean enabledByDefault; - public boolean result; + public final @ApiType int api; + public @Result int result; - public AppManifestConfigCall(String packageName) { + public AppManifestConfigCall(String packageName, @ApiType int api) { + switch (api) { + case API_TOPICS: + case API_CUSTOM_AUDIENCES: + case API_ATTRIBUTION: + this.api = api; + break; + default: + throw new IllegalArgumentException(String.format(INVALID_API_TEMPLATE, api)); + } this.packageName = Objects.requireNonNull(packageName, "packageName cannot be null"); } @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AppManifestConfigCall other = (AppManifestConfigCall) obj; + return api == other.api + && Objects.equals(packageName, other.packageName) + && result == other.result; + } + + @Override + public int hashCode() { + return Objects.hash(api, packageName, result); + } + + @Override public String toString() { return "AppManifestConfigCall[pkg=" + packageName - + ", appExists=" - + appExists - + ", appHasConfig=" - + appHasConfig - + ", enabledByDefault=" - + enabledByDefault + + ", api=" + + apiToString(api) + ", result=" - + result + + resultToString(result) + "]"; } + + static String resultToString(@Result int result) { + switch (result) { + case RESULT_UNSPECIFIED: + return "UNSPECIFIED"; + case RESULT_ALLOWED_BY_DEFAULT_APP_DOES_NOT_HAVE_CONFIG: + return "ALLOWED_BY_DEFAULT_APP_DOES_NOT_HAVE_CONFIG"; + case RESULT_ALLOWED_BY_DEFAULT_APP_HAS_CONFIG_WITHOUT_API_SECTION: + return "ALLOWED_BY_DEFAULT_APP_HAS_CONFIG_WITHOUT_API_SECTION"; + case RESULT_ALLOWED_APP_ALLOWS_ALL: + return "ALLOWED_APP_ALLOWS_ALL"; + case RESULT_ALLOWED_APP_ALLOWS_SPECIFIC_ID: + return "ALLOWED_APP_ALLOWS_SPECIFIC_ID"; + case RESULT_DISALLOWED_APP_DOES_NOT_EXIST: + return "DISALLOWED_APP_DOES_NOT_EXIST"; + case RESULT_DISALLOWED_APP_CONFIG_PARSING_ERROR: + return "DISALLOWED_APP_CONFIG_PARSING_ERROR"; + case RESULT_DISALLOWED_APP_DOES_NOT_HAVE_CONFIG: + return "DISALLOWED_APP_DOES_NOT_HAVE_CONFIG"; + case RESULT_DISALLOWED_APP_HAS_CONFIG_WITHOUT_API_SECTION: + return "DISALLOWED_APP_HAS_CONFIG_WITHOUT_API_SECTION"; + case RESULT_DISALLOWED_BY_APP: + return "DISALLOWED_BY_APP"; + case RESULT_DISALLOWED_GENERIC_ERROR: + return "DISALLOWED_GENERIC_ERROR"; + default: + return "INVALID-" + result; + } + } + + static String apiToString(@ApiType int result) { + switch (result) { + case API_UNSPECIFIED: + return "UNSPECIFIED"; + case API_TOPICS: + return "TOPICS"; + case API_CUSTOM_AUDIENCES: + return "CUSTOM_AUDIENCES"; + case API_ATTRIBUTION: + return "ATTRIBUTION"; + default: + return "INVALID-" + result; + } + } + + static boolean isAllowed(@Result int result) { + switch (result) { + case RESULT_ALLOWED_BY_DEFAULT_APP_DOES_NOT_HAVE_CONFIG: + case RESULT_ALLOWED_BY_DEFAULT_APP_HAS_CONFIG_WITHOUT_API_SECTION: + case RESULT_ALLOWED_APP_ALLOWS_ALL: + case RESULT_ALLOWED_APP_ALLOWS_SPECIFIC_ID: + return true; + default: + return false; + } + } } diff --git a/adservices/service-core/java/com/android/adservices/service/common/AppManifestConfigHelper.java b/adservices/service-core/java/com/android/adservices/service/common/AppManifestConfigHelper.java index 0fe09ff8dc..b549717eee 100644 --- a/adservices/service-core/java/com/android/adservices/service/common/AppManifestConfigHelper.java +++ b/adservices/service-core/java/com/android/adservices/service/common/AppManifestConfigHelper.java @@ -16,6 +16,18 @@ package com.android.adservices.service.common; +import static com.android.adservices.service.common.AppManifestConfigCall.API_ATTRIBUTION; +import static com.android.adservices.service.common.AppManifestConfigCall.API_CUSTOM_AUDIENCES; +import static com.android.adservices.service.common.AppManifestConfigCall.API_TOPICS; +import static com.android.adservices.service.common.AppManifestConfigCall.RESULT_ALLOWED_APP_ALLOWS_SPECIFIC_ID; +import static com.android.adservices.service.common.AppManifestConfigCall.RESULT_ALLOWED_BY_DEFAULT_APP_DOES_NOT_HAVE_CONFIG; +import static com.android.adservices.service.common.AppManifestConfigCall.RESULT_DISALLOWED_APP_CONFIG_PARSING_ERROR; +import static com.android.adservices.service.common.AppManifestConfigCall.RESULT_DISALLOWED_APP_DOES_NOT_EXIST; +import static com.android.adservices.service.common.AppManifestConfigCall.RESULT_DISALLOWED_APP_DOES_NOT_HAVE_CONFIG; +import static com.android.adservices.service.common.AppManifestConfigCall.RESULT_DISALLOWED_BY_APP; +import static com.android.adservices.service.common.AppManifestConfigCall.isAllowed; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__APP_MANIFEST_CONFIG_PARSING_ERROR; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__APP_MANIFEST_CONFIG_PARSING_ERROR; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON; @@ -62,6 +74,7 @@ public class AppManifestConfigHelper { @NonNull String enrollmentId) { return isAllowedApiAccess( "isAllowedAttributionAccess()", + API_ATTRIBUTION, appPackageName, enrollmentId, config -> config.isAllowedAttributionAccess(enrollmentId)); @@ -80,6 +93,7 @@ public class AppManifestConfigHelper { @NonNull String enrollmentId) { return isAllowedApiAccess( "isAllowedCustomAudiencesAccess()", + API_CUSTOM_AUDIENCES, appPackageName, enrollmentId, config -> config.isAllowedCustomAudiencesAccess(enrollmentId)); @@ -101,14 +115,17 @@ public class AppManifestConfigHelper { @NonNull String enrollmentId) { return isAllowedApiAccess( "isAllowedTopicsAccess()", + API_TOPICS, appPackageName, enrollmentId, config -> { // If the request comes directly from the app, check that the app has declared // that it includes this Sdk library. if (!useSandboxCheck) { - return config.getIncludesSdkLibraryConfig().contains(enrollmentId) - && config.isAllowedTopicsAccess(enrollmentId); + return (config.getIncludesSdkLibraryConfig().contains(enrollmentId) + && isAllowed(config.isAllowedTopicsAccess(enrollmentId))) + ? RESULT_ALLOWED_APP_ALLOWS_SPECIFIC_ID + : RESULT_DISALLOWED_BY_APP; } // If the request comes from the SdkRuntime, then the app had to have declared @@ -118,29 +135,22 @@ public class AppManifestConfigHelper { } @Nullable - private static XmlResourceParser getXmlParser(AppManifestConfigCall call) + private static XmlResourceParser getXmlParser(String appPackageName) throws NameNotFoundException, XmlParseException, XmlPullParserException, IOException { Context context = ApplicationContextSingleton.get(); - String appPackageName = call.packageName; - LogUtil.v("getXmlParser(%s): context=%s", call.packageName, context); + LogUtil.v("getXmlParser(%s): context=%s", appPackageName, context); + // NOTE: resources is only used pre-S, but it must be called regardless to make sure the app // exists Resources resources = context.getPackageManager().getResourcesForApplication(appPackageName); - call.appExists = true; - Integer resId = SdkLevel.isAtLeastS() ? getAdServicesConfigResourceIdOnExistingPackageOnSPlus( context, appPackageName) : getAdServicesConfigResourceIdOnRMinus(context, resources, appPackageName); - XmlResourceParser xmlResourceParser = null; - if (resId != null) { - xmlResourceParser = resources.getXml(resId); - call.appHasConfig = true; - } - return xmlResourceParser; + return resId != null ? resources.getXml(resId) : null; } @Nullable @@ -171,28 +181,37 @@ public class AppManifestConfigHelper { private static boolean isAllowedApiAccess( String method, + int api, String appPackageName, String enrollmentId, ApiAccessChecker checker) { Objects.requireNonNull(appPackageName); Objects.requireNonNull(enrollmentId); - AppManifestConfigCall call = new AppManifestConfigCall(appPackageName); - call.enabledByDefault = FlagsFactory.getFlags().getAppConfigReturnsEnabledByDefault(); + + AppManifestConfigCall call = new AppManifestConfigCall(appPackageName, api); + boolean enabledByDefault = FlagsFactory.getFlags().getAppConfigReturnsEnabledByDefault(); + try { - XmlResourceParser in = getXmlParser(call); + XmlResourceParser in = getXmlParser(appPackageName); if (in == null) { + call.result = + enabledByDefault + ? RESULT_ALLOWED_BY_DEFAULT_APP_DOES_NOT_HAVE_CONFIG + : RESULT_DISALLOWED_APP_DOES_NOT_HAVE_CONFIG; LogUtil.v( "%s: returning %b for app (%s) that doesn't have the AdServices XML config", - method, call.enabledByDefault, appPackageName); - return call.enabledByDefault; + method, enabledByDefault, appPackageName); + return enabledByDefault; } AppManifestConfig appManifestConfig = - AppManifestConfigParser.getConfig(in, call.enabledByDefault); + AppManifestConfigParser.getConfig(in, enabledByDefault); call.result = checker.isAllowedAccess(appManifestConfig); } catch (NameNotFoundException e) { + call.result = RESULT_DISALLOWED_APP_DOES_NOT_EXIST; LogUtil.v( "Name not found while looking for manifest for app %s: %s", appPackageName, e); } catch (Exception e) { + call.result = RESULT_DISALLOWED_APP_CONFIG_PARSING_ERROR; LogUtil.e(e, "App manifest parse failed."); ErrorLogUtil.e( e, @@ -201,10 +220,10 @@ public class AppManifestConfigHelper { } finally { AppManifestConfigMetricsLogger.logUsage(call); } - return call.result; + return isAllowed(call.result); } private interface ApiAccessChecker { - boolean isAllowedAccess(AppManifestConfig config); + int isAllowedAccess(AppManifestConfig config); } } diff --git a/adservices/service-core/java/com/android/adservices/service/common/AppManifestConfigMetricsLogger.java b/adservices/service-core/java/com/android/adservices/service/common/AppManifestConfigMetricsLogger.java index d573e05ef3..17524ee946 100644 --- a/adservices/service-core/java/com/android/adservices/service/common/AppManifestConfigMetricsLogger.java +++ b/adservices/service-core/java/com/android/adservices/service/common/AppManifestConfigMetricsLogger.java @@ -15,9 +15,13 @@ */ package com.android.adservices.service.common; +import static com.android.adservices.service.common.AppManifestConfigCall.RESULT_UNSPECIFIED; +import static com.android.adservices.service.common.AppManifestConfigCall.apiToString; +import static com.android.adservices.service.common.AppManifestConfigCall.resultToString; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__APP_MANIFEST_CONFIG_LOGGING_ERROR; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SHARED_PREF_EXCEPTION; 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__COMMON; import android.content.Context; import android.content.SharedPreferences; @@ -27,12 +31,15 @@ import com.android.adservices.LogUtil; import com.android.adservices.concurrency.AdServicesExecutors; import com.android.adservices.errorlogging.ErrorLogUtil; import com.android.adservices.service.FlagsFactory; +import com.android.adservices.service.common.AppManifestConfigCall.ApiType; +import com.android.adservices.service.common.AppManifestConfigCall.Result; import com.android.adservices.service.common.compat.FileCompatUtils; import com.android.adservices.shared.common.ApplicationContextSingleton; import com.android.internal.annotations.VisibleForTesting; import java.io.File; import java.io.PrintWriter; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -45,40 +52,39 @@ public final class AppManifestConfigMetricsLogger { static final String PREFS_NAME = FileCompatUtils.getAdservicesFilename("AppManifestConfigMetricsLogger"); - private static final int NOT_SET = -1; - private static final int FLAG_APP_EXISTS = 0x1; - private static final int FLAG_APP_HAS_CONFIG = 0x2; - private static final int FLAG_ENABLED_BY_DEFAULT = 0x4; + @VisibleForTesting static final String PREFS_KEY_TEMPLATE = "%s-%d"; // TODO(b/310270746): make it package-protected when TopicsServiceImplTest is refactored /** Represents a call to a public {@link AppManifestConfigHelper} method. */ /** Logs the app usage. */ public static void logUsage(AppManifestConfigCall call) { Objects.requireNonNull(call, "call cannot be null"); + + // Cannot be RESULT_UNSPECIFIED because that's used to check if the shared preferences value + // doesn't exist yet + if (call.result == RESULT_UNSPECIFIED) { + LogUtil.e("invalid call result: %s", call); + ErrorLogUtil.e( + AD_SERVICES_ERROR_REPORTED__ERROR_CODE__APP_MANIFEST_CONFIG_LOGGING_ERROR, + AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON); + return; + } AdServicesExecutors.getBackgroundExecutor().execute(() -> handleLogUsage(call)); } private static void handleLogUsage(AppManifestConfigCall call) { Context context = ApplicationContextSingleton.get(); try { - int newValue = - (call.appExists ? FLAG_APP_EXISTS : 0) - | (call.appHasConfig ? FLAG_APP_HAS_CONFIG : 0) - | (call.enabledByDefault ? FLAG_ENABLED_BY_DEFAULT : 0); + @Result int newValue = call.result; LogUtil.d( - "AppManifestConfigMetricsLogger.logUsage(): app=[name=%s, exists=%b," - + " hasConfig=%b], enabledByDefault=%b, newValue=%d", - call.packageName, - call.appExists, - call.appHasConfig, - call.enabledByDefault, - newValue); + "AppManifestConfigMetricsLogger.logUsage(): call=%s, newValue=%d", + call, newValue); SharedPreferences prefs = getPrefs(context); - String key = call.packageName; + String key = String.format(Locale.US, PREFS_KEY_TEMPLATE, call.packageName, call.api); - int currentValue = prefs.getInt(key, NOT_SET); - if (currentValue == NOT_SET) { + @Result int currentValue = prefs.getInt(key, RESULT_UNSPECIFIED); + if (currentValue == RESULT_UNSPECIFIED) { LogUtil.v("Logging for the first time (value=%d)", newValue); } else if (currentValue != newValue) { LogUtil.v("Logging as value change (was %d)", currentValue); @@ -95,28 +101,14 @@ public final class AppManifestConfigMetricsLogger { LogUtil.v("Changes committed"); } else { LogUtil.e( - "logUsage(ctx, file=%s, app=%s, appExist=%b, appHasConfig=%b," - + " enabledByDefault=%b, newValue=%d): failed to commit", - PREFS_NAME, - call.packageName, - call.appExists, - call.appHasConfig, - call.enabledByDefault, - newValue); + "logUsage(ctx, file=%s, call=%s, newValue=%d): failed to commit", + PREFS_NAME, call, newValue); ErrorLogUtil.e( AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SHARED_PREF_UPDATE_FAILURE, AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON); } } catch (Exception e) { - LogUtil.e( - e, - "logUsage(ctx, file=%s, app=%s, appExist=%b, appHasConfig=%b," - + " enabledByDefault=%b) failed", - PREFS_NAME, - call.packageName, - call.appExists, - call.appHasConfig, - call.enabledByDefault); + LogUtil.e(e, "logUsage(ctx, file=%s, call=%s) failed", PREFS_NAME, call); ErrorLogUtil.e( e, AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SHARED_PREF_EXCEPTION, @@ -126,13 +118,13 @@ public final class AppManifestConfigMetricsLogger { /** Dumps the internal state. */ public static void dump(Context context, PrintWriter pw) { + String prefix = " "; pw.println("AppManifestConfigMetricsLogger"); - String prefix = " "; - @SuppressWarnings("NewAdServicesFile") // PREFS_NAME already called FileCompatUtils // NOTE: shared_prefs is hard-coded on ContextImpl, but unfortunately Context doesn't offer // any API we could use here to get that path (getSharedPreferencesPath() is @removed and // the available APIs return a SharedPreferences, not a File). + @SuppressWarnings("NewAdServicesFile") // PREFS_NAME already called FileCompatUtils String path = new File(context.getDataDir() + "/shared_prefs", PREFS_NAME).getAbsolutePath(); pw.printf("%sPreferences file: %s.xml\n", prefix, path); @@ -147,19 +139,25 @@ public final class AppManifestConfigMetricsLogger { String prefix2 = prefix + " "; for (Entry<String, ?> pref : appPrefs.entrySet()) { - String app = pref.getKey(); + String key = pref.getKey(); + String appAndApi = key; + try { + String[] keyParts = key.split("-"); + String app = keyParts[0]; + @ApiType int api = Integer.parseInt(keyParts[1]); + appAndApi = app + "-" + apiToString(api); + } catch (Exception e) { + LogUtil.e(e, "failed to parse key %s", key); + } Object value = pref.getValue(); if (value instanceof Integer) { - int flags = (Integer) value; - boolean appExists = (flags & FLAG_APP_EXISTS) != 0; - boolean appHasConfig = (flags & FLAG_APP_HAS_CONFIG) != 0; - boolean enabledByDefault = (flags & FLAG_ENABLED_BY_DEFAULT) != 0; - pw.printf( - "%s%s: rawValue=%d, appExists=%b, appHasConfig=%b, enabledByDefault=%b\n", - prefix2, app, flags, appExists, appHasConfig, enabledByDefault); + @Result int result = (Integer) value; + pw.printf("%s%s: %s\n", prefix2, appAndApi, resultToString(result)); } else { // Shouldn't happen - pw.printf(" %s: unexpected value %s (class %s):\n", app, value, value.getClass()); + pw.printf( + " %s: unexpected value %s (class %s):\n", + appAndApi, value, value.getClass()); } } } diff --git a/adservices/service-core/java/com/android/adservices/service/common/FledgeAuthorizationFilter.java b/adservices/service-core/java/com/android/adservices/service/common/FledgeAuthorizationFilter.java index c99b0b9a39..7809ecd409 100644 --- a/adservices/service-core/java/com/android/adservices/service/common/FledgeAuthorizationFilter.java +++ b/adservices/service-core/java/com/android/adservices/service/common/FledgeAuthorizationFilter.java @@ -123,47 +123,22 @@ public class FledgeAuthorizationFilter { * @param apiNameLoggingId the id of the api being called * @throws SecurityException if the package did not declare custom audience permission */ - public void assertAppDeclaredCustomAudiencePermission( + public void assertAppDeclaredPermission( @NonNull Context context, @NonNull String appPackageName, int apiNameLoggingId) throws SecurityException { Objects.requireNonNull(context); Objects.requireNonNull(appPackageName); if (!PermissionHelper.hasCustomAudiencesPermission(context, appPackageName)) { - logAndThrowPermissionFailure(apiNameLoggingId); - } - } - - /** - * Check if the app had declared the protected signals permission. - * - * @param context api service context - * @param apiNameLoggingId the id of the api being called - * @throws SecurityException if the package did not declare custom audience permission - */ - public void assertAppDeclaredProtectedSignalsPermission( - @NonNull Context context, @NonNull String appPackageName, int apiNameLoggingId) - throws SecurityException { - Objects.requireNonNull(context); - Objects.requireNonNull(appPackageName); - - if (!PermissionHelper.hasProtectedSignalsPermission(context, appPackageName)) { - /* - * Using the same message for both since getAdSelectionData can be called with either - * permission and we don't want the error message to depend on which is checked first. - */ - logAndThrowPermissionFailure(apiNameLoggingId); + sLogger.v("Permission not declared by caller in API %d", apiNameLoggingId); + mAdServicesLogger.logFledgeApiCallStats( + apiNameLoggingId, STATUS_PERMISSION_NOT_REQUESTED, 0); + throw new SecurityException( + AdServicesStatusUtils + .SECURITY_EXCEPTION_PERMISSION_NOT_REQUESTED_ERROR_MESSAGE); } } - private void logAndThrowPermissionFailure(int apiNameLoggingId) { - sLogger.v("Permission not declared by caller in API %d", apiNameLoggingId); - mAdServicesLogger.logFledgeApiCallStats( - apiNameLoggingId, STATUS_PERMISSION_NOT_REQUESTED, 0); - throw new SecurityException( - AdServicesStatusUtils.SECURITY_EXCEPTION_PERMISSION_NOT_REQUESTED_ERROR_MESSAGE); - } - /** * Check if a certain ad tech is enrolled and authorized to perform the operation for the * package. diff --git a/adservices/service-core/java/com/android/adservices/service/common/PackageChangedReceiver.java b/adservices/service-core/java/com/android/adservices/service/common/PackageChangedReceiver.java index e2afca1366..35d6ff658c 100644 --- a/adservices/service-core/java/com/android/adservices/service/common/PackageChangedReceiver.java +++ b/adservices/service-core/java/com/android/adservices/service/common/PackageChangedReceiver.java @@ -295,6 +295,10 @@ public class PackageChangedReceiver extends BroadcastReceiver { @VisibleForTesting void consentOnPackageFullyRemoved( @NonNull Context context, @NonNull Uri packageUri, int packageUid) { + if (!SdkLevel.isAtLeastS()) { + LogUtil.d("consentOnPackageFullyRemoved is not needed on Android R, returning..."); + return; + } Objects.requireNonNull(context); Objects.requireNonNull(packageUri); diff --git a/adservices/service-core/java/com/android/adservices/service/common/PermissionHelper.java b/adservices/service-core/java/com/android/adservices/service/common/PermissionHelper.java index 67142b6d3e..d02225e4ed 100644 --- a/adservices/service-core/java/com/android/adservices/service/common/PermissionHelper.java +++ b/adservices/service-core/java/com/android/adservices/service/common/PermissionHelper.java @@ -101,16 +101,6 @@ public final class PermissionHelper { } /** - * @return {@code true} if the caller has the permission to invoke Protected Signals APIs. - */ - public static boolean hasProtectedSignalsPermission( - @NonNull Context context, @NonNull String appPackageName) { - // TODO(b/236268316): Add check for SDK permission. - return hasPermission( - context, appPackageName, AdServicesPermissions.ACCESS_ADSERVICES_PROTECTED_SIGNALS); - } - - /** * @return {@code true} if the caller has the permission to invoke AdService's state * modification API. */ diff --git a/adservices/service-core/java/com/android/adservices/service/common/httpclient/AdServicesHttpsClient.java b/adservices/service-core/java/com/android/adservices/service/common/httpclient/AdServicesHttpsClient.java index f809950c9e..7ab2ff9fac 100644 --- a/adservices/service-core/java/com/android/adservices/service/common/httpclient/AdServicesHttpsClient.java +++ b/adservices/service-core/java/com/android/adservices/service/common/httpclient/AdServicesHttpsClient.java @@ -175,7 +175,7 @@ public class AdServicesHttpsClient { // Setting true explicitly to follow redirects Uri uri = Uri.parse(url.toString()); if (WebAddresses.isLocalhost(uri) && devContext.getDevOptionsEnabled()) { - LogUtil.v("Using unsafe HTTPS for url ", url.toString()); + LogUtil.v("Using unsafe HTTPS for url %s", url.toString()); urlConnection.setSSLSocketFactory(getUnsafeSslSocketFactory()); } else if (WebAddresses.isLocalhost(uri)) { LogUtil.v( 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; } diff --git a/adservices/service-core/java/com/android/adservices/service/customaudience/CustomAudienceServiceImpl.java b/adservices/service-core/java/com/android/adservices/service/customaudience/CustomAudienceServiceImpl.java index 8373bb5ebb..fcab6c9a88 100644 --- a/adservices/service-core/java/com/android/adservices/service/customaudience/CustomAudienceServiceImpl.java +++ b/adservices/service-core/java/com/android/adservices/service/customaudience/CustomAudienceServiceImpl.java @@ -203,8 +203,7 @@ public class CustomAudienceServiceImpl extends ICustomAudienceService.Stub { } // Caller permissions must be checked in the binder thread, before anything else - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( - mContext, ownerPackageName, apiName); + mFledgeAuthorizationFilter.assertAppDeclaredPermission(mContext, ownerPackageName, apiName); final int callerUid = getCallingUid(apiName); final DevContext devContext = mDevContextFilter.createDevContext(); @@ -301,7 +300,7 @@ public class CustomAudienceServiceImpl extends ICustomAudienceService.Stub { } // Caller permissions must be checked in the binder thread, before anything else - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, input.getCallerPackageName(), apiName); final int callerUid = getCallingUid(apiName); @@ -386,8 +385,7 @@ public class CustomAudienceServiceImpl extends ICustomAudienceService.Stub { } // Caller permissions must be checked in the binder thread, before anything else - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( - mContext, ownerPackageName, apiName); + mFledgeAuthorizationFilter.assertAppDeclaredPermission(mContext, ownerPackageName, apiName); final int callerUid = getCallingUid(apiName); final DevContext devContext = mDevContextFilter.createDevContext(); @@ -506,7 +504,7 @@ public class CustomAudienceServiceImpl extends ICustomAudienceService.Stub { } // Caller permissions must be checked with a non-null callingAppPackageName - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, devContext.getCallingAppPackageName(), apiName); CustomAudienceDao customAudienceDao = mCustomAudienceImpl.getCustomAudienceDao(); @@ -567,7 +565,7 @@ public class CustomAudienceServiceImpl extends ICustomAudienceService.Stub { } // Caller permissions must be checked with a non-null callingAppPackageName - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, devContext.getCallingAppPackageName(), apiName); CustomAudienceDao customAudienceDao = mCustomAudienceImpl.getCustomAudienceDao(); @@ -616,7 +614,7 @@ public class CustomAudienceServiceImpl extends ICustomAudienceService.Stub { } // Caller permissions must be checked with a non-null callingAppPackageName - mFledgeAuthorizationFilter.assertAppDeclaredCustomAudiencePermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, devContext.getCallingAppPackageName(), apiName); CustomAudienceDao customAudienceDao = mCustomAudienceImpl.getCustomAudienceDao(); diff --git a/adservices/service-core/java/com/android/adservices/service/devapi/DevContextFilter.java b/adservices/service-core/java/com/android/adservices/service/devapi/DevContextFilter.java index 9e4b05ddc2..5a78bdaea2 100644 --- a/adservices/service-core/java/com/android/adservices/service/devapi/DevContextFilter.java +++ b/adservices/service-core/java/com/android/adservices/service/devapi/DevContextFilter.java @@ -147,7 +147,7 @@ public class DevContextFilter { } catch (PackageManager.NameNotFoundException e) { LogUtil.w( - "Unable to retrieve application info for app with ID %d and resolved package " + "Unable to retrieve application info for app with ID %s and resolved package " + "name '%s', considering not debuggable for safety.", callingAppPackage, callingAppPackage); return false; diff --git a/adservices/service-core/java/com/android/adservices/service/encryptionkey/EncryptionKey.java b/adservices/service-core/java/com/android/adservices/service/encryptionkey/EncryptionKey.java index 1938979634..07dc2aa9c9 100644 --- a/adservices/service-core/java/com/android/adservices/service/encryptionkey/EncryptionKey.java +++ b/adservices/service-core/java/com/android/adservices/service/encryptionkey/EncryptionKey.java @@ -135,6 +135,21 @@ public class EncryptionKey { return mLastFetchTime; } + /** Returns the builder for the instance */ + public EncryptionKey.Builder cloneToBuilder() { + return new EncryptionKey.Builder() + .setId(this.mId) + .setKeyType(this.mKeyType) + .setEnrollmentId(this.mEnrollmentId) + .setReportingOrigin(this.mReportingOrigin) + .setEncryptionKeyUrl(this.mEncryptionKeyUrl) + .setProtocolType(this.mProtocolType) + .setKeyCommitmentId(this.mKeyCommitmentId) + .setBody(this.mBody) + .setExpiration(this.mExpiration) + .setLastFetchTime(this.mLastFetchTime); + } + /** Builder for {@link EncryptionKey}. */ public static final class Builder { private final EncryptionKey mBuilding; diff --git a/adservices/service-core/java/com/android/adservices/service/enrollment/EnrollmentData.java b/adservices/service-core/java/com/android/adservices/service/enrollment/EnrollmentData.java index d166f3b6a5..6cb1f8c0a2 100644 --- a/adservices/service-core/java/com/android/adservices/service/enrollment/EnrollmentData.java +++ b/adservices/service-core/java/com/android/adservices/service/enrollment/EnrollmentData.java @@ -140,6 +140,21 @@ public class EnrollmentData { return Arrays.asList(input.trim().split(SEPARATOR)); } + /** Returns the builder for the instance */ + @NonNull + public EnrollmentData.Builder cloneToBuilder() { + return new EnrollmentData.Builder() + .setEnrollmentId(this.mEnrollmentId) + .setCompanyId(this.mCompanyId) + .setSdkNames(this.mSdkNames) + .setAttributionSourceRegistrationUrl(this.mAttributionSourceRegistrationUrl) + .setAttributionTriggerRegistrationUrl(this.mAttributionTriggerRegistrationUrl) + .setAttributionReportingUrl(this.mAttributionReportingUrl) + .setRemarketingResponseBasedRegistrationUrl( + this.mRemarketingResponseBasedRegistrationUrl) + .setEncryptionKeyUrl(this.mEncryptionKeyUrl); + } + /** Builder for {@link EnrollmentData}. */ public static final class Builder { private final EnrollmentData mBuilding; diff --git a/adservices/service-core/java/com/android/adservices/service/exception/ConsentStorageDeferException.java b/adservices/service-core/java/com/android/adservices/service/exception/ConsentStorageDeferException.java new file mode 100644 index 0000000000..5edf8e68fb --- /dev/null +++ b/adservices/service-core/java/com/android/adservices/service/exception/ConsentStorageDeferException.java @@ -0,0 +1,26 @@ +/* + * 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.exception; + +import java.io.IOException; + +/** + * Exception class to indicate operation is not supported by current storage manager. This exception + * will always be caught in {@link ConsentCompositeStorage}, and ConsentCompositeStorage will call + * the next IConsentStorage instance to get/set the right value. + */ +public final class ConsentStorageDeferException extends IOException {} diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/PrivacyParams.java b/adservices/service-core/java/com/android/adservices/service/measurement/PrivacyParams.java index 00a945fa96..b62df02677 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/PrivacyParams.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/PrivacyParams.java @@ -37,11 +37,6 @@ public final class PrivacyParams { public static final int EVENT_SOURCE_MAX_REPORTS = 1; /** - * Max reports for Install Attributed 'Navigation' {@link Source}. - */ - public static final int INSTALL_ATTR_NAVIGATION_SOURCE_MAX_REPORTS = 3; - - /** * Max reports for Install Attributed 'Event' {@link Source}. */ public static final int INSTALL_ATTR_EVENT_SOURCE_MAX_REPORTS = 2; diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/Source.java b/adservices/service-core/java/com/android/adservices/service/measurement/Source.java index 2fe5e16b0d..35da5c7058 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/Source.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/Source.java @@ -121,6 +121,7 @@ public class Source { if (mParsedEventReportWindows != null) { return mParsedEventReportWindows; } + if (mEventReportWindows == null) { return null; } @@ -140,7 +141,8 @@ public class Source { } /** - * Returns parsed or default value of event report windows. + * Returns parsed or default value of event report windows (can be used during {@code Source} + * construction since the method does not require a {@code Source} object). * * @param eventReportWindows string to be parsed * @param sourceType Source's Type @@ -149,13 +151,13 @@ public class Source { * @return parsed or default value */ @Nullable - public static List<Pair<Long, Long>> getOrDefaultEventReportWindows( + public static List<Pair<Long, Long>> getOrDefaultEventReportWindowsForFlex( @Nullable JSONObject eventReportWindows, @NonNull SourceType sourceType, long expiryDelta, @NonNull Flags flags) { if (eventReportWindows == null) { - return getDefaultEventReportWindows(expiryDelta, sourceType, flags); + return getDefaultEventReportWindowsForFlex(expiryDelta, sourceType, flags); } return parseEventReportWindows(eventReportWindows); } @@ -180,17 +182,17 @@ public class Source { @NonNull JSONObject jsonObject) { List<Pair<Long, Long>> result = new ArrayList<>(); try { - long startDuration = 0; + long startTime = 0L; if (!jsonObject.isNull("start_time")) { - startDuration = jsonObject.getLong("start_time"); + startTime = jsonObject.getLong("start_time"); } JSONArray endTimesJSON = jsonObject.getJSONArray("end_times"); for (int i = 0; i < endTimesJSON.length(); i++) { - long endDuration = endTimesJSON.getLong(i); - Pair<Long, Long> window = new Pair<>(startDuration, endDuration); + long endTime = endTimesJSON.getLong(i); + Pair<Long, Long> window = Pair.create(startTime, endTime); result.add(window); - startDuration = endDuration; + startTime = endTime; } } catch (JSONException e) { LoggerFactory.getMeasurementLogger() @@ -200,24 +202,25 @@ public class Source { return result; } - private static List<Pair<Long, Long>> getDefaultEventReportWindows( + private static List<Pair<Long, Long>> getDefaultEventReportWindowsForFlex( long expiryDelta, SourceType sourceType, Flags flags) { List<Pair<Long, Long>> result = new ArrayList<>(); - List<Long> defaultEarlyWindows = - EventReportWindowCalcDelegate.getDefaultEarlyReportingWindows(sourceType, false); - List<Long> earlyWindows = + // Obtain default early report windows without regard to install-related behaviour. + List<Long> defaultEarlyWindowEnds = + EventReportWindowCalcDelegate.getDefaultEarlyReportingWindowEnds(sourceType, false); + List<Long> earlyWindowEnds = new EventReportWindowCalcDelegate(flags) - .getConfiguredOrDefaultEarlyReportingWindows( - sourceType, defaultEarlyWindows, false); + .getConfiguredOrDefaultEarlyReportingWindowEnds( + sourceType, defaultEarlyWindowEnds); long windowStart = 0; - for (long earlyWindow : earlyWindows) { - if (earlyWindow >= expiryDelta) { - continue; + for (long earlyWindowEnd : earlyWindowEnds) { + if (earlyWindowEnd >= expiryDelta) { + break; } - result.add(new Pair<>(windowStart, earlyWindow)); - windowStart = earlyWindow; + result.add(Pair.create(windowStart, earlyWindowEnd)); + windowStart = earlyWindowEnd; } - result.add(new Pair<>(windowStart, expiryDelta)); + result.add(Pair.create(windowStart, expiryDelta)); return result; } @@ -234,8 +237,7 @@ public class Source { } private double getInformationGainThreshold(Flags flags) { - int destinationMultiplier = getDestinationTypeMultiplier(flags); - if (destinationMultiplier == 2) { + if (getDestinationTypeMultiplier(flags) == 2) { return mSourceType == SourceType.EVENT ? flags.getMeasurementFlexApiMaxInformationGainDualDestinationEvent() : flags.getMeasurementFlexApiMaxInformationGainDualDestinationNavigation(); @@ -262,32 +264,31 @@ public class Source { setFlipProbability(mTriggerSpecs.getFlipProbability(this, flags)); return; } - boolean installCase = SourceNoiseHandler.isInstallDetectionEnabled(this); EventReportWindowCalcDelegate eventReportWindowCalcDelegate = new EventReportWindowCalcDelegate(flags); int reportingWindowCountForNoising = - eventReportWindowCalcDelegate.getReportingWindowCountForNoising(this, installCase); - int maxReportCount = - eventReportWindowCalcDelegate.getMaxReportCount(this, installCase); - int destinationMultiplier = getDestinationTypeMultiplier(flags); + eventReportWindowCalcDelegate.getReportingWindowCountForNoising(this); long numberOfStates = Combinatorics.getNumberOfStarsAndBarsSequences( - /*numStars=*/ maxReportCount, + /*numStars=*/ eventReportWindowCalcDelegate.getMaxReportCount(this), /*numBars=*/ getTriggerDataCardinality() * reportingWindowCountForNoising - * destinationMultiplier); + * getDestinationTypeMultiplier(flags)); setNumStates(numberOfStates); setFlipProbability(Combinatorics.getFlipProbability(numberOfStates)); } + /** Should source report coarse destinations */ + public boolean shouldReportCoarseDestinations(Flags flags) { + return flags.getMeasurementEnableCoarseEventReportDestinations() + && hasCoarseEventReportDestinations(); + } + /** * Returns the number of destination types to use in privacy computations. */ public int getDestinationTypeMultiplier(Flags flags) { - boolean shouldReportCoarseDestinations = - flags.getMeasurementEnableCoarseEventReportDestinations() - && hasCoarseEventReportDestinations(); - return !shouldReportCoarseDestinations && hasAppDestinations() + return !shouldReportCoarseDestinations(flags) && hasAppDestinations() && hasWebDestinations() ? SourceNoiseHandler.DUAL_DESTINATION_IMPRESSION_NOISE_MULTIPLIER : SourceNoiseHandler.SINGLE_DESTINATION_IMPRESSION_NOISE_MULTIPLIER; @@ -650,9 +651,9 @@ public class Source { /** * Time when {@link Source} event report window will expire. (Appends the Event Time to window) */ - public Long getProcessedEventReportWindow() { + public long getEffectiveEventReportWindow() { if (mEventReportWindow == null) { - return null; + return getExpiryTime(); } // TODO(b/290098169): Cleanup after a few releases // Handling cases where ReportWindow is already stored as mEventTime + mEventReportWindow @@ -744,6 +745,11 @@ public class Source { return mInstallCooldownWindow; } + /** Check if install detection is enabled for the source. */ + public boolean isInstallDetectionEnabled() { + return getInstallCooldownWindow() > 0 && hasAppDestinations(); + } + /** * Is an App-install attributed to the {@link Source}. */ @@ -971,10 +977,9 @@ public class Source { @Nullable Integer maxEventLevelReports, @NonNull Flags flags) { if (maxEventLevelReports == null) { - maxEventLevelReports = - sourceType == Source.SourceType.NAVIGATION - ? PrivacyParams.NAVIGATION_SOURCE_MAX_REPORTS - : flags.getMeasurementVtcConfigurableMaxEventReportsCount(); + return sourceType == Source.SourceType.NAVIGATION + ? PrivacyParams.NAVIGATION_SOURCE_MAX_REPORTS + : flags.getMeasurementVtcConfigurableMaxEventReportsCount(); } return maxEventLevelReports; } diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/attribution/AttributionJobHandler.java b/adservices/service-core/java/com/android/adservices/service/measurement/attribution/AttributionJobHandler.java index 20dbe424e6..3d2eec8c2c 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/attribution/AttributionJobHandler.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/attribution/AttributionJobHandler.java @@ -57,6 +57,7 @@ import com.android.adservices.service.measurement.reporting.DebugKeyAccessor; import com.android.adservices.service.measurement.reporting.DebugReportApi; import com.android.adservices.service.measurement.reporting.DebugReportApi.Type; import com.android.adservices.service.measurement.reporting.EventReportWindowCalcDelegate; +import com.android.adservices.service.measurement.reporting.EventReportWindowCalcDelegate.MomentPlacement; import com.android.adservices.service.measurement.util.BaseUriExtractor; import com.android.adservices.service.measurement.util.Filter; import com.android.adservices.service.measurement.util.UnsignedLong; @@ -813,12 +814,8 @@ class AttributionJobHandler { return TriggeringStatus.DROPPED; } - if (mEventReportWindowCalcDelegate.getReportingTime( - source, trigger.getTriggerTime(), trigger.getDestinationType()) == -1 - && (source.getTriggerSpecsString() == null - || source.getTriggerSpecsString().isEmpty())) { - mDebugReportApi.scheduleTriggerDebugReport( - source, trigger, null, measurementDao, Type.TRIGGER_EVENT_REPORT_WINDOW_PASSED); + if (source.getTriggerSpecs() == null + && !isTriggerFallsWithinWindow(source, trigger, measurementDao)) { return TriggeringStatus.DROPPED; } @@ -1332,16 +1329,10 @@ class AttributionJobHandler { private boolean isWithinReportLimit( Source source, int existingReportCount, @EventSurfaceType int destinationType) { - return mEventReportWindowCalcDelegate.getMaxReportCount( - source, hasAppInstallAttributionOccurred(source, destinationType)) + return mEventReportWindowCalcDelegate.getMaxReportCount(source, destinationType) > existingReportCount; } - private static boolean hasAppInstallAttributionOccurred( - Source source, @EventSurfaceType int destinationType) { - return destinationType == EventSurfaceType.APP && source.isInstallAttributed(); - } - private static boolean isWithinInstallCooldownWindow(Source source, Trigger trigger) { return trigger.getTriggerTime() < (source.getEventTime() + source.getInstallCooldownWindow()); @@ -1754,4 +1745,27 @@ class AttributionJobHandler { attributionStatus.setAggregateDebugReportCount( attributionStatus.getAggregateDebugReportCount() + count); } + + private boolean isTriggerFallsWithinWindow( + Source source, Trigger trigger, IMeasurementDao measurementDao) + throws DatastoreException { + MomentPlacement momentPlacement = + mEventReportWindowCalcDelegate.fallsWithinWindow( + source, trigger.getTriggerTime(), trigger.getDestinationType()); + if (momentPlacement == MomentPlacement.BEFORE) { + mDebugReportApi.scheduleTriggerDebugReport( + source, + trigger, + null, + measurementDao, + Type.TRIGGER_EVENT_REPORT_WINDOW_NOT_STARTED); + return false; + } + if (momentPlacement == MomentPlacement.AFTER) { + mDebugReportApi.scheduleTriggerDebugReport( + source, trigger, null, measurementDao, Type.TRIGGER_EVENT_REPORT_WINDOW_PASSED); + return false; + } + return true; + } } diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/noising/ImpressionNoiseUtil.java b/adservices/service-core/java/com/android/adservices/service/measurement/noising/ImpressionNoiseUtil.java index 0a456919f9..c83f608b64 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/noising/ImpressionNoiseUtil.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/noising/ImpressionNoiseUtil.java @@ -29,72 +29,6 @@ import java.util.concurrent.ThreadLocalRandom; */ public final class ImpressionNoiseUtil { - /** - * This is used in the scenario where both app and web destinations are available with the - * {@link com.android.adservices.service.measurement.Source} and the source is of {@link - * com.android.adservices.service.measurement.Source.SourceType#EVENT} type and post-install - * detection is enabled (cooldown window being available). It's a special case because in this - * condition an extra early window is added only if install detection is enabled (install - * cool-down window is available) and only app conversions are relevant in that window. - * - * <p>Reading guide - The outermost array signifies different states of reporting - one of them - * will be picked at a time. The middle array holds reports in 1st and 2nd window, so there will - * be either 0 elements (no conversions in either window), 1 element (a conversion report in one - * of the windows) and 2 elements (conversions in both windows). The innermost array represents - * a single report. 3 elements in the innermost array are trigger metadata (0 - trigger1 or 1 - - * trigger2), window index (0 - window1, 1 - window2) and destination type (0 - app or 1 - web). - * - * <p>E.g. The element at index 5 is {{0, 1, 0}, {0, 1, 0}}, it means 2 conversions with trigger - * metadata as 0 in window2 (1) of app destination type(0). - */ - public static final int[][][] DUAL_DESTINATION_POST_INSTALL_FAKE_REPORT_CONFIG = - new int[][][] { - // window1 - no conversion, window 2 - no conversion - {}, - // window1 - no conversion, window 2 - 1 conversion with metadata 0 - {{0, 1, 0}}, - {{0, 1, 1}}, - // window1 - no conversion, window 2 - 1 conversion with metadata 1 - {{1, 1, 0}}, - {{1, 1, 1}}, - // window1 - no conversion, window 2 - 2 conversions with metadata 0 and 0 - {{0, 1, 0}, {0, 1, 0}}, - {{0, 1, 0}, {0, 1, 1}}, - // window1 - no conversion, window 2 - 2 conversions with metadata 0 and 1 - {{0, 1, 0}, {1, 1, 0}}, - {{0, 1, 0}, {1, 1, 1}}, - {{0, 1, 1}, {1, 1, 0}}, - // window1 - no conversion, window 2 - 2 conversions with metadata 1 and 1 - {{1, 1, 0}, {1, 1, 0}}, - {{1, 1, 0}, {1, 1, 1}}, - // window1 - 1 app conversion with metadata 0, window 2 - no conversion - {{0, 0, 0}}, - // window1 - 1 app conversion with metadata 1, window 2 - no conversion - {{1, 0, 0}}, - // window1 - 2 conversions with metadata 0 and 0, window 2 - no conversion - {{0, 0, 0}, {0, 0, 0}}, - // window1 - 2 app conversions with metadata 0 and 1, window 2 - no conversion - {{0, 0, 0}, {1, 0, 0}}, - // window1 - 2 app conversions with metadata 1 and 1, window 2 - no conversion - {{1, 0, 0}, {1, 0, 0}}, - // window1 - 1 app conversion with metadata 0, window 2 - 1 conversion with - // metadata 0 - {{0, 0, 0}, {0, 1, 0}}, - {{0, 0, 0}, {0, 1, 1}}, - // window1 - 1 app conversion with metadata 0, window 2 - 1 conversion with - // metadata 1 - {{0, 0, 0}, {1, 1, 0}}, - {{0, 0, 0}, {1, 1, 1}}, - // window1 - 1 app conversion with metadata 1, window 2 - 1 conversions with - // metadata 0 - {{1, 0, 0}, {0, 1, 0}}, - {{1, 0, 0}, {0, 1, 1}}, - // window1 - 1 app conversion with metadata 1, window 2 - 1 conversions with - // metadata 1 - {{1, 0, 0}, {1, 1, 0}}, - {{1, 0, 0}, {1, 1, 1}} - }; - private ImpressionNoiseUtil() {} /** diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/noising/SourceNoiseHandler.java b/adservices/service-core/java/com/android/adservices/service/measurement/noising/SourceNoiseHandler.java index 2c28fa51e5..8897662a95 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/noising/SourceNoiseHandler.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/noising/SourceNoiseHandler.java @@ -30,11 +30,9 @@ import com.google.common.collect.ImmutableList; import java.math.BigDecimal; import java.math.RoundingMode; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.Random; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; @@ -85,33 +83,24 @@ public class SourceNoiseHandler { List<Source.FakeReport> fakeReports; TriggerSpecs triggerSpecs = source.getTriggerSpecs(); if (triggerSpecs == null) { - if (isVtcDualDestinationModeWithPostInstallEnabled(source)) { - // Source is 'EVENT' type, both app and web destination are set and install - // exclusivity - // window is provided. Pick one of the static reporting states randomly. - fakeReports = generateVtcDualDestinationPostInstallFakeReports(source); - } else { - // There will at least be one (app or web) destination available - ImpressionNoiseParams noiseParams = getImpressionNoiseParams(source); - fakeReports = - ImpressionNoiseUtil.selectRandomStateAndGenerateReportConfigs( - noiseParams, rand) - .stream() - .map( - reportConfig -> - new Source.FakeReport( - new UnsignedLong( - Long.valueOf(reportConfig[0])), - mEventReportWindowCalcDelegate - .getReportingTimeForNoising( - source, - reportConfig[1], - isInstallDetectionEnabled( - source)), - resolveFakeReportDestinations( - source, reportConfig[2]))) - .collect(Collectors.toList()); - } + // There will at least be one (app or web) destination available + ImpressionNoiseParams noiseParams = getImpressionNoiseParams(source); + fakeReports = + ImpressionNoiseUtil.selectRandomStateAndGenerateReportConfigs( + noiseParams, rand) + .stream() + .map( + reportConfig -> + new Source.FakeReport( + new UnsignedLong( + Long.valueOf(reportConfig[0])), + mEventReportWindowCalcDelegate + .getReportingTimeForNoising( + source, + reportConfig[1]), + resolveFakeReportDestinations( + source, reportConfig[2]))) + .collect(Collectors.toList()); } else { int destinationTypeMultiplier = source.getDestinationTypeMultiplier(mFlags); List<int[]> fakeReportConfigs = @@ -144,51 +133,11 @@ public class SourceNoiseHandler { /** @return Probability of selecting random state for attribution */ public double getRandomAttributionProbability(@NonNull Source source) { - if (source.getTriggerSpecs() != null - || mFlags.getMeasurementEnableConfigurableEventReportingWindows() - || mFlags.getMeasurementEnableVtcConfigurableMaxEventReports() - || (mFlags.getMeasurementFlexLiteApiEnabled() - && (source.getMaxEventLevelReports() != null - || source.hasManualEventReportWindows()))) { - return convertToDoubleAndLimitDecimal(source.getFlipProbability(mFlags)); - } - // TODO(b/290117352): Remove Hardcoded noise values - - // Both destinations are set and install attribution is supported - if (!shouldReportCoarseDestinations(source) - && source.hasWebDestinations() - && isInstallDetectionEnabled(source)) { - return source.getSourceType() == Source.SourceType.EVENT - ? convertToDoubleAndLimitDecimal( - mFlags.getMeasurementInstallAttrDualDestinationEventNoiseProbability()) - : convertToDoubleAndLimitDecimal( - mFlags.getMeasurementInstallAttrDualDestinationNavigationNoiseProbability()); - } - - // Both destinations are set but install attribution isn't supported - if (!shouldReportCoarseDestinations(source) - && source.hasAppDestinations() - && source.hasWebDestinations()) { - return source.getSourceType() == Source.SourceType.EVENT - ? convertToDoubleAndLimitDecimal( - mFlags.getMeasurementDualDestinationEventNoiseProbability()) - : convertToDoubleAndLimitDecimal( - mFlags.getMeasurementDualDestinationNavigationNoiseProbability()); - } - - // App destination is set and install attribution is supported - if (isInstallDetectionEnabled(source)) { - return source.getSourceType() == Source.SourceType.EVENT - ? convertToDoubleAndLimitDecimal( - mFlags.getMeasurementInstallAttrEventNoiseProbability()) - : convertToDoubleAndLimitDecimal( - mFlags.getMeasurementInstallAttrNavigationNoiseProbability()); - } - - // One of the destinations is available without install attribution support - return source.getSourceType() == Source.SourceType.EVENT - ? convertToDoubleAndLimitDecimal(mFlags.getMeasurementEventNoiseProbability()) - : convertToDoubleAndLimitDecimal(mFlags.getMeasurementNavigationNoiseProbability()); + // Methods on Source and EventReportWindowCalcDelegate that calculate flip probability for + // the source rely on reporting windows and max reports that are obtained with consideration + // to install-state and its interaction with configurable report windows and configurable + // max reports. + return convertToDoubleAndLimitDecimal(source.getFlipProbability(mFlags)); } private double convertToDoubleAndLimitDecimal(double probability) { @@ -197,15 +146,6 @@ public class SourceNoiseHandler { .doubleValue(); } - private boolean isVtcDualDestinationModeWithPostInstallEnabled(Source source) { - return !shouldReportCoarseDestinations(source) - && !source.hasManualEventReportWindows() - && source.getMaxEventLevelReports() == null - && source.getSourceType() == Source.SourceType.EVENT - && source.hasWebDestinations() - && isInstallDetectionEnabled(source); - } - /** * Either both app and web destinations can be available or one of them will be available. When * both destinations are available, we double the number of states at noise generation to be @@ -216,7 +156,7 @@ public class SourceNoiseHandler { * @return app or web destination {@link Uri} */ private List<Uri> resolveFakeReportDestinations(Source source, int destinationIdentifier) { - if (shouldReportCoarseDestinations(source)) { + if (source.shouldReportCoarseDestinations(mFlags)) { ImmutableList.Builder<Uri> destinations = new ImmutableList.Builder<>(); Optional.ofNullable(source.getAppDestinations()).ifPresent(destinations::addAll); Optional.ofNullable(source.getWebDestinations()).ifPresent(destinations::addAll); @@ -234,44 +174,13 @@ public class SourceNoiseHandler { : source.getWebDestinations(); } - /** Check if install detection is enabled for the source. */ - public static boolean isInstallDetectionEnabled(@NonNull Source source) { - return source.getInstallCooldownWindow() > 0 && source.hasAppDestinations(); - } - - private boolean shouldReportCoarseDestinations(Source source) { - return mFlags.getMeasurementEnableCoarseEventReportDestinations() - && source.hasCoarseEventReportDestinations(); - } - - private List<Source.FakeReport> generateVtcDualDestinationPostInstallFakeReports( - Source source) { - int[][][] fakeReportsConfig = - ImpressionNoiseUtil.DUAL_DESTINATION_POST_INSTALL_FAKE_REPORT_CONFIG; - int randomIndex = new Random().nextInt(fakeReportsConfig.length); - int[][] reportsConfig = fakeReportsConfig[randomIndex]; - return Arrays.stream(reportsConfig) - .map( - reportConfig -> - new Source.FakeReport( - new UnsignedLong(Long.valueOf(reportConfig[0])), - mEventReportWindowCalcDelegate.getReportingTimeForNoising( - source, - /* window index */ reportConfig[1], - isInstallDetectionEnabled(source)), - resolveFakeReportDestinations(source, reportConfig[2]))) - .collect(Collectors.toList()); - } - @VisibleForTesting ImpressionNoiseParams getImpressionNoiseParams(Source source) { int destinationTypeMultiplier = source.getDestinationTypeMultiplier(mFlags); return new ImpressionNoiseParams( - mEventReportWindowCalcDelegate.getMaxReportCount( - source, isInstallDetectionEnabled(source)), + mEventReportWindowCalcDelegate.getMaxReportCount(source), source.getTriggerDataCardinality(), - mEventReportWindowCalcDelegate.getReportingWindowCountForNoising( - source, isInstallDetectionEnabled(source)), + mEventReportWindowCalcDelegate.getReportingWindowCountForNoising(source), destinationTypeMultiplier); } } diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncRedirect.java b/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncRedirect.java index 1c76ade570..6106b1a520 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncRedirect.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncRedirect.java @@ -18,46 +18,31 @@ package com.android.adservices.service.measurement.registration; import android.net.Uri; -import java.util.ArrayList; -import java.util.List; - -/** Wrapper for a list of redirect Uris and a redirect type */ public class AsyncRedirect { - private final List<Uri> mLocationRedirects; - private final List<Uri> mListRedirects; - - public AsyncRedirect() { - mLocationRedirects = new ArrayList<>(); - mListRedirects = new ArrayList<>(); + public enum RedirectBehavior { + /** Do not modify redirect path */ + AS_IS, + /** Prepend well known path prefix to path of redirect url for Location type redirects */ + LOCATION_TO_WELL_KNOWN, } - public AsyncRedirect(List<Uri> locationRedirects, List<Uri> listRedirects) { - mLocationRedirects = locationRedirects; - mListRedirects = listRedirects; - } + private final Uri mUri; + private final RedirectBehavior mRedirectBehavior; - /** The list the redirect Uris */ - public List<Uri> getRedirects() { - List<Uri> allRedirects = new ArrayList<>(mListRedirects); - allRedirects.addAll(mLocationRedirects); - return allRedirects; + public AsyncRedirect(Uri uri, RedirectBehavior redirectBehavior) { + mUri = uri; + mRedirectBehavior = redirectBehavior; } - /** Get list by redirect type */ - public List<Uri> getRedirectsByType(AsyncRegistration.RedirectType redirectType) { - if (redirectType == AsyncRegistration.RedirectType.LOCATION) { - return new ArrayList<>(mLocationRedirects); - } else { - return new ArrayList<>(mListRedirects); - } + public Uri getUri() { + return mUri; } - /** Add to the list the redirect Uris based on type */ - public void addToRedirects(AsyncRegistration.RedirectType redirectType, List<Uri> uris) { - if (redirectType == AsyncRegistration.RedirectType.LOCATION) { - mLocationRedirects.addAll(uris); - } else { - mListRedirects.addAll(uris); - } + public RedirectBehavior getRedirectBehavior() { + return mRedirectBehavior; } + + + + } diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncRedirects.java b/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncRedirects.java new file mode 100644 index 0000000000..d05cb29032 --- /dev/null +++ b/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncRedirects.java @@ -0,0 +1,126 @@ +/* + * 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.measurement.registration; + +import android.net.Uri; + +import com.android.adservices.service.Flags; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** Class for handling redirects */ +public class AsyncRedirects { + public static final String HEADER_ATTRIBUTION_REPORTING_REDIRECT_CONFIG = + "Attribution-Reporting-Redirect-Config"; + public static final String REDIRECT_302_TO_WELL_KNOWN = "redirect-302-to-well-known"; + public static final String WELL_KNOWN_PATH_SEGMENT = + ".well-known/attribution-reporting/register-redirect"; + public static final String WELL_KNOWN_QUERY_PARAM = "302_url"; + public static final String REDIRECT_LIST_HEADER_KEY = "Attribution-Reporting-Redirect"; + public static final String REDIRECT_LOCATION_HEADER_KEY = "Location"; + private final List<AsyncRedirect> mLocationRedirects; + private final List<AsyncRedirect> mListRedirects; + + public AsyncRedirects() { + mLocationRedirects = new ArrayList<>(); + mListRedirects = new ArrayList<>(); + } + + /** Return flattened list of {@link AsyncRedirect} */ + public List<AsyncRedirect> getRedirects() { + List<AsyncRedirect> allRedirects = new ArrayList<>(mListRedirects); + allRedirects.addAll(mLocationRedirects); + + return allRedirects; + } + + /** Get list of {@link AsyncRedirect} by redirect type */ + public List<AsyncRedirect> getRedirectsByType(AsyncRegistration.RedirectType redirectType) { + if (redirectType == AsyncRegistration.RedirectType.LOCATION) { + return new ArrayList<>(mLocationRedirects); + } else { + return new ArrayList<>(mListRedirects); + } + } + + /** Process redirects based on the given headers */ + public void configure( + Map<String, List<String>> headers, Flags flags, AsyncRegistration parentRegistration) { + if (!parentRegistration.shouldProcessRedirects()) { + return; + } + + Map<AsyncRegistration.RedirectType, List<Uri>> urisByType = + FetcherUtil.parseRedirects(headers); + + for (Uri locationRedirectUri : urisByType.get(AsyncRegistration.RedirectType.LOCATION)) { + if (shouldRedirect302ToWellKnown(headers, flags, parentRegistration)) { + mLocationRedirects.add( + new AsyncRedirect( + getLocationRedirectToWellKnownUri(locationRedirectUri), + AsyncRedirect.RedirectBehavior.LOCATION_TO_WELL_KNOWN)); + } else { + mLocationRedirects.add( + new AsyncRedirect( + locationRedirectUri, AsyncRedirect.RedirectBehavior.AS_IS)); + } + } + + for (Uri listRedirectUri : urisByType.get(AsyncRegistration.RedirectType.LIST)) { + mListRedirects.add( + new AsyncRedirect(listRedirectUri, AsyncRedirect.RedirectBehavior.AS_IS)); + } + } + + private static boolean shouldRedirect302ToWellKnown( + Map<String, List<String>> headers, Flags flags, AsyncRegistration parentRegistration) { + boolean isParentRegistrationRedirectsToWellKnown = + AsyncRedirect.RedirectBehavior.LOCATION_TO_WELL_KNOWN.equals( + parentRegistration.getRedirectBehavior()); + + return flags.getMeasurementEnableRedirectToWellKnownPath() + && (isParentRegistrationRedirectsToWellKnown + || isRedirect302ToWellKnownPath(headers)); + } + + /** + * Return true if the given headers indicate redirects should prepend well known prefix to the + * path. + */ + private static boolean isRedirect302ToWellKnownPath(Map<String, List<String>> headers) { + if (!headers.containsKey(HEADER_ATTRIBUTION_REPORTING_REDIRECT_CONFIG)) { + return false; + } + List<String> config = headers.get(HEADER_ATTRIBUTION_REPORTING_REDIRECT_CONFIG); + if (config == null || config.size() != 1) { + return false; + } + + return config.get(0).equalsIgnoreCase(REDIRECT_302_TO_WELL_KNOWN); + } + + private Uri getLocationRedirectToWellKnownUri(Uri redirectUri) { + return redirectUri + .buildUpon() + .encodedPath(WELL_KNOWN_PATH_SEGMENT) + .clearQuery() + .appendQueryParameter(WELL_KNOWN_QUERY_PARAM, redirectUri.toString()) + .build(); + } +} diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncRegistration.java b/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncRegistration.java index 98c95bfe0e..ade38df694 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncRegistration.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncRegistration.java @@ -51,9 +51,9 @@ public class AsyncRegistration { private final boolean mDebugKeyAllowed; private final boolean mAdIdPermission; @Nullable private String mRegistrationId; - @Nullable private final String mPlatformAdId; @Nullable private String mPostBody; + private final AsyncRedirect.RedirectBehavior mRedirectBehavior; public enum RedirectType { LOCATION, @@ -77,6 +77,7 @@ public class AsyncRegistration { mRegistrationId = builder.mRegistrationId; mPlatformAdId = builder.mPlatformAdId; mPostBody = builder.mPostBody; + mRedirectBehavior = builder.mRedirectBehavior; } @Override @@ -99,7 +100,8 @@ public class AsyncRegistration { && mType == that.mType && Objects.equals(mRegistrationId, that.mRegistrationId) && mPlatformAdId.equals(that.mPlatformAdId) - && Objects.equals(mPostBody, that.mPostBody); + && Objects.equals(mPostBody, that.mPostBody) + && Objects.equals(mRedirectBehavior, that.mRedirectBehavior); } @Override @@ -120,7 +122,8 @@ public class AsyncRegistration { mAdIdPermission, mRegistrationId, mPlatformAdId, - mPostBody); + mPostBody, + mRedirectBehavior); } /** Unique identifier for the {@link AsyncRegistration}. */ @@ -214,6 +217,11 @@ public class AsyncRegistration { return mPostBody; } + /** Return the configuration for redirect behavior. */ + public AsyncRedirect.RedirectBehavior getRedirectBehavior() { + return mRedirectBehavior; + } + /** Increments the retry count of the current record. */ public void incrementRetryCount() { ++mRetryCount; @@ -260,9 +268,9 @@ public class AsyncRegistration { private boolean mDebugKeyAllowed; private boolean mAdIdPermission; @Nullable private String mRegistrationId; - @Nullable private String mPlatformAdId; @Nullable private String mPostBody; + private AsyncRedirect.RedirectBehavior mRedirectBehavior; /** See {@link AsyncRegistration#getId()}. */ @NonNull @@ -385,6 +393,12 @@ public class AsyncRegistration { return this; } + /** See {@link AsyncRegistration#getRedirectBehavior()}. */ + public Builder setRedirectBehavior(AsyncRedirect.RedirectBehavior redirectBehavior) { + mRedirectBehavior = redirectBehavior; + return this; + } + /** Build the {@link AsyncRegistration}. */ @NonNull public AsyncRegistration build() { diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncRegistrationQueueRunner.java b/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncRegistrationQueueRunner.java index 207f8b3f0f..41f66aea2e 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncRegistrationQueueRunner.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncRegistrationQueueRunner.java @@ -210,10 +210,11 @@ public class AsyncRegistrationQueueRunner { AsyncRegistration asyncRegistration, Set<Uri> failedOrigins) { AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); asyncFetchStatus.setRetryCount(Long.valueOf(asyncRegistration.getRetryCount()).intValue()); - AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncRedirects asyncRedirects = new AsyncRedirects(); long startTime = asyncRegistration.getRequestTime(); Optional<Source> resultSource = - mAsyncSourceFetcher.fetchSource(asyncRegistration, asyncFetchStatus, asyncRedirect); + mAsyncSourceFetcher.fetchSource( + asyncRegistration, asyncFetchStatus, asyncRedirects); long endTime = System.currentTimeMillis(); asyncFetchStatus.setRegistrationDelay(endTime - startTime); @@ -225,7 +226,7 @@ public class AsyncRegistrationQueueRunner { storeSource(resultSource.get(), asyncRegistration, dao); } handleSuccess( - asyncRegistration, asyncFetchStatus, asyncRedirect, dao); + asyncRegistration, asyncFetchStatus, asyncRedirects, dao); } else { handleFailure( asyncRegistration, asyncFetchStatus, failedOrigins, dao); @@ -273,10 +274,11 @@ public class AsyncRegistrationQueueRunner { AsyncRegistration asyncRegistration, Set<Uri> failedOrigins) { AsyncFetchStatus asyncFetchStatus = new AsyncFetchStatus(); asyncFetchStatus.setRetryCount(Long.valueOf(asyncRegistration.getRetryCount()).intValue()); - AsyncRedirect asyncRedirect = new AsyncRedirect(); + AsyncRedirects asyncRedirects = new AsyncRedirects(); long startTime = asyncRegistration.getRequestTime(); - Optional<Trigger> resultTrigger = mAsyncTriggerFetcher.fetchTrigger( - asyncRegistration, asyncFetchStatus, asyncRedirect); + Optional<Trigger> resultTrigger = + mAsyncTriggerFetcher.fetchTrigger( + asyncRegistration, asyncFetchStatus, asyncRedirects); long endTime = System.currentTimeMillis(); asyncFetchStatus.setRegistrationDelay(endTime - startTime); @@ -288,7 +290,7 @@ public class AsyncRegistrationQueueRunner { storeTrigger(resultTrigger.get(), dao); } handleSuccess( - asyncRegistration, asyncFetchStatus, asyncRedirect, dao); + asyncRegistration, asyncFetchStatus, asyncRedirects, dao); } else { handleFailure( asyncRegistration, asyncFetchStatus, failedOrigins, dao); @@ -583,11 +585,11 @@ public class AsyncRegistrationQueueRunner { < FlagsFactory.getFlags().getMeasurementMaxTriggersPerDestination(); } - private static AsyncRegistration createAsyncRegistrationFromRedirect( - AsyncRegistration asyncRegistration, Uri redirectUri) { + private AsyncRegistration createAsyncRegistrationFromRedirect( + AsyncRegistration asyncRegistration, AsyncRedirect asyncRedirect) { return new AsyncRegistration.Builder() .setId(UUID.randomUUID().toString()) - .setRegistrationUri(redirectUri) + .setRegistrationUri(asyncRedirect.getUri()) .setWebDestination(asyncRegistration.getWebDestination()) .setOsDestination(asyncRegistration.getOsDestination()) .setRegistrant(asyncRegistration.getRegistrant()) @@ -600,6 +602,7 @@ public class AsyncRegistrationQueueRunner { .setDebugKeyAllowed(asyncRegistration.getDebugKeyAllowed()) .setAdIdPermission(asyncRegistration.hasAdIdPermission()) .setRegistrationId(asyncRegistration.getRegistrationId()) + .setRedirectBehavior(asyncRedirect.getRedirectBehavior()) .build(); } @@ -690,14 +693,14 @@ public class AsyncRegistrationQueueRunner { private void handleSuccess( AsyncRegistration asyncRegistration, AsyncFetchStatus asyncFetchStatus, - AsyncRedirect asyncRedirect, + AsyncRedirects asyncRedirects, IMeasurementDao dao) throws DatastoreException { // deleteAsyncRegistration will throw an exception & rollback the transaction if the record // is already deleted. This can happen if both fallback & regular job are running at the // same time or if deletion job deletes the records. dao.deleteAsyncRegistration(asyncRegistration.getId()); - if (asyncRedirect.getRedirects().isEmpty()) { + if (asyncRedirects.getRedirects().isEmpty()) { return; } int maxRedirects = FlagsFactory.getFlags().getMeasurementMaxRegistrationRedirects(); @@ -706,16 +709,17 @@ public class AsyncRegistrationQueueRunner { asyncRegistration.getRegistrationId(), DataType.REGISTRATION_REDIRECT_COUNT); int currentCount = keyValueData.getRegistrationRedirectCount(); - if (currentCount == maxRedirects) { + if (currentCount >= maxRedirects) { asyncFetchStatus.setRedirectError(true); return; } - for (Uri uri : asyncRedirect.getRedirects()) { + + for (AsyncRedirect asyncRedirect : asyncRedirects.getRedirects()) { if (currentCount >= maxRedirects) { break; } dao.insertAsyncRegistration( - createAsyncRegistrationFromRedirect(asyncRegistration, uri)); + createAsyncRegistrationFromRedirect(asyncRegistration, asyncRedirect)); currentCount++; } keyValueData.setRegistrationRedirectCount(currentCount); diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncSourceFetcher.java b/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncSourceFetcher.java index c1f3a30c77..26157a01b1 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncSourceFetcher.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncSourceFetcher.java @@ -492,8 +492,9 @@ public class AsyncSourceFetcher { Source.SourceType sourceType, int maxEventLevelReports, Source.TriggerDataMatching triggerDataMatching) { - List<Pair<Long, Long>> parsedEventReportWindows = Source.getOrDefaultEventReportWindows( - eventReportWindows, sourceType, expiry, mFlags); + List<Pair<Long, Long>> parsedEventReportWindows = + Source.getOrDefaultEventReportWindowsForFlex( + eventReportWindows, sourceType, expiry, mFlags); long defaultStart = parsedEventReportWindows.get(0).first; List<Long> defaultEnds = parsedEventReportWindows.stream().map((x) -> x.second).collect(Collectors.toList()); @@ -799,7 +800,7 @@ public class AsyncSourceFetcher { public Optional<Source> fetchSource( AsyncRegistration asyncRegistration, AsyncFetchStatus asyncFetchStatus, - AsyncRedirect asyncRedirect) { + AsyncRedirects asyncRedirects) { HttpURLConnection urlConnection = null; Map<String, List<String>> headers; if (!asyncRegistration.getRegistrationUri().getScheme().equalsIgnoreCase("https")) { @@ -852,9 +853,7 @@ public class AsyncSourceFetcher { } } - if (asyncRegistration.shouldProcessRedirects()) { - FetcherUtil.parseRedirects(headers).forEach(asyncRedirect::addToRedirects); - } + asyncRedirects.configure(headers, mFlags, asyncRegistration); if (!isSourceHeaderPresent(headers)) { asyncFetchStatus.setEntityStatus(AsyncFetchStatus.EntityStatus.HEADER_MISSING); @@ -864,7 +863,9 @@ public class AsyncSourceFetcher { Optional<String> enrollmentId = mFlags.isDisableMeasurementEnrollmentCheck() - ? Optional.of(Enrollment.FAKE_ENROLLMENT) + ? WebAddresses.topPrivateDomainAndScheme( + asyncRegistration.getRegistrationUri()) + .map(Uri::toString) : Enrollment.getValidEnrollmentId( asyncRegistration.getRegistrationUri(), asyncRegistration.getRegistrant().getAuthority(), diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncTriggerFetcher.java b/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncTriggerFetcher.java index 9fd56f6c6f..d7d5e9e95d 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncTriggerFetcher.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/registration/AsyncTriggerFetcher.java @@ -308,7 +308,7 @@ public class AsyncTriggerFetcher { public Optional<Trigger> fetchTrigger( AsyncRegistration asyncRegistration, AsyncFetchStatus asyncFetchStatus, - AsyncRedirect asyncRedirect) { + AsyncRedirects asyncRedirects) { HttpURLConnection urlConnection = null; Map<String, List<String>> headers; if (!asyncRegistration.getRegistrationUri().getScheme().equalsIgnoreCase("https")) { @@ -347,9 +347,7 @@ public class AsyncTriggerFetcher { } } - if (asyncRegistration.shouldProcessRedirects()) { - FetcherUtil.parseRedirects(headers).forEach(asyncRedirect::addToRedirects); - } + asyncRedirects.configure(headers, mFlags, asyncRegistration); if (!isTriggerHeaderPresent(headers)) { asyncFetchStatus.setEntityStatus(AsyncFetchStatus.EntityStatus.HEADER_MISSING); @@ -359,7 +357,9 @@ public class AsyncTriggerFetcher { Optional<String> enrollmentId = mFlags.isDisableMeasurementEnrollmentCheck() - ? Optional.of(Enrollment.FAKE_ENROLLMENT) + ? WebAddresses.topPrivateDomainAndScheme( + asyncRegistration.getRegistrationUri()) + .map(Uri::toString) : Enrollment.getValidEnrollmentId( asyncRegistration.getRegistrationUri(), asyncRegistration.getRegistrant().getAuthority(), @@ -627,8 +627,7 @@ public class AsyncTriggerFetcher { JSONArray aggregateDeduplicationKeys) throws JSONException { JSONArray validAggregateDeduplicationKeys = new JSONArray(); if (aggregateDeduplicationKeys.length() - > FlagsFactory.getFlags() - .getMeasurementMaxAggregateDeduplicationKeysPerRegistration()) { + > mFlags.getMeasurementMaxAggregateDeduplicationKeysPerRegistration()) { LoggerFactory.getMeasurementLogger() .d( "Aggregate deduplication keys have more keys than permitted. %s", diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/registration/EnqueueAsyncRegistration.java b/adservices/service-core/java/com/android/adservices/service/measurement/registration/EnqueueAsyncRegistration.java index 0c8a801b0f..f6a7bdfb55 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/registration/EnqueueAsyncRegistration.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/registration/EnqueueAsyncRegistration.java @@ -255,6 +255,7 @@ public class EnqueueAsyncRegistration { .setPlatformAdId(platformAdIdValue) .setPostBody(postBody) .setRegistrationId(registrationId) + .setRedirectBehavior(AsyncRedirect.RedirectBehavior.AS_IS) .build(); dao.insertAsyncRegistration(asyncRegistration); diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/registration/FetcherUtil.java b/adservices/service-core/java/com/android/adservices/service/measurement/registration/FetcherUtil.java index d5d268e7c1..50c64886d2 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/registration/FetcherUtil.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/registration/FetcherUtil.java @@ -48,15 +48,12 @@ import java.util.regex.Pattern; * @hide */ class FetcherUtil { - static final String REDIRECT_LIST_HEADER_KEY = "Attribution-Reporting-Redirect"; - static final String REDIRECT_LOCATION_HEADER_KEY = "Location"; static final Pattern HEX_PATTERN = Pattern.compile("\\p{XDigit}+"); /** * Determine all redirects. * - * <p>Generates a list of: (url, allows_regular_redirects) tuples. Returns true if all steps - * succeed. Returns false if there are any failures. + * <p>Generates a map of: (redirectType, List<Uri>) */ static Map<AsyncRegistration.RedirectType, List<Uri>> parseRedirects( @NonNull Map<String, List<String>> headers) { @@ -304,7 +301,7 @@ class FetcherUtil { private static List<Uri> parseListRedirects(Map<String, List<String>> headers) { List<Uri> redirects = new ArrayList<>(); - List<String> field = headers.get(REDIRECT_LIST_HEADER_KEY); + List<String> field = headers.get(AsyncRedirects.REDIRECT_LIST_HEADER_KEY); int maxRedirects = FlagsFactory.getFlags().getMeasurementMaxRegistrationRedirects(); if (field != null) { for (int i = 0; i < Math.min(field.size(), maxRedirects); i++) { @@ -316,7 +313,7 @@ class FetcherUtil { private static List<Uri> parseLocationRedirects(Map<String, List<String>> headers) { List<Uri> redirects = new ArrayList<>(); - List<String> field = headers.get(REDIRECT_LOCATION_HEADER_KEY); + List<String> field = headers.get(AsyncRedirects.REDIRECT_LOCATION_HEADER_KEY); if (field != null && !field.isEmpty()) { redirects.add(Uri.parse(field.get(0))); if (field.size() > 1) { diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/reporting/AggregateReportBody.java b/adservices/service-core/java/com/android/adservices/service/measurement/reporting/AggregateReportBody.java index a4d1971070..0498d4a85c 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/reporting/AggregateReportBody.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/reporting/AggregateReportBody.java @@ -50,7 +50,8 @@ public class AggregateReportBody { private static final String API_NAME = "attribution-reporting"; - private interface PayloadBodyKeys { + @VisibleForTesting + interface PayloadBodyKeys { String SHARED_INFO = "shared_info"; String AGGREGATION_SERVICE_PAYLOADS = "aggregation_service_payloads"; String SOURCE_DEBUG_KEY = "source_debug_key"; @@ -64,7 +65,8 @@ public class AggregateReportBody { String DEBUG_CLEARTEXT_PAYLOAD = "debug_cleartext_payload"; } - private interface SharedInfoKeys { + @VisibleForTesting + interface SharedInfoKeys { String API_NAME = "api"; String ATTRIBUTION_DESTINATION = "attribution_destination"; String REPORT_ID = "report_id"; diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/reporting/AggregateReportingJobHandler.java b/adservices/service-core/java/com/android/adservices/service/measurement/reporting/AggregateReportingJobHandler.java index 4367ee1b8b..849ec8164b 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/reporting/AggregateReportingJobHandler.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/reporting/AggregateReportingJobHandler.java @@ -16,7 +16,10 @@ package com.android.adservices.service.measurement.reporting; -import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_CODE_UNSPECIFIED; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_ENCRYPTION_ERROR; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_NETWORK_ERROR; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_PARSING_ERROR; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_UNKNOWN_ERROR; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_MESUREMENT_REPORTS_UPLOADED; @@ -289,7 +292,7 @@ public class AggregateReportingJobHandler { // TODO(b/298330312): Change to defined error codes ErrorLogUtil.e( e, - AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_CODE_UNSPECIFIED, + AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_NETWORK_ERROR, AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); return AdServicesStatusUtils.STATUS_IO_ERROR; } catch (JSONException e) { @@ -299,7 +302,7 @@ public class AggregateReportingJobHandler { // TODO(b/298330312): Change to defined error codes ErrorLogUtil.e( e, - AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_CODE_UNSPECIFIED, + AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_PARSING_ERROR, AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); if (mFlags.getMeasurementEnableReportDeletionOnUnrecoverableException()) { // Unrecoverable state - delete the report. @@ -324,7 +327,7 @@ public class AggregateReportingJobHandler { // TODO(b/298330312): Change to defined error codes ErrorLogUtil.e( e, - AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_CODE_UNSPECIFIED, + AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_ENCRYPTION_ERROR, AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); if (mFlags.getMeasurementEnableReportingJobsThrowCryptoException() && ThreadLocalRandom.current().nextFloat() @@ -338,7 +341,7 @@ public class AggregateReportingJobHandler { // TODO(b/298330312): Change to defined error codes ErrorLogUtil.e( e, - AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_CODE_UNSPECIFIED, + AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_UNKNOWN_ERROR, AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); if (mFlags.getMeasurementEnableReportingJobsThrowUnaccountedException() && ThreadLocalRandom.current().nextFloat() @@ -371,8 +374,7 @@ public class AggregateReportingJobHandler { .setTriggerDebugKey(aggregateReport.getTriggerDebugKey()) .setAggregationCoordinatorOrigin(aggregateReport.getAggregationCoordinatorOrigin()) .setDebugMode( - mIsDebugInstance - && aggregateReport.getSourceDebugKey() != null + aggregateReport.getSourceDebugKey() != null && aggregateReport.getTriggerDebugKey() != null ? "enabled" : null) diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/reporting/DebugReportApi.java b/adservices/service-core/java/com/android/adservices/service/measurement/reporting/DebugReportApi.java index c4a6d7201e..778570c249 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/reporting/DebugReportApi.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/reporting/DebugReportApi.java @@ -80,6 +80,7 @@ public class DebugReportApi { String TRIGGER_UNKNOWN_ERROR = "trigger-unknown-error"; String TRIGGER_AGGREGATE_STORAGE_LIMIT = "trigger-aggregate-storage-limit"; String TRIGGER_AGGREGATE_EXCESSIVE_REPORTS = "trigger-aggregate-excessive-reports"; + String TRIGGER_EVENT_REPORT_WINDOW_NOT_STARTED = "trigger-event-report-window-not-started"; } /** Defines different verbose debug report body parameters. */ diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/reporting/DebugReportingJobHandler.java b/adservices/service-core/java/com/android/adservices/service/measurement/reporting/DebugReportingJobHandler.java index 0650f67e90..4391b37254 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/reporting/DebugReportingJobHandler.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/reporting/DebugReportingJobHandler.java @@ -16,7 +16,9 @@ package com.android.adservices.service.measurement.reporting; -import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_CODE_UNSPECIFIED; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_NETWORK_ERROR; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_PARSING_ERROR; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_UNKNOWN_ERROR; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_MESUREMENT_REPORTS_UPLOADED; @@ -184,7 +186,7 @@ public class DebugReportingJobHandler { .d(e, "Network error occurred when attempting to deliver debug report."); ErrorLogUtil.e( e, - AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_CODE_UNSPECIFIED, + AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_NETWORK_ERROR, AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); reportingStatus.setFailureStatus(ReportingStatus.FailureStatus.NETWORK); // TODO(b/298330312): Change to defined error codes @@ -195,7 +197,7 @@ public class DebugReportingJobHandler { // TODO(b/298330312): Change to defined error codes ErrorLogUtil.e( e, - AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_CODE_UNSPECIFIED, + AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_PARSING_ERROR, AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); reportingStatus.setFailureStatus(ReportingStatus.FailureStatus.SERIALIZATION_ERROR); if (mFlags.getMeasurementEnableReportDeletionOnUnrecoverableException()) { @@ -216,7 +218,7 @@ public class DebugReportingJobHandler { // TODO(b/298330312): Change to defined error codes ErrorLogUtil.e( e, - AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_CODE_UNSPECIFIED, + AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_UNKNOWN_ERROR, AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); reportingStatus.setFailureStatus(ReportingStatus.FailureStatus.UNKNOWN); if (mFlags.getMeasurementEnableReportingJobsThrowUnaccountedException() diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/reporting/EventReportWindowCalcDelegate.java b/adservices/service-core/java/com/android/adservices/service/measurement/reporting/EventReportWindowCalcDelegate.java index 9d7564c43c..93f7913854 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/reporting/EventReportWindowCalcDelegate.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/reporting/EventReportWindowCalcDelegate.java @@ -54,36 +54,42 @@ public class EventReportWindowCalcDelegate { } /** - * Max reports count based on conversion destination type and installation state. + * Max reports count given the Source object. * - * @param isInstallCase is app installed + * @param source the Source object * @return maximum number of reports allowed */ - public int getMaxReportCount(@NonNull Source source, boolean isInstallCase) { + public int getMaxReportCount(Source source) { + return getMaxReportCount(source, source.isInstallDetectionEnabled()); + } + + /** + * Max reports count based on conversion destination type. + * + * @param source the Source object + * @param destinationType destination type + * @return maximum number of reports allowed + */ + public int getMaxReportCount(@NonNull Source source, @EventSurfaceType int destinationType) { + return getMaxReportCount(source, isInstallCase(source, destinationType)); + } + + private int getMaxReportCount(@NonNull Source source, boolean isInstallCase) { // TODO(b/290101531): Cleanup flags if (mFlags.getMeasurementFlexLiteApiEnabled() && source.getMaxEventLevelReports() != null) { return source.getMaxEventLevelReports(); } - if (source.getSourceType() == Source.SourceType.EVENT - && mFlags.getMeasurementEnableVtcConfigurableMaxEventReports()) { - // Max VTC event reports are configurable - int configuredMaxReports = mFlags.getMeasurementVtcConfigurableMaxEventReportsCount(); + + if (source.getSourceType() == Source.SourceType.EVENT) { // Additional report essentially for first open + 1 post install conversion. If there // is already more than 1 report allowed, no need to have that additional report. - if (isInstallCase && configuredMaxReports == PrivacyParams.EVENT_SOURCE_MAX_REPORTS) { + if (isInstallCase && !source.hasWebDestinations() && isDefaultConfiguredVtc()) { return PrivacyParams.INSTALL_ATTR_EVENT_SOURCE_MAX_REPORTS; } - return configuredMaxReports; + return mFlags.getMeasurementVtcConfigurableMaxEventReportsCount(); } - if (isInstallCase) { - return source.getSourceType() == Source.SourceType.EVENT - ? PrivacyParams.INSTALL_ATTR_EVENT_SOURCE_MAX_REPORTS - : PrivacyParams.INSTALL_ATTR_NAVIGATION_SOURCE_MAX_REPORTS; - } - return source.getSourceType() == Source.SourceType.EVENT - ? PrivacyParams.EVENT_SOURCE_MAX_REPORTS - : PrivacyParams.NAVIGATION_SOURCE_MAX_REPORTS; + return PrivacyParams.NAVIGATION_SOURCE_MAX_REPORTS; } /** @@ -100,18 +106,14 @@ public class EventReportWindowCalcDelegate { // Cases where source could have both web and app destinations, there if the trigger // destination is an app, and it was installed, then installState should be considered true. - boolean isAppInstalled = isAppInstalled(source, destinationType); - List<Pair<Long, Long>> earlyReportingWindows = - getEarlyReportingWindows(source, isAppInstalled); - for (Pair<Long, Long> window : earlyReportingWindows) { + List<Pair<Long, Long>> reportingWindows = + getEffectiveReportingWindows(source, isInstallCase(source, destinationType)); + for (Pair<Long, Long> window : reportingWindows) { if (isWithinWindow(triggerTime, window)) { return window.second + mFlags.getMeasurementMinEventReportDelayMillis(); } } - Pair<Long, Long> finalWindow = getFinalReportingWindow(source, earlyReportingWindows); - if (isWithinWindow(triggerTime, finalWindow)) { - return finalWindow.second + mFlags.getMeasurementMinEventReportDelayMillis(); - } + return -1; } @@ -120,45 +122,60 @@ public class EventReportWindowCalcDelegate { } /** + * Enum shows trigger time and source window time relationship. It is used to generate different + * verbose debug reports. + */ + public enum MomentPlacement { + BEFORE, + AFTER, + WITHIN; + } + + /** + * @param source source for which the window is calculated + * @param triggerTime time for the trigger + * @param destinationType trigger destination type + * @return how trigger time falls in source windows + */ + public MomentPlacement fallsWithinWindow( + @NonNull Source source, long triggerTime, @EventSurfaceType int destinationType) { + List<Pair<Long, Long>> reportingWindows = + getEffectiveReportingWindows(source, isInstallCase(source, destinationType)); + if (triggerTime < reportingWindows.get(0).first) { + return MomentPlacement.BEFORE; + } + if (triggerTime >= reportingWindows.get(reportingWindows.size() - 1).second) { + return MomentPlacement.AFTER; + } + return MomentPlacement.WITHIN; + } + + /** * Return reporting time by index for noising based on the index * * @param windowIndex index of the reporting window for which * @return reporting time in milliseconds */ public long getReportingTimeForNoising( - @NonNull Source source, int windowIndex, boolean isInstallCase) { - List<Pair<Long, Long>> earlyWindows = getEarlyReportingWindows(source, isInstallCase); - Pair<Long, Long> finalWindow = getFinalReportingWindow(source, earlyWindows); - return windowIndex < earlyWindows.size() - ? earlyWindows.get(windowIndex).second + @NonNull Source source, int windowIndex) { + List<Pair<Long, Long>> reportingWindows = getEffectiveReportingWindows( + source, source.isInstallDetectionEnabled()); + Pair<Long, Long> finalWindow = reportingWindows.get(reportingWindows.size() - 1); + // TODO: (b/288646239) remove this check, confirming noising indexing accuracy. + return windowIndex < reportingWindows.size() + ? reportingWindows.get(windowIndex).second + mFlags.getMeasurementMinEventReportDelayMillis() : finalWindow.second + mFlags.getMeasurementMinEventReportDelayMillis(); } - private Pair<Long, Long> getFinalReportingWindow( - Source source, List<Pair<Long, Long>> earlyWindows) { - if (mFlags.getMeasurementFlexLiteApiEnabled() && source.hasManualEventReportWindows()) { - List<Pair<Long, Long>> windowList = source.parsedProcessedEventReportWindows(); - return windowList.get(windowList.size() - 1); - } - long secondToLastWindowEnd = - !earlyWindows.isEmpty() ? earlyWindows.get(earlyWindows.size() - 1).second : 0; - if (source.getProcessedEventReportWindow() != null) { - return new Pair<>(secondToLastWindowEnd, source.getProcessedEventReportWindow()); - } - return new Pair<>(secondToLastWindowEnd, source.getExpiryTime()); - } - /** - * Returns effective, i.e. the ones that occur before {@link - * Source#getProcessedEventReportWindow()}, event reporting windows count for noising cases. + * Returns effective, that is, the ones that occur before {@link + * Source#getEffectiveEventReportWindow()}, event reporting windows count for noising cases. * * @param source source for which the count is requested - * @param isInstallCase true of cool down window was specified */ - public int getReportingWindowCountForNoising(@NonNull Source source, boolean isInstallCase) { - // Early Count + lastWindow - return getEarlyReportingWindows(source, isInstallCase).size() + 1; + public int getReportingWindowCountForNoising(@NonNull Source source) { + return getEffectiveReportingWindows(source, source.isInstallDetectionEnabled()).size(); } /** @@ -213,47 +230,53 @@ public class EventReportWindowCalcDelegate { return -1L; } - private boolean isAppInstalled(Source source, int destinationType) { + private static boolean isInstallCase(Source source, @EventSurfaceType int destinationType) { return destinationType == EventSurfaceType.APP && source.isInstallAttributed(); } /** * If the flag is enabled and the specified report windows are valid, picks from flag controlled - * configurable early reporting windows. Otherwise, falls back to the statical {@link - * com.android.adservices.service.measurement.PrivacyParams} values. It curtails the windows - * that occur after {@link Source#getProcessedEventReportWindow()} because they would - * effectively be unusable. + * configurable early reporting windows. Otherwise, falls back to the values provided in + * {@code getDefaultEarlyReportingWindowEnds}, which can have install-related custom behaviour. + * It curtails the windows that occur after {@link Source#getEffectiveEventReportWindow()} + * because they would effectively be unusable. */ - private List<Pair<Long, Long>> getEarlyReportingWindows(Source source, boolean installState) { + private List<Pair<Long, Long>> getEffectiveReportingWindows(Source source, + boolean installState) { // TODO(b/290221611) Remove early reporting windows from code, only use them for flags. if (mFlags.getMeasurementFlexLiteApiEnabled() && source.hasManualEventReportWindows()) { - List<Pair<Long, Long>> windows = source.parsedProcessedEventReportWindows(); - // Select early windows only i.e. skip the last element - return windows.subList(0, windows.size() - 1); + return source.parsedProcessedEventReportWindows(); } - List<Long> earlyWindows; - List<Long> defaultEarlyWindows = - getDefaultEarlyReportingWindows(source.getSourceType(), installState); - earlyWindows = - getConfiguredOrDefaultEarlyReportingWindows( - source.getSourceType(), defaultEarlyWindows, true); + List<Long> defaultEarlyWindowEnds = + getDefaultEarlyReportingWindowEnds( + source.getSourceType(), + installState && !source.hasWebDestinations()); + List<Long> earlyWindowEnds = + getConfiguredOrDefaultEarlyReportingWindowEnds( + source.getSourceType(), defaultEarlyWindowEnds); // Add source event time to windows - earlyWindows = - earlyWindows.stream() + earlyWindowEnds = + earlyWindowEnds.stream() .map((x) -> source.getEventTime() + x) .collect(Collectors.toList()); List<Pair<Long, Long>> windowList = new ArrayList<>(); - long windowStart = 0; + long windowStart = 0L; Pair<Long, Long> finalWindow = - getFinalReportingWindow(source, createStartEndWindow(earlyWindows)); - for (long windowEnd : earlyWindows) { - if (finalWindow.second <= windowEnd) { - continue; + getFinalReportingWindow(source, earlyWindowEnds); + + for (long windowEnd : earlyWindowEnds) { + // Start time of `finalWindow` is either 0 or one of `earlyWindowEnds` times; stop + // iterating if we see it, and add `finalWindow`. + if (windowStart == finalWindow.first) { + break; } - windowList.add(new Pair<>(windowStart, windowEnd)); + windowList.add(Pair.create(windowStart, windowEnd)); windowStart = windowEnd; } + + windowList.add(finalWindow); + return ImmutableList.copyOf(windowList); } @@ -261,13 +284,13 @@ public class EventReportWindowCalcDelegate { * Returns the default early reporting windows * * @param sourceType Source's Type - * @param installState Install State of the source + * @param installAttributionEnabled whether windows for install attribution should be provided * @return a list of windows */ - public static List<Long> getDefaultEarlyReportingWindows( - Source.SourceType sourceType, boolean installState) { + public static List<Long> getDefaultEarlyReportingWindowEnds( + Source.SourceType sourceType, boolean installAttributionEnabled) { long[] earlyWindows; - if (installState) { + if (installAttributionEnabled) { earlyWindows = sourceType == Source.SourceType.EVENT ? INSTALL_ATTR_EVENT_EARLY_REPORTING_WINDOW_MILLISECONDS @@ -281,53 +304,34 @@ public class EventReportWindowCalcDelegate { return asList(earlyWindows); } - private List<Pair<Long, Long>> createStartEndWindow(List<Long> windowEnds) { - List<Pair<Long, Long>> windows = new ArrayList<>(); - long start = 0; - for (Long end : windowEnds) { - windows.add(new Pair<>(start, end)); - start = end; - } - return windows; - } - /** * Returns default or configured (via flag) early reporting windows for the SourceType * * @param sourceType Source's Type * @param defaultEarlyWindows default value for early windows - * @param checkEnableFlag set true if configurable window flag should be checked * @return list of windows */ - public List<Long> getConfiguredOrDefaultEarlyReportingWindows( - Source.SourceType sourceType, List<Long> defaultEarlyWindows, boolean checkEnableFlag) { - // TODO(b/290101531): Cleanup flags - if (checkEnableFlag && !mFlags.getMeasurementEnableConfigurableEventReportingWindows()) { - return defaultEarlyWindows; + public List<Long> getConfiguredOrDefaultEarlyReportingWindowEnds( + Source.SourceType sourceType, List<Long> defaultEarlyWindowEnds) { + // `defaultEarlyWindowEnds` may contain custom install-related logic, which we only apply if + // the configurable report windows (and max reports) are in their default state. Without + // this check, we may construct default-value report windows without the custom + // install-related logic applied. + if ((sourceType == Source.SourceType.EVENT && isDefaultConfiguredVtc()) + || (sourceType == Source.SourceType.NAVIGATION && isDefaultConfiguredCtc())) { + return defaultEarlyWindowEnds; } String earlyReportingWindowsString = pickEarlyReportingWindowsConfig(mFlags, sourceType); - if (earlyReportingWindowsString == null) { - LoggerFactory.getMeasurementLogger() - .d("Invalid configurable early reporting windows; null"); - return defaultEarlyWindows; - } - if (earlyReportingWindowsString.isEmpty()) { // No early reporting windows specified. It needs to be handled separately because - // splitting an empty string results into an array containing a single element, - // i.e. "". We want to handle it as an array having no element. - - if (Source.SourceType.EVENT.equals(sourceType)) { - // We need to add a reporting window at 2d for post-install case. Non-install case - // has no early reporting window by default. - return defaultEarlyWindows; - } + // splitting an empty string results in an array containing a single empty string. We + // want to handle it as an empty array. return Collections.emptyList(); } - ImmutableList.Builder<Long> earlyWindows = new ImmutableList.Builder<>(); + ImmutableList.Builder<Long> earlyWindowEnds = new ImmutableList.Builder<>(); String[] split = earlyReportingWindowsString.split(EARLY_REPORTING_WINDOWS_CONFIG_DELIMITER); if (split.length > MAX_CONFIGURABLE_EVENT_REPORT_EARLY_REPORTING_WINDOWS) { @@ -335,22 +339,54 @@ public class EventReportWindowCalcDelegate { .d( "Invalid configurable early reporting window; more than allowed size: " + MAX_CONFIGURABLE_EVENT_REPORT_EARLY_REPORTING_WINDOWS); - return defaultEarlyWindows; + return defaultEarlyWindowEnds; } - for (String window : split) { + for (String windowEnd : split) { try { - earlyWindows.add(TimeUnit.SECONDS.toMillis(Long.parseLong(window))); + earlyWindowEnds.add(TimeUnit.SECONDS.toMillis(Long.parseLong(windowEnd))); } catch (NumberFormatException e) { LoggerFactory.getMeasurementLogger() .d(e, "Configurable early reporting window parsing failed."); - return defaultEarlyWindows; + return defaultEarlyWindowEnds; + } + } + return earlyWindowEnds.build(); + } + + private Pair<Long, Long> getFinalReportingWindow( + Source source, List<Long> earlyWindowEnds) { + // The latest end-time we can associate with a report for this source + long effectiveExpiry = Math.min( + source.getEffectiveEventReportWindow(), source.getExpiryTime()); + // Find the latest end-time that can start a window ending at effectiveExpiry + for (int i = earlyWindowEnds.size() - 1; i >= 0; i--) { + long windowEnd = earlyWindowEnds.get(i); + if (windowEnd < effectiveExpiry) { + return Pair.create(windowEnd, effectiveExpiry); } } - return earlyWindows.build(); + return Pair.create(0L, effectiveExpiry); + } + + /** Indicates whether VTC report windows and max reports are default configured, which can + * affect custom install-related attribution. + */ + public boolean isDefaultConfiguredVtc() { + return mFlags.getMeasurementEventReportsVtcEarlyReportingWindows().isEmpty() + && mFlags.getMeasurementVtcConfigurableMaxEventReportsCount() == 1; + } + + /** Indicates whether CTC report windows are default configured, which can affect custom + * install-related attribution. + */ + private boolean isDefaultConfiguredCtc() { + return mFlags.getMeasurementEventReportsCtcEarlyReportingWindows().equals( + Flags.MEASUREMENT_EVENT_REPORTS_CTC_EARLY_REPORTING_WINDOWS); } - private String pickEarlyReportingWindowsConfig(Flags flags, Source.SourceType sourceType) { + private static String pickEarlyReportingWindowsConfig(Flags flags, + Source.SourceType sourceType) { return sourceType == Source.SourceType.EVENT ? flags.getMeasurementEventReportsVtcEarlyReportingWindows() : flags.getMeasurementEventReportsCtcEarlyReportingWindows(); diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/reporting/EventReportingJobHandler.java b/adservices/service-core/java/com/android/adservices/service/measurement/reporting/EventReportingJobHandler.java index f5042f3afd..c67f0acd67 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/reporting/EventReportingJobHandler.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/reporting/EventReportingJobHandler.java @@ -16,7 +16,9 @@ package com.android.adservices.service.measurement.reporting; -import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_CODE_UNSPECIFIED; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_NETWORK_ERROR; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_PARSING_ERROR; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_UNKNOWN_ERROR; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_MESUREMENT_REPORTS_UPLOADED; @@ -270,7 +272,7 @@ public class EventReportingJobHandler { // TODO(b/298330312): Change to defined error codes ErrorLogUtil.e( e, - AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_CODE_UNSPECIFIED, + AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_NETWORK_ERROR, AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); return AdServicesStatusUtils.STATUS_IO_ERROR; } catch (JSONException e) { @@ -281,7 +283,7 @@ public class EventReportingJobHandler { // TODO(b/298330312): Change to defined error codes ErrorLogUtil.e( e, - AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_CODE_UNSPECIFIED, + AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_PARSING_ERROR, AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); if (mFlags.getMeasurementEnableReportDeletionOnUnrecoverableException()) { // Unrecoverable state - delete the report. @@ -305,7 +307,7 @@ public class EventReportingJobHandler { reportingStatus.setFailureStatus(ReportingStatus.FailureStatus.UNKNOWN); ErrorLogUtil.e( e, - AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ERROR_CODE_UNSPECIFIED, + AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_UNKNOWN_ERROR, AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); if (mFlags.getMeasurementEnableReportingJobsThrowUnaccountedException() && ThreadLocalRandom.current().nextFloat() diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/reporting/ReportingStatus.java b/adservices/service-core/java/com/android/adservices/service/measurement/reporting/ReportingStatus.java index 8e7b1780fe..4e83edb8c9 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/reporting/ReportingStatus.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/reporting/ReportingStatus.java @@ -52,7 +52,9 @@ public class ReportingStatus { VERBOSE_DEBUG_TRIGGER_UNKNOWN_ERROR(26), VERBOSE_DEBUG_TRIGGER_AGGREGATE_STORAGE_LIMIT(27), VERBOSE_DEBUG_TRIGGER_AGGREGATE_EXCESSIVE_REPORTS(28), - VERBOSE_DEBUG_UNKNOWN(29); + VERBOSE_DEBUG_TRIGGER_EVENT_REPORT_WINDOW_NOT_STARTED(29), + VERBOSE_DEBUG_UNKNOWN(9999); + private final int mValue; ReportType(int value) { @@ -202,6 +204,10 @@ public class ReportingStatus { mReportType = ReportType.VERBOSE_DEBUG_TRIGGER_AGGREGATE_STORAGE_LIMIT; } else if (reportType.equals(DebugReportApi.Type.TRIGGER_AGGREGATE_EXCESSIVE_REPORTS)) { mReportType = ReportType.VERBOSE_DEBUG_TRIGGER_AGGREGATE_EXCESSIVE_REPORTS; + } else if (reportType.equals(DebugReportApi.Type.TRIGGER_EVENT_REPORT_WINDOW_NOT_STARTED)) { + mReportType = ReportType.VERBOSE_DEBUG_TRIGGER_EVENT_REPORT_WINDOW_NOT_STARTED; + } else { + mReportType = ReportType.VERBOSE_DEBUG_UNKNOWN; } } diff --git a/adservices/service-core/java/com/android/adservices/service/measurement/util/Enrollment.java b/adservices/service-core/java/com/android/adservices/service/measurement/util/Enrollment.java index 7f71d2eb07..f98f138fb5 100644 --- a/adservices/service-core/java/com/android/adservices/service/measurement/util/Enrollment.java +++ b/adservices/service-core/java/com/android/adservices/service/measurement/util/Enrollment.java @@ -30,7 +30,6 @@ import java.util.Optional; /** Enrollment utilities for measurement. */ public final class Enrollment { - public static final String FAKE_ENROLLMENT = "fake_enrollment"; public static final String LOCALHOST_ENROLLMENT_ID = "localhost_enrollment_id"; public static final String LOCALHOST_IP_ENROLLMENT_ID = "localhost_ip_enrollment_id"; diff --git a/adservices/service-core/java/com/android/adservices/service/shell/AdServicesShellCommandHandler.java b/adservices/service-core/java/com/android/adservices/service/shell/AdServicesShellCommandHandler.java index ab2227b46a..d93eba2db7 100644 --- a/adservices/service-core/java/com/android/adservices/service/shell/AdServicesShellCommandHandler.java +++ b/adservices/service-core/java/com/android/adservices/service/shell/AdServicesShellCommandHandler.java @@ -21,6 +21,7 @@ import android.util.Log; import com.android.adservices.service.common.AppManifestConfigHelper; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; import java.io.PrintWriter; import java.util.Arrays; @@ -45,24 +46,30 @@ public final class AdServicesShellCommandHandler { @VisibleForTesting static final String CMD_IS_ALLOWED_ATTRIBUTION_ACCESS = "is-allowed-attribution-access"; + @VisibleForTesting static final String CMD_IS_ALLOWED_CUSTOM_AUDIENCES_ACCESS = "is-allowed-custom-audiences-access"; + @VisibleForTesting static final String CMD_IS_ALLOWED_TOPICS_ACCESS = "is-allowed-topics-access"; + @VisibleForTesting static final String HELP_ECHO = CMD_ECHO + " <message> - prints the given message (useful to check cmd is working)."; + @VisibleForTesting static final String HELP_IS_ALLOWED_ATTRIBUTION_ACCESS = CMD_IS_ALLOWED_ATTRIBUTION_ACCESS + " <package_name> <enrollment_id> - checks if the given enrollment id is" + " allowed to use the Attribution APIs in the given app."; + @VisibleForTesting static final String HELP_IS_ALLOWED_CUSTOM_AUDIENCES_ACCESS = CMD_IS_ALLOWED_CUSTOM_AUDIENCES_ACCESS + " <package_name> <enrollment_id> - checks if the given enrollment id is" + " allowed to use the Custom Audience APIs in the given app."; + @VisibleForTesting static final String HELP_IS_ALLOWED_TOPICS_ACCESS = CMD_IS_ALLOWED_TOPICS_ACCESS @@ -74,6 +81,7 @@ public final class AdServicesShellCommandHandler { @VisibleForTesting static final String ERROR_TEMPLATE_INVALID_ARGS = "Invalid cmd (%s). Syntax: %s"; + // TODO(b/280460130): use adservice helpers for tag name / logging methods private static final String TAG = "AdServicesShellCmd"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @@ -81,23 +89,27 @@ public final class AdServicesShellCommandHandler { private static final int RESULT_GENERIC_ERROR = -1; private final PrintWriter mOut; + private final PrintWriter mErr; private String[] mArgs; private int mArgPos; private String mCurArgData; + /** If PrintWriter {@code err} is not provided, we use {@code out} for the {@code err}. */ public AdServicesShellCommandHandler(PrintWriter out) { + this(out, /* err= */ out); + } + + public AdServicesShellCommandHandler(PrintWriter out, PrintWriter err) { mOut = Objects.requireNonNull(out, "out cannot be null"); + mErr = Objects.requireNonNull(err, "err cannot be null"); } /** Runs the given command ({@code args[0]}) and optional arguments */ public int run(String... args) { Objects.requireNonNull(args, "args cannot be null"); - // TODO(b/303886367): use Preconditions - if (args.length < 1) { - throw new IllegalArgumentException( - "must have at least one argument (the command itself)"); - } + Preconditions.checkArgument( + args.length >= 1, "must have at least one argument (the command itself)"); if (DEBUG) { Log.d(TAG, "run(): " + Arrays.toString(args)); } @@ -112,13 +124,14 @@ public final class AdServicesShellCommandHandler { } } catch (Throwable e) { // TODO(b/308009734): need to test this - mOut.printf("Exception occurred while executing %s\n", Arrays.toString(mArgs)); + mErr.printf("Exception occurred while executing %s\n", Arrays.toString(mArgs)); e.printStackTrace(mOut); } finally { if (DEBUG) { Log.d(TAG, "Flushing output"); } mOut.flush(); + mErr.flush(); } if (DEBUG) { Log.d(TAG, "Sending command result: " + res); @@ -168,7 +181,7 @@ public final class AdServicesShellCommandHandler { } private int invalidArgsError(String syntax) { - mOut.println(String.format(ERROR_TEMPLATE_INVALID_ARGS, Arrays.toString(mArgs), syntax)); + mErr.println(String.format(ERROR_TEMPLATE_INVALID_ARGS, Arrays.toString(mArgs), syntax)); return RESULT_GENERIC_ERROR; } @@ -188,9 +201,9 @@ public final class AdServicesShellCommandHandler { case CMD_SHORT_HELP: case CMD_HELP: onHelp(); - return RESULT_GENERIC_ERROR; + return RESULT_OK; case "": - mOut.println(ERROR_EMPTY_COMMAND); + mErr.println(ERROR_EMPTY_COMMAND); return RESULT_GENERIC_ERROR; case CMD_ECHO: return runEcho(); @@ -199,7 +212,7 @@ public final class AdServicesShellCommandHandler { case CMD_IS_ALLOWED_TOPICS_ACCESS: return runIsAllowedApiAccess(cmd); default: - mOut.printf("Unknown command: %s\n", cmd); + mErr.printf("Unknown command: %s\n", cmd); return RESULT_GENERIC_ERROR; } } diff --git a/adservices/service-core/java/com/android/adservices/service/shell/ShellCommandServiceImpl.java b/adservices/service-core/java/com/android/adservices/service/shell/ShellCommandServiceImpl.java new file mode 100644 index 0000000000..285396c155 --- /dev/null +++ b/adservices/service-core/java/com/android/adservices/service/shell/ShellCommandServiceImpl.java @@ -0,0 +1,59 @@ +/* + * 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.shell; + +import android.adservices.shell.IShellCommand; +import android.adservices.shell.IShellCommandCallback; +import android.adservices.shell.ShellCommandParam; +import android.adservices.shell.ShellCommandResult; +import android.os.RemoteException; + +import com.android.adservices.LogUtil; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * Implements a service which runs shell command in the AdServices process. + * + * <p>This internally calls {@link AdServicesShellCommandHandler} which has main logic to execute + * the shell command. + * + * @hide + */ +public final class ShellCommandServiceImpl extends IShellCommand.Stub { + @Override + public void runShellCommand(ShellCommandParam param, IShellCommandCallback callback) { + StringWriter outStringWriter = new StringWriter(); + StringWriter ErrStringWriter = new StringWriter(); + + try (PrintWriter outPw = new PrintWriter(outStringWriter); + PrintWriter errPw = new PrintWriter(ErrStringWriter); ) { + AdServicesShellCommandHandler handler = new AdServicesShellCommandHandler(outPw, errPw); + int resultCode = handler.run(param.getCommandArgs()); + ShellCommandResult response = + new ShellCommandResult.Builder() + .setResultCode(resultCode) + .setOut(outStringWriter.toString()) + .setErr(ErrStringWriter.toString()) + .build(); + callback.onResult(response); + } catch (RemoteException e) { + LogUtil.e(e, "Unable to send result to the callback for request: %s", param); + } + } +} diff --git a/adservices/service-core/java/com/android/adservices/service/signals/PeriodicEncodingJobWorker.java b/adservices/service-core/java/com/android/adservices/service/signals/PeriodicEncodingJobWorker.java index 08a6c65254..b792dc7733 100644 --- a/adservices/service-core/java/com/android/adservices/service/signals/PeriodicEncodingJobWorker.java +++ b/adservices/service-core/java/com/android/adservices/service/signals/PeriodicEncodingJobWorker.java @@ -24,10 +24,11 @@ import com.android.adservices.LoggerFactory; import com.android.adservices.concurrency.AdServicesExecutors; import com.android.adservices.data.signals.DBEncodedPayload; import com.android.adservices.data.signals.DBEncoderLogicMetadata; +import com.android.adservices.data.signals.DBSignalsUpdateMetadata; import com.android.adservices.data.signals.EncodedPayloadDao; import com.android.adservices.data.signals.EncoderLogicHandler; import com.android.adservices.data.signals.EncoderLogicMetadataDao; -import com.android.adservices.data.signals.EncoderPersistenceDao; +import com.android.adservices.data.signals.ProtectedSignalsDao; import com.android.adservices.data.signals.ProtectedSignalsDatabase; import com.android.adservices.service.Flags; import com.android.adservices.service.FlagsFactory; @@ -75,9 +76,9 @@ public class PeriodicEncodingJobWorker { private final EncoderLogicHandler mEncoderLogicHandler; private final EncoderLogicMetadataDao mEncoderLogicMetadataDao; - private final EncoderPersistenceDao mEncoderPersistenceDao; private final EncodedPayloadDao mEncodedPayloadDao; private final SignalsProvider mSignalsProvider; + private final ProtectedSignalsDao mProtectedSignalsDao; private final AdSelectionScriptEngine mScriptEngine; private final ListeningExecutorService mBackgroundExecutor; private final ListeningExecutorService mLightWeightExecutor; @@ -92,9 +93,9 @@ public class PeriodicEncodingJobWorker { protected PeriodicEncodingJobWorker( @NonNull EncoderLogicHandler encoderLogicHandler, @NonNull EncoderLogicMetadataDao encoderLogicMetadataDao, - @NonNull EncoderPersistenceDao encoderPersistenceDao, @NonNull EncodedPayloadDao encodedPayloadDao, @NonNull SignalsProviderImpl signalStorageManager, + @NonNull ProtectedSignalsDao protectedSignalsDao, @NonNull AdSelectionScriptEngine scriptEngine, @NonNull ListeningExecutorService backgroundExecutor, @NonNull ListeningExecutorService lightWeightExecutor, @@ -102,9 +103,9 @@ public class PeriodicEncodingJobWorker { @NonNull Flags flags) { mEncoderLogicHandler = encoderLogicHandler; mEncoderLogicMetadataDao = encoderLogicMetadataDao; - mEncoderPersistenceDao = encoderPersistenceDao; mEncodedPayloadDao = encodedPayloadDao; mSignalsProvider = signalStorageManager; + mProtectedSignalsDao = protectedSignalsDao; mScriptEngine = scriptEngine; mBackgroundExecutor = backgroundExecutor; mLightWeightExecutor = lightWeightExecutor; @@ -135,9 +136,9 @@ public class PeriodicEncodingJobWorker { new PeriodicEncodingJobWorker( new EncoderLogicHandler(context), signalsDatabase.getEncoderLogicMetadataDao(), - EncoderPersistenceDao.getInstance(context), signalsDatabase.getEncodedPayloadDao(), new SignalsProviderImpl(signalsDatabase.protectedSignalsDao()), + signalsDatabase.protectedSignalsDao(), new AdSelectionScriptEngine( context, () -> @@ -242,12 +243,30 @@ public class PeriodicEncodingJobWorker { FluentFuture<Void> runEncodingPerBuyer( DBEncoderLogicMetadata encoderLogicMetadata, int timeout) { AdTechIdentifier buyer = encoderLogicMetadata.getBuyer(); + Map<String, List<ProtectedSignal>> signals = mSignalsProvider.getSignals(buyer); if (signals.isEmpty()) { mEncoderLogicHandler.deleteEncoderForBuyer(buyer); return FluentFuture.from(Futures.immediateFuture(null)); } + DBSignalsUpdateMetadata signalsUpdateMetadata = + mProtectedSignalsDao.getSignalsUpdateMetadata(buyer); + DBEncodedPayload existingPayload = mEncodedPayloadDao.getEncodedPayload(buyer); + if (signalsUpdateMetadata != null && existingPayload != null) { + boolean isNoNewSignalUpdateAfterLastEncoding = + signalsUpdateMetadata + .getLastSignalsUpdatedTime() + .isBefore(existingPayload.getCreationTime()); + boolean isEncoderLogicNotUpdatedAfterLastEncoding = + encoderLogicMetadata + .getCreationTime() + .isBefore(existingPayload.getCreationTime()); + if (isNoNewSignalUpdateAfterLastEncoding && isEncoderLogicNotUpdatedAfterLastEncoding) { + return FluentFuture.from(Futures.immediateFuture(null)); + } + } + int failedCount = encoderLogicMetadata.getFailedEncodingCount(); if (failedCount >= mEncoderLogicMaximumFailure) { return FluentFuture.from(Futures.immediateFuture(null)); diff --git a/adservices/service-core/java/com/android/adservices/service/signals/ProtectedSignalsServiceImpl.java b/adservices/service-core/java/com/android/adservices/service/signals/ProtectedSignalsServiceImpl.java index 902ac74117..9cac9eef52 100644 --- a/adservices/service-core/java/com/android/adservices/service/signals/ProtectedSignalsServiceImpl.java +++ b/adservices/service-core/java/com/android/adservices/service/signals/ProtectedSignalsServiceImpl.java @@ -58,6 +58,7 @@ import com.android.adservices.service.stats.AdServicesLogger; import com.android.adservices.service.stats.AdServicesLoggerImpl; import com.android.internal.annotations.VisibleForTesting; +import java.time.Clock; import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -104,7 +105,8 @@ public class ProtectedSignalsServiceImpl extends IProtectedSignalsService.Stub { new UpdateProcessorSelector(), new UpdateEncoderEventHandler(context), new SignalEvictionController()), - new AdTechUriValidator(ADTECH_CALLER_NAME, "", CLASS_NAME, FIELD_NAME)), + new AdTechUriValidator(ADTECH_CALLER_NAME, "", CLASS_NAME, FIELD_NAME), + Clock.systemUTC()), FledgeAuthorizationFilter.create(context, AdServicesLoggerImpl.getInstance()), ConsentManager.getInstance(context), DevContextFilter.create(context), @@ -187,7 +189,7 @@ public class ProtectedSignalsServiceImpl extends IProtectedSignalsService.Stub { } // Caller permissions must be checked in the binder thread, before anything else - mFledgeAuthorizationFilter.assertAppDeclaredProtectedSignalsPermission( + mFledgeAuthorizationFilter.assertAppDeclaredPermission( mContext, updateSignalsInput.getCallerPackageName(), apiName); final int callerUid = getCallingUid(apiName); diff --git a/adservices/service-core/java/com/android/adservices/service/signals/SignalsMaintenanceTasksWorker.java b/adservices/service-core/java/com/android/adservices/service/signals/SignalsMaintenanceTasksWorker.java index 2df832c1ab..0205389b1d 100644 --- a/adservices/service-core/java/com/android/adservices/service/signals/SignalsMaintenanceTasksWorker.java +++ b/adservices/service-core/java/com/android/adservices/service/signals/SignalsMaintenanceTasksWorker.java @@ -109,18 +109,20 @@ public class SignalsMaintenanceTasksWorker { * </ul> */ public void clearInvalidProtectedSignalsData() { - Instant expirationInstant = - mClock.instant().minusSeconds(ProtectedSignal.EXPIRATION_SECONDS); - clearInvalidSignals(expirationInstant); + Instant now = mClock.instant(); + Instant expirationInstant = now.minusSeconds(ProtectedSignal.EXPIRATION_SECONDS); + clearInvalidSignals(expirationInstant, now); clearInvalidEncoders(expirationInstant); clearInvalidEncodedPayloads(expirationInstant); } @VisibleForTesting - void clearInvalidSignals(Instant expirationInstant) { + void clearInvalidSignals(Instant expirationInstant, Instant now) { sLogger.v("Clearing expired signals older than %s", expirationInstant); - int numExpiredSignals = mProtectedSignalsDao.deleteSignalsBeforeTime(expirationInstant); + int numExpiredSignals = + mProtectedSignalsDao.deleteExpiredSignalsAndUpdateSignalsUpdateMetadata( + expirationInstant, now); sLogger.v("Cleared %d expired signals", numExpiredSignals); // Read from flags directly, since this maintenance task worker is attached to a background @@ -138,7 +140,8 @@ public class SignalsMaintenanceTasksWorker { sLogger.v("Clearing signals for disallowed source apps"); int numDisallowedSourceAppSignals = - mProtectedSignalsDao.deleteAllDisallowedPackageSignals(mPackageManager, mFlags); + mProtectedSignalsDao.deleteAllDisallowedPackageSignalsAndUpdateSignalUpdateMetadata( + mPackageManager, mFlags, now); sLogger.v("Cleared %d signals for disallowed source apps", numDisallowedSourceAppSignals); } diff --git a/adservices/service-core/java/com/android/adservices/service/signals/UpdateProcessingOrchestrator.java b/adservices/service-core/java/com/android/adservices/service/signals/UpdateProcessingOrchestrator.java index 9a3c004da4..476445b87d 100644 --- a/adservices/service-core/java/com/android/adservices/service/signals/UpdateProcessingOrchestrator.java +++ b/adservices/service-core/java/com/android/adservices/service/signals/UpdateProcessingOrchestrator.java @@ -115,7 +115,7 @@ public class UpdateProcessingOrchestrator { mSignalEvictionController.evict(adtech, updatedSignals, combinedUpdates); - writeChanges(adtech, combinedUpdates, devContext); + writeChanges(adtech, creationTime, combinedUpdates, devContext); } catch (JSONException e) { throw new IllegalArgumentException("Couldn't unpack signal updates JSON", e); } @@ -195,11 +195,16 @@ public class UpdateProcessingOrchestrator { } private void writeChanges( - AdTechIdentifier adTech, UpdateOutput combinedUpdates, DevContext devContext) { + AdTechIdentifier adTech, + Instant creationTime, + UpdateOutput combinedUpdates, + DevContext devContext) { /* Modify the DB based on the output of the update processors. Might be worth skipping * this is both signalsToAdd and signalsToDelete are empty. */ mProtectedSignalsDao.insertAndDelete( + adTech, + creationTime, combinedUpdates.getToAdd().stream() .map(DBProtectedSignal.Builder::build) .collect(Collectors.toList()), diff --git a/adservices/service-core/java/com/android/adservices/service/signals/UpdateSignalsOrchestrator.java b/adservices/service-core/java/com/android/adservices/service/signals/UpdateSignalsOrchestrator.java index 24ff9be759..73ad03455b 100644 --- a/adservices/service-core/java/com/android/adservices/service/signals/UpdateSignalsOrchestrator.java +++ b/adservices/service-core/java/com/android/adservices/service/signals/UpdateSignalsOrchestrator.java @@ -27,7 +27,7 @@ import com.google.common.util.concurrent.FluentFuture; import org.json.JSONObject; -import java.time.Instant; +import java.time.Clock; import java.util.Objects; import java.util.concurrent.Executor; @@ -38,20 +38,24 @@ public class UpdateSignalsOrchestrator { @NonNull private final UpdatesDownloader mUpdatesDownloader; @NonNull private final UpdateProcessingOrchestrator mUpdateProcessingOrchestrator; @NonNull private final AdTechUriValidator mAdTechUriValidator; + @NonNull private final Clock mClock; public UpdateSignalsOrchestrator( @NonNull Executor backgroundExecutor, @NonNull UpdatesDownloader updatesDownloader, @NonNull UpdateProcessingOrchestrator updateProcessingOrchestrator, - @NonNull AdTechUriValidator adTechUriValidator) { + @NonNull AdTechUriValidator adTechUriValidator, + @NonNull Clock clock) { Objects.requireNonNull(backgroundExecutor); Objects.requireNonNull(updatesDownloader); Objects.requireNonNull(updateProcessingOrchestrator); Objects.requireNonNull(adTechUriValidator); + Objects.requireNonNull(clock); mBackgroundExecutor = backgroundExecutor; mUpdateProcessingOrchestrator = updateProcessingOrchestrator; mUpdatesDownloader = updatesDownloader; mAdTechUriValidator = adTechUriValidator; + mClock = clock; } /** @@ -70,7 +74,7 @@ public class UpdateSignalsOrchestrator { return jsonFuture.transform( x -> { mUpdateProcessingOrchestrator.processUpdates( - adtech, packageName, Instant.now(), x, devContext); + adtech, packageName, mClock.instant(), x, devContext); return null; }, mBackgroundExecutor); diff --git a/adservices/service-core/java/com/android/adservices/service/stats/ConsentMigrationStats.java b/adservices/service-core/java/com/android/adservices/service/stats/ConsentMigrationStats.java index b5437a8da2..d877f45dd6 100644 --- a/adservices/service-core/java/com/android/adservices/service/stats/ConsentMigrationStats.java +++ b/adservices/service-core/java/com/android/adservices/service/stats/ConsentMigrationStats.java @@ -19,6 +19,8 @@ import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICE import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_CONSENT_MIGRATED__MIGRATION_STATUS__SUCCESS_WITH_SHARED_PREF_NOT_UPDATED; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_CONSENT_MIGRATED__MIGRATION_STATUS__SUCCESS_WITH_SHARED_PREF_UPDATED; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_CONSENT_MIGRATED__MIGRATION_STATUS__UNSPECIFIED_MIGRATION_STATUS; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_CONSENT_MIGRATED__MIGRATION_TYPE__ADEXT_SERVICE_TO_APPSEARCH; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_CONSENT_MIGRATED__MIGRATION_TYPE__ADEXT_SERVICE_TO_SYSTEM_SERVICE; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_CONSENT_MIGRATED__MIGRATION_TYPE__APPSEARCH_TO_SYSTEM_SERVICE; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_CONSENT_MIGRATED__MIGRATION_TYPE__PPAPI_TO_SYSTEM_SERVICE; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_CONSENT_MIGRATED__MIGRATION_TYPE__UNSPECIFIED_MIGRATION_TYPE; @@ -75,7 +77,14 @@ public abstract class ConsentMigrationStats { AD_SERVICES_CONSENT_MIGRATED__MIGRATION_TYPE__PPAPI_TO_SYSTEM_SERVICE), // Migrating consent from App Search to system service APPSEARCH_TO_SYSTEM_SERVICE( - AD_SERVICES_CONSENT_MIGRATED__MIGRATION_TYPE__APPSEARCH_TO_SYSTEM_SERVICE); + AD_SERVICES_CONSENT_MIGRATED__MIGRATION_TYPE__APPSEARCH_TO_SYSTEM_SERVICE), + // Migrating consent from AdServicesExtDataStorageService to System Server + ADEXT_SERVICE_TO_SYSTEM_SERVICE( + AD_SERVICES_CONSENT_MIGRATED__MIGRATION_TYPE__ADEXT_SERVICE_TO_SYSTEM_SERVICE), + // Migrating consent from AdServicesExtDataStorageService to App Search + ADEXT_SERVICE_TO_APPSEARCH( + AD_SERVICES_CONSENT_MIGRATED__MIGRATION_TYPE__ADEXT_SERVICE_TO_APPSEARCH); + private final int mMigrationType; MigrationType(int migrationType) { diff --git a/adservices/service-core/java/com/android/adservices/service/topics/EncryptionManager.java b/adservices/service-core/java/com/android/adservices/service/topics/EncryptionManager.java index a9a56b8aaa..4f147fe67f 100644 --- a/adservices/service-core/java/com/android/adservices/service/topics/EncryptionManager.java +++ b/adservices/service-core/java/com/android/adservices/service/topics/EncryptionManager.java @@ -116,8 +116,9 @@ public class EncryptionManager { * public key is missing. */ private Optional<String> fetchPublicKeyFor(String sdkName) { - if (mFlags.isDisableTopicsEnrollmentCheck()) { - return Optional.of(TEST_PUBLIC_KEY_BASE64); + if (!mFlags.getTopicsTestEncryptionPublicKey().isEmpty()) { + // Use testing key for encryption if the test key is non-empty. + return Optional.of(mFlags.getTopicsTestEncryptionPublicKey()); } sLogger.v("Fetching EnrollmentData for %s", sdkName); diff --git a/adservices/service-core/java/com/android/adservices/service/topics/TopicsServiceImpl.java b/adservices/service-core/java/com/android/adservices/service/topics/TopicsServiceImpl.java index 5091491c27..683a63d9ff 100644 --- a/adservices/service-core/java/com/android/adservices/service/topics/TopicsServiceImpl.java +++ b/adservices/service-core/java/com/android/adservices/service/topics/TopicsServiceImpl.java @@ -31,6 +31,7 @@ import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICE import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__API_CALLBACK_ERROR; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__PACKAGE_NAME_NOT_FOUND_EXCEPTION; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__RATE_LIMIT_CALLBACK_FAILURE; +import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_REQUEST_EMPTY_SDK_NAME; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS; import android.adservices.common.AdServicesStatusUtils; @@ -147,7 +148,7 @@ public class TopicsServiceImpl extends ITopicsService.Stub { // Check if the request is valid. if (!validateRequest(topicsParam, callback)) { // Return early if the request is invalid. - sLogger.d("Invalid request %s", topicsParam); + sLogger.e("Invalid request %s", topicsParam); return; } } @@ -202,6 +203,9 @@ public class TopicsServiceImpl extends ITopicsService.Stub { GetTopicsParam topicsParam, IGetTopicsCallback callback) { // Return false if sdkName is empty or null. if (TextUtils.isEmpty(topicsParam.getSdkName())) { + ErrorLogUtil.e( + AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_REQUEST_EMPTY_SDK_NAME, + AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS); invokeCallbackWithStatus( callback, STATUS_INVALID_ARGUMENT, diff --git a/adservices/service-core/java/com/android/adservices/service/topics/classifier/ClassifierInputManager.java b/adservices/service-core/java/com/android/adservices/service/topics/classifier/ClassifierInputManager.java index 8e257aff4b..139d2d9ba6 100644 --- a/adservices/service-core/java/com/android/adservices/service/topics/classifier/ClassifierInputManager.java +++ b/adservices/service-core/java/com/android/adservices/service/topics/classifier/ClassifierInputManager.java @@ -138,7 +138,7 @@ public class ClassifierInputManager { applicationInfo.descriptionRes))); break; default: - LogUtil.e("Invalid input field in config: {}", inputField); + LogUtil.e("Invalid input field in config: %s", inputField); return null; } } @@ -171,7 +171,7 @@ public class ClassifierInputManager { PackageManager packageManager, ApplicationInfo applicationInfo, int resourceId) { if (!SdkLevel.isAtLeastS()) { LogUtil.d( - "English app resource not available for SDK version {} - " + "English app resource not available for SDK version %d - " + "returning localized app resource", Build.VERSION.SDK_INT); return getLocalAppResource(packageManager, applicationInfo, resourceId); @@ -192,7 +192,7 @@ public class ClassifierInputManager { } catch (PackageManager.NameNotFoundException e) { LogUtil.e("No resources returned from packageManager."); } catch (Resources.NotFoundException e) { - LogUtil.e("Resource not found by packageManager - resourceId: {}", resourceId); + LogUtil.e("Resource not found by packageManager - resourceId: %d", resourceId); } return EMPTY_STRING; } @@ -204,7 +204,7 @@ public class ClassifierInputManager { } catch (PackageManager.NameNotFoundException e) { LogUtil.e("No resources returned from packageManager."); } catch (Resources.NotFoundException e) { - LogUtil.e("Resource not found by packageManager - resourceId: {}", resourceId); + LogUtil.e("Resource not found by packageManager - resourceId: %d", resourceId); } return EMPTY_STRING; } diff --git a/adservices/service-core/java/com/android/adservices/service/topics/classifier/ModelManager.java b/adservices/service-core/java/com/android/adservices/service/topics/classifier/ModelManager.java index 3a1f1934a8..4a2fda2f4d 100644 --- a/adservices/service-core/java/com/android/adservices/service/topics/classifier/ModelManager.java +++ b/adservices/service-core/java/com/android/adservices/service/topics/classifier/ModelManager.java @@ -665,7 +665,7 @@ public class ModelManager { try { inputFields.add(ClassifierInputField.valueOf(line)); } catch (IllegalArgumentException e) { - LogUtil.e("Invalid input field in classifier input config: {}", line); + LogUtil.e("Invalid input field in classifier input config: %s", line); return ClassifierInputConfig.getEmptyConfig(); } } @@ -696,7 +696,7 @@ public class ModelManager { try { String formattedInput = String.format(classifierInputConfig.getInputFormat(), (Object[]) inputFields); - LogUtil.d("Validated classifier input format: {}", formattedInput); + LogUtil.d("Validated classifier input format: %s", formattedInput); } catch (IllegalFormatException e) { LogUtil.e("Classifier input config is incorrectly formatted"); return false; diff --git a/adservices/service-core/java/com/android/adservices/service/ui/data/UxStatesManager.java b/adservices/service-core/java/com/android/adservices/service/ui/data/UxStatesManager.java index ea6c8203f9..9079562e3a 100644 --- a/adservices/service-core/java/com/android/adservices/service/ui/data/UxStatesManager.java +++ b/adservices/service-core/java/com/android/adservices/service/ui/data/UxStatesManager.java @@ -102,7 +102,7 @@ public class UxStatesManager { /** Returns process statble UX flags. */ public boolean getFlag(String uxFlagKey) { if (!mUxFlags.containsKey(uxFlagKey)) { - LogUtil.e("Key not found in cached UX flags: ", uxFlagKey); + LogUtil.e("Key not found in cached UX flags: %s", uxFlagKey); } Boolean value = mUxFlags.get(uxFlagKey); return value != null ? value : false; diff --git a/adservices/service-core/java/com/android/adservices/service/ui/enrollment/impl/AlreadyEnrolledChannel.java b/adservices/service-core/java/com/android/adservices/service/ui/enrollment/impl/AlreadyEnrolledChannel.java index caea3ca6c4..bff7b772c5 100644 --- a/adservices/service-core/java/com/android/adservices/service/ui/enrollment/impl/AlreadyEnrolledChannel.java +++ b/adservices/service-core/java/com/android/adservices/service/ui/enrollment/impl/AlreadyEnrolledChannel.java @@ -56,12 +56,9 @@ public class AlreadyEnrolledChannel implements PrivacySandboxEnrollmentChannel { private static boolean isPreNotificationManualUser( ConsentManager consentManager, UxStatesManager uxStatesManager) { - if (uxStatesManager.getFlag(KEY_CONSENT_ALREADY_INTERACTED_FIX_ENABLE) + return uxStatesManager.getFlag(KEY_CONSENT_ALREADY_INTERACTED_FIX_ENABLE) && consentManager.getUserManualInteractionWithConsent() - == MANUAL_INTERACTIONS_RECORDED) { - return true; - } - return false; + == MANUAL_INTERACTIONS_RECORDED; } /** No-Op if the user has already enrolled. */ diff --git a/adservices/service-core/java/com/android/adservices/service/ui/enrollment/impl/ConsentNotificationResetChannel.java b/adservices/service-core/java/com/android/adservices/service/ui/enrollment/impl/ConsentNotificationResetChannel.java index 126e35346a..aa26433a8e 100644 --- a/adservices/service-core/java/com/android/adservices/service/ui/enrollment/impl/ConsentNotificationResetChannel.java +++ b/adservices/service-core/java/com/android/adservices/service/ui/enrollment/impl/ConsentNotificationResetChannel.java @@ -16,6 +16,8 @@ package com.android.adservices.service.ui.enrollment.impl; +import static com.android.adservices.service.consent.ConsentManager.NO_MANUAL_INTERACTIONS_RECORDED; + import android.content.Context; import android.content.SharedPreferences; import android.os.Build; @@ -72,6 +74,7 @@ public class ConsentNotificationResetChannel implements PrivacySandboxEnrollment /** Perform enrollment logic for the reset channel. */ public void enroll(Context context, ConsentManager consentManager) { + consentManager.recordUserManualInteractionWithConsent(NO_MANUAL_INTERACTIONS_RECORDED); consentManager.recordNotificationDisplayed(false); consentManager.recordGaUxNotificationDisplayed(false); consentManager.setU18NotificationDisplayed(false); diff --git a/adservices/service-core/java/com/android/adservices/service/ui/enrollment/impl/RvcPostOTAChannel.java b/adservices/service-core/java/com/android/adservices/service/ui/enrollment/impl/RvcPostOTAChannel.java index 6d6bc18d67..6d89bd4d3e 100644 --- a/adservices/service-core/java/com/android/adservices/service/ui/enrollment/impl/RvcPostOTAChannel.java +++ b/adservices/service-core/java/com/android/adservices/service/ui/enrollment/impl/RvcPostOTAChannel.java @@ -16,7 +16,7 @@ package com.android.adservices.service.ui.enrollment.impl; -import static com.android.adservices.service.FlagsConstants.KEY_RVC_NOTIFICATION_ENABLED; +import static com.android.adservices.service.FlagsConstants.KEY_RVC_POST_OTA_NOTIFICATION_ENABLED; import android.content.Context; import android.os.Build; @@ -40,8 +40,7 @@ public class RvcPostOTAChannel implements PrivacySandboxEnrollmentChannel { ConsentManager consentManager, UxStatesManager uxStatesManager) { // Rvc user should be matched to RvcPostOTAChannel on S+ - // TODO: rename flag to KEY_RVC_POST_OTA_NOTIFICATION_ENABLED - return uxStatesManager.getFlag(KEY_RVC_NOTIFICATION_ENABLED) + return uxStatesManager.getFlag(KEY_RVC_POST_OTA_NOTIFICATION_ENABLED) && consentManager.isOtaAdultUserFromRvc(); } |