Extend Rb integration with AppSearch to consent data.
Test: atest AppSearchConsentManagerTest AppSearchDaoTest AppSearchConsentDaoTest AppSearchConsentWorkerTest ConsentManagerTest AppSearchAppConsentDaoTest
Bug: b/263297331
Change-Id: Ic5fde2b51a627a4225f12f6a19c8e326efd4d70e
diff --git a/adservices/apk/tests/Settings/src/com/android/adservices/ui/settings/AppConsentSettingsUiAutomatorTest.java b/adservices/apk/tests/Settings/src/com/android/adservices/ui/settings/AppConsentSettingsUiAutomatorTest.java
index cb7df7a..a03b998 100644
--- a/adservices/apk/tests/Settings/src/com/android/adservices/ui/settings/AppConsentSettingsUiAutomatorTest.java
+++ b/adservices/apk/tests/Settings/src/com/android/adservices/ui/settings/AppConsentSettingsUiAutomatorTest.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
+
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -37,6 +38,7 @@
import com.android.adservices.api.R;
import com.android.adservices.common.AdservicesTestHelper;
import com.android.adservices.common.CompatAdServicesTestUtils;
+import com.android.adservices.service.Flags;
import com.android.compatibility.common.util.ShellUtils;
import com.android.modules.utils.build.SdkLevel;
@@ -61,8 +63,6 @@
private static final String PRIVACY_SANDBOX_TEST_PACKAGE = "android.test.adservices.ui.MAIN";
private static final int LAUNCH_TIMEOUT = 5000;
private static UiDevice sDevice;
- private static final String ADEXTSERVICES_PACKAGE_NAME =
- "com.google.android.ext.adservices.api";
@Before
public void setup() throws UiObjectNotFoundException {
@@ -126,6 +126,27 @@
@Test
@Ignore("Flaky test. (b/268351419)")
+ public void consentAppSearchOnlyTest() throws UiObjectNotFoundException, InterruptedException {
+ ShellUtils.runShellCommand(
+ "device_config put adservices enable_appsearch_consent_data true");
+ ShellUtils.runShellCommand("device_config put adservices consent_source_of_truth 3");
+ appConsentTest(Flags.APPSEARCH_ONLY, false);
+ ShellUtils.runShellCommand("device_config put adservices consent_source_of_truth null");
+ }
+
+ @Test
+ @Ignore("Flaky test. (b/268351419)")
+ public void consentAppSearchOnlyDialogsOnTest()
+ throws UiObjectNotFoundException, InterruptedException {
+ ShellUtils.runShellCommand(
+ "device_config put adservices enable_appsearch_consent_data true");
+ ShellUtils.runShellCommand("device_config put adservices consent_source_of_truth 3");
+ appConsentTest(Flags.APPSEARCH_ONLY, true);
+ ShellUtils.runShellCommand("device_config put adservices consent_source_of_truth null");
+ }
+
+ @Test
+ @Ignore("Flaky test. (b/268351419)")
public void consentSystemServerOnlyDialogsOnTest()
throws UiObjectNotFoundException, InterruptedException {
// System server is not available on S-, skip this test for S-
@@ -291,6 +312,8 @@
ShellUtils.runShellCommand(
"device_config put adservices"
+ " fledge_custom_audience_service_kill_switch false");
+ ShellUtils.runShellCommand(
+ "device_config put adservices disable_fledge_enrollment_check true");
ShellUtils.runShellCommand("device_config put adservices ppapi_app_allow_list *");
@@ -306,5 +329,7 @@
ShellUtils.runShellCommand("device_config set_sync_disabled_for_tests none");
ShellUtils.runShellCommand(
"am force-stop com.example.adservices.samples.ui.consenttestapp");
+ ShellUtils.runShellCommand(
+ "device_config put adservices disable_fledge_enrollment_check null");
}
}
diff --git a/adservices/apk/tests/Settings/src/com/android/adservices/ui/settings/ConsentSettingsUiAutomatorTest.java b/adservices/apk/tests/Settings/src/com/android/adservices/ui/settings/ConsentSettingsUiAutomatorTest.java
index b1000d6..22e29d1 100644
--- a/adservices/apk/tests/Settings/src/com/android/adservices/ui/settings/ConsentSettingsUiAutomatorTest.java
+++ b/adservices/apk/tests/Settings/src/com/android/adservices/ui/settings/ConsentSettingsUiAutomatorTest.java
@@ -161,10 +161,30 @@
@Test
public void consentAppSearchOnlyTest() throws UiObjectNotFoundException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
- doReturn(3).when(mMockFlags).getConsentSourceOfTruth();
+ doReturn(true).when(mMockFlags).getEnableAppsearchConsentData();
+ doReturn(Flags.APPSEARCH_ONLY).when(mMockFlags).getConsentSourceOfTruth();
+ consentTest(true);
+ } else {
+ ShellUtils.runShellCommand(
+ "device_config put adservices enable_appsearch_consent_data true");
+ ShellUtils.runShellCommand("device_config put adservices consent_source_of_truth 3");
+ consentTest(true);
+ ShellUtils.runShellCommand("device_config put adservices consent_source_of_truth null");
+ ShellUtils.runShellCommand(
+ "device_config put adservices enable_appsearch_consent_data null");
+ }
+ }
+
+ @Test
+ public void consentAppSearchOnlyDialogsOnTest() throws UiObjectNotFoundException {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ doReturn(true).when(mMockFlags).getEnableAppsearchConsentData();
+ doReturn(Flags.APPSEARCH_ONLY).when(mMockFlags).getConsentSourceOfTruth();
doReturn(true).when(mPhFlags).getUIDialogsFeatureEnabled();
consentTest(true);
} else {
+ ShellUtils.runShellCommand(
+ "device_config put adservices enable_appsearch_consent_data true");
ShellUtils.runShellCommand("device_config put adservices consent_source_of_truth 3");
ShellUtils.runShellCommand(
"device_config put adservices ui_dialogs_feature_enabled true");
@@ -174,7 +194,6 @@
}
private void consentTest(boolean dialogsOn) throws UiObjectNotFoundException {
-
ApkTestUtil.launchSettingView(
ApplicationProvider.getApplicationContext(), sDevice, LAUNCH_TIMEOUT);
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 dac66b8..484c11e 100644
--- a/adservices/service-core/java/com/android/adservices/service/Flags.java
+++ b/adservices/service-core/java/com/android/adservices/service/Flags.java
@@ -26,6 +26,7 @@
import com.android.adservices.data.adselection.DBRegisteredAdInteraction;
import com.android.adservices.service.adselection.AdOutcomeSelectorImpl;
import com.android.adservices.service.common.cache.FledgeHttpCache;
+import com.android.modules.utils.build.SdkLevel;
import com.google.common.collect.ImmutableList;
@@ -881,11 +882,21 @@
int PPAPI_ONLY = 1;
/** Write consent to both PPAPI and system server. Read consent from system server only. */
int PPAPI_AND_SYSTEM_SERVER = 2;
- /** Write consent data to AppSearch only. */
+ /**
+ * Write consent data to AppSearch only. To store consent data in AppSearch the flag
+ * enable_appsearch_consent_data must also be true. This ensures that both writes and reads can
+ * happen to/from AppSearch. The writes are done by code on S-, while reads are done from code
+ * running on S- for all consent requests and on T+ once after OTA.
+ */
int APPSEARCH_ONLY = 3;
- /* Consent source of truth intended to be used by default. */
- @ConsentSourceOfTruth int DEFAULT_CONSENT_SOURCE_OF_TRUTH = PPAPI_AND_SYSTEM_SERVER;
+ /**
+ * Consent source of truth intended to be used by default. On S- devices, there is no AdServices
+ * code running in the system server, so the default for those is PPAPI_ONLY.
+ */
+ @ConsentSourceOfTruth
+ int DEFAULT_CONSENT_SOURCE_OF_TRUTH =
+ SdkLevel.isAtLeastT() ? PPAPI_AND_SYSTEM_SERVER : PPAPI_ONLY;
/** Returns the consent source of truth currently used for PPAPI. */
@ConsentSourceOfTruth
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 af06f20..a1fd454 100644
--- a/adservices/service-core/java/com/android/adservices/service/PhFlags.java
+++ b/adservices/service-core/java/com/android/adservices/service/PhFlags.java
@@ -2954,13 +2954,11 @@
@Override
public boolean getEnableAppsearchConsentData() {
- // Check if enable Back compat is true first and then check flag value
// The priority of applying the flag values: PH (DeviceConfig) and then hard-coded value.
- return getEnableBackCompat()
- && DeviceConfig.getBoolean(
- NAMESPACE_ADSERVICES,
- /* flagName */ KEY_ENABLE_APPSEARCH_CONSENT_DATA,
- /* defaultValue */ ENABLE_APPSEARCH_CONSENT_DATA);
+ return DeviceConfig.getBoolean(
+ NAMESPACE_ADSERVICES,
+ /* flagName */ KEY_ENABLE_APPSEARCH_CONSENT_DATA,
+ /* defaultValue */ ENABLE_APPSEARCH_CONSENT_DATA);
}
@Override
diff --git a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchAppConsentDao.java b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchAppConsentDao.java
new file mode 100644
index 0000000..71be5c0
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchAppConsentDao.java
@@ -0,0 +1,216 @@
+/*
+ * 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.appsearch;
+
+import android.annotation.NonNull;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.AppSearchSchema.StringPropertyConfig;
+import androidx.appsearch.app.GlobalSearchSession;
+
+import com.android.adservices.LogUtil;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/** This class represents the data access object for the app consent data written to AppSearch. */
+// TODO(b/269798827): Enable for R.
+@RequiresApi(Build.VERSION_CODES.S)
+@Document
+class AppSearchAppConsentDao extends AppSearchDao {
+ /**
+ * Identifier of the Consent Document; must be unique within the Document's `namespace`. This is
+ * the row ID for consent data. It is a combination of user ID and consent type.
+ */
+ @Document.Id private final String mId;
+
+ @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ private final String mUserId;
+
+ /** Namespace of the Consent Document. Used to group documents during querying or deletion. */
+ @Document.Namespace private final String mNamespace;
+
+ /**
+ * Consent type for this table. Possible values are: APPS_WITH_CONSENT,
+ * APPS_WITH_REVOKED_CONSENT, APPS_WITH_FLEDGE_CONSENT and APPS_WITH_FLEDGE_REVOKED_CONSENT.
+ */
+ @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+ private final String mConsentType;
+
+ /** List of apps. */
+ @Document.StringProperty private List<String> mApps;
+
+ // Column names used for preparing the query string, are not part of the @Document.
+ private static final String USER_ID_COLNAME = "userId";
+ private static final String CONSENT_TYPE_COLNAME = "consentType";
+ public static final String NAMESPACE = "appConsent";
+
+ // Consent types we store with this DAO.
+ public static final String APPS_WITH_CONSENT = "APPS_WITH_CONSENT";
+ public static final String APPS_WITH_REVOKED_CONSENT = "APPS_WITH_REVOKED_CONSENT";
+
+ /**
+ * Create an AppSearchConsentDao instance.
+ *
+ * @param id is a combination of the user ID and apiType
+ * @param userId is the user ID for this user
+ * @param namespace (required by AppSearch)
+ * @param consentType is the consentType for which we are storing consent data
+ * @param apps list of apps
+ */
+ AppSearchAppConsentDao(
+ String id, String userId, String namespace, String consentType, List<String> apps) {
+ this.mId = id;
+ this.mUserId = userId;
+ this.mNamespace = namespace;
+ this.mConsentType = consentType;
+ this.mApps = apps;
+ }
+
+ /**
+ * Get the row ID for this row.
+ *
+ * @return ID
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Get the user ID for this row.
+ *
+ * @return user ID
+ */
+ public String getUserId() {
+ return mUserId;
+ }
+
+ /**
+ * Get the namespace for this row.
+ *
+ * @return nameespace
+ */
+ public String getNamespace() {
+ return mNamespace;
+ }
+
+ /**
+ * Get the apiType for this row.
+ *
+ * @return apiType
+ */
+ public String getConsentType() {
+ return mConsentType;
+ }
+
+ /**
+ * Gets the list of apps.
+ *
+ * @return List of app package names.
+ */
+ public List<String> getApps() {
+ return mApps;
+ }
+
+ /** Sets the apps. */
+ public void setApps(List<String> apps) {
+ mApps = apps;
+ }
+
+ /** Returns the row ID that should be unique for the consent namespace. */
+ public static String getRowId(@NonNull String uid, @NonNull String consentType) {
+ Objects.requireNonNull(uid);
+ Objects.requireNonNull(consentType);
+ return uid + "_" + consentType;
+ }
+
+ /**
+ * Converts the DAO to a string.
+ *
+ * @return string representing the DAO.
+ */
+ public String toString() {
+ return "id="
+ + mId
+ + "; userId="
+ + mUserId
+ + "; consentType="
+ + mConsentType
+ + "; namespace="
+ + mNamespace
+ + "; apps="
+ + (mApps == null ? "null" : Arrays.toString(mApps.toArray()));
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mId, mUserId, mNamespace, mApps, mConsentType);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof AppSearchAppConsentDao)) return false;
+ AppSearchAppConsentDao obj = (AppSearchAppConsentDao) o;
+ return (Objects.equals(this.mId, obj.mId))
+ && (Objects.equals(this.mUserId, obj.mUserId))
+ && (Objects.equals(this.mConsentType, obj.mConsentType))
+ && (Objects.equals(this.mNamespace, obj.mNamespace))
+ && (Objects.equals(this.mApps, obj.mApps));
+ }
+
+ /**
+ * Read the consent data from AppSearch.
+ *
+ * @param searchSession we use GlobalSearchSession here to allow AdServices to read.
+ * @param executor the Executor to use.
+ * @param userId the user ID for the query.
+ * @param apiType the API type for the query.
+ * @return whether the row is consented for this user ID and apiType.
+ */
+ static AppSearchAppConsentDao readConsentData(
+ @NonNull ListenableFuture<GlobalSearchSession> searchSession,
+ @NonNull Executor executor,
+ @NonNull String userId,
+ @NonNull String apiType) {
+ Objects.requireNonNull(searchSession);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(userId);
+ Objects.requireNonNull(apiType);
+
+ String query = getQuery(userId, apiType);
+ AppSearchAppConsentDao dao =
+ AppSearchDao.readConsentData(
+ AppSearchAppConsentDao.class, searchSession, executor, NAMESPACE, query);
+ LogUtil.d("AppSearch app consent data read: " + dao + " [ query: " + query + "]");
+ return dao;
+ }
+
+ // Get the search query for AppSearch. Format specified at http://shortn/_RwVKmB74f3.
+ // Note: AND as an operator is not supported by AppSearch on S or T.
+ @VisibleForTesting
+ static String getQuery(String userId, String consentType) {
+ return USER_ID_COLNAME + ":" + userId + " " + CONSENT_TYPE_COLNAME + ":" + consentType;
+ }
+}
diff --git a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentDao.java b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentDao.java
index 75aba03..095c28a 100644
--- a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentDao.java
+++ b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentDao.java
@@ -16,6 +16,7 @@
package com.android.adservices.service.appsearch;
+import android.annotation.NonNull;
import android.os.Build;
import androidx.annotation.RequiresApi;
@@ -35,7 +36,7 @@
// TODO(b/269798827): Enable for R.
@RequiresApi(Build.VERSION_CODES.S)
@Document
-public class AppSearchConsentDao extends AppSearchDao {
+class AppSearchConsentDao extends AppSearchDao {
/**
* Identifier of the Consent Document; must be unique within the Document's `namespace`. This is
* the row ID for consent data. It is a combination of user ID and api type.
@@ -49,8 +50,9 @@
@Document.Namespace private final String mNamespace;
/**
- * API type for this consent. Possible values are CONSENT, CONSENT-FLEDGE, CONSENT-MEASUREMENT
- * and CONSENT-TOPICS.
+ * API type for this consent. Possible values are a) CONSENT, CONSENT-FLEDGE,
+ * CONSENT-MEASUREMENT, CONSENT-TOPICS, b) DEFAULT_CONSENT, TOPICS_DEFAULT_CONSENT,
+ * FLEDGE_DEFAULT_CONSENT, MEASUREMENT_DEFAULT_CONSENT and c) DEFAULT_AD_ID_STATE.
*/
@Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
private final String mApiType;
@@ -131,6 +133,11 @@
return mConsent.equals("true");
}
+ /** Returns the row ID that should be unique for the consent namespace. */
+ public static String getRowId(@NonNull String uid, @NonNull String apiType) {
+ return uid + "_" + apiType;
+ }
+
/**
* Converts the DAO to a string.
*
@@ -175,18 +182,21 @@
* @param apiType the API type for the query.
* @return whether the row is consented for this user ID and apiType.
*/
- public static boolean readConsentData(
- ListenableFuture<GlobalSearchSession> searchSession,
- Executor executor,
- String userId,
- String apiType) {
+ static boolean readConsentData(
+ @NonNull ListenableFuture<GlobalSearchSession> searchSession,
+ @NonNull Executor executor,
+ @NonNull String userId,
+ @NonNull String apiType) {
+ Objects.requireNonNull(searchSession);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(userId);
+ Objects.requireNonNull(apiType);
+
+ String query = getQuery(userId, apiType);
AppSearchConsentDao dao =
AppSearchDao.readConsentData(
- AppSearchConsentDao.class,
- searchSession,
- executor,
- getQuery(userId, apiType));
- LogUtil.d("AppSearch consent data read: " + dao + " [" + userId + ":" + apiType + "]");
+ AppSearchConsentDao.class, searchSession, executor, NAMESPACE, query);
+ LogUtil.d("AppSearch app consent data read: " + dao + " [ query: " + query + "]");
if (dao == null) {
return false;
}
diff --git a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentManager.java b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentManager.java
new file mode 100644
index 0000000..42fc7cd
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentManager.java
@@ -0,0 +1,308 @@
+/*
+ * 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.appsearch;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.android.adservices.service.common.compat.PackageManagerCompatUtils;
+import com.android.adservices.service.common.feature.PrivacySandboxFeatureType;
+import com.android.adservices.service.consent.App;
+import com.android.adservices.service.consent.ConsentManager;
+
+import com.google.common.collect.ImmutableList;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * This class manages the interface to AppSearch for reading/writing all AdServices consent data on
+ * S- devices. This is needed because AdServices does not run any code in the system server on S-
+ * devices, so consent data is rollback safe by storing it in AppSearch.
+ */
+// TODO(b/269798827): Enable for R.
+@RequiresApi(Build.VERSION_CODES.S)
+public class AppSearchConsentManager {
+ private Context mContext;
+ private AppSearchConsentWorker mAppSearchConsentWorker;
+
+ private AppSearchConsentManager(
+ @NonNull Context context, @NonNull AppSearchConsentWorker appSearchConsentWorker) {
+ Objects.requireNonNull(context);
+ Objects.requireNonNull(appSearchConsentWorker);
+ mContext = context;
+ mAppSearchConsentWorker = appSearchConsentWorker;
+ }
+
+ /** Returns an instance of AppSearchConsentManager. */
+ public static AppSearchConsentManager getInstance(@NonNull Context context) {
+ Objects.requireNonNull(context);
+ return new AppSearchConsentManager(context, AppSearchConsentWorker.getInstance(context));
+ }
+
+ /**
+ * Get the consent for this user ID for this API type, as stored in AppSearch. Returns false if
+ * the database doesn't exist in AppSearch.
+ */
+ public boolean getConsent(@NonNull String apiType) {
+ Objects.requireNonNull(apiType);
+ return mAppSearchConsentWorker.getConsent(apiType);
+ }
+
+ /**
+ * 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.
+ */
+ public void setConsent(@NonNull String apiType, @NonNull Boolean consented) {
+ Objects.requireNonNull(apiType);
+ Objects.requireNonNull(consented);
+ mAppSearchConsentWorker.setConsent(apiType, consented);
+ }
+
+ /**
+ * Get known apps with consent as stored in AppSearch.
+ *
+ * @return an {@link ImmutableList} of all known apps in the database that have not had user
+ * consent revoked.
+ */
+ public ImmutableList<App> getKnownAppsWithConsent() {
+ List<String> apps =
+ mAppSearchConsentWorker.getAppsWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_CONSENT);
+ Set<String> installedPackages = getInstalledPackages();
+ List<App> result = new ArrayList<>();
+ for (String app : apps) {
+ if (installedPackages.contains(app)) {
+ result.add(App.create(app));
+ }
+ }
+ return ImmutableList.copyOf(result);
+ }
+
+ /**
+ * Get apps with consent revoked, as stored in AppSearch.
+ *
+ * @return an {@link ImmutableList} of all known apps in the database that have had user consent
+ * revoked.
+ */
+ public ImmutableList<App> getAppsWithRevokedConsent() {
+ List<String> apps =
+ mAppSearchConsentWorker.getAppsWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_REVOKED_CONSENT);
+ Set<String> installedPackages = getInstalledPackages();
+ List<App> result = new ArrayList<>();
+ for (String app : apps) {
+ if (installedPackages.contains(app)) {
+ result.add(App.create(app));
+ }
+ }
+ return ImmutableList.copyOf(result);
+ }
+
+ /**
+ * Clears all app data related to the provided {@link App}.
+ *
+ * @param app {@link App} to block.
+ */
+ public void revokeConsentForApp(@NonNull App app) {
+ Objects.requireNonNull(app);
+ mAppSearchConsentWorker.addAppWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_REVOKED_CONSENT, app.getPackageName());
+ mAppSearchConsentWorker.removeAppWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_CONSENT, app.getPackageName());
+ }
+
+ /**
+ * Restore consent for provided {@link App}.
+ *
+ * @param app {@link App} to restore consent for.
+ */
+ public void restoreConsentForApp(@NonNull App app) {
+ Objects.requireNonNull(app);
+ mAppSearchConsentWorker.removeAppWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_REVOKED_CONSENT, app.getPackageName());
+ mAppSearchConsentWorker.addAppWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_CONSENT, app.getPackageName());
+ }
+
+ /**
+ * 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.
+ */
+ public void clearAllAppConsentData() {
+ mAppSearchConsentWorker.clearAppsWithConsent(AppSearchAppConsentDao.APPS_WITH_CONSENT);
+ mAppSearchConsentWorker.clearAppsWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_REVOKED_CONSENT);
+ }
+
+ /**
+ * 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.
+ */
+ public void clearKnownAppsWithConsent() throws IOException {
+ mAppSearchConsentWorker.clearAppsWithConsent(AppSearchAppConsentDao.APPS_WITH_CONSENT);
+ }
+
+ /**
+ * 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) {
+ Objects.requireNonNull(packageName);
+
+ boolean isConsented =
+ mAppSearchConsentWorker
+ .getAppsWithConsent(AppSearchAppConsentDao.APPS_WITH_CONSENT)
+ .contains(packageName);
+ boolean isRevoked =
+ mAppSearchConsentWorker
+ .getAppsWithConsent(AppSearchAppConsentDao.APPS_WITH_REVOKED_CONSENT)
+ .contains(packageName);
+ return isRevoked || !isConsented;
+ }
+
+ /**
+ * 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) {
+ Objects.requireNonNull(packageName);
+
+ return !mAppSearchConsentWorker.addAppWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_CONSENT, packageName);
+ }
+
+ /**
+ * Clear consent data after an app was uninstalled, but the package Uid is unavailable. This
+ * happens 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) {
+ Objects.requireNonNull(packageName);
+
+ mAppSearchConsentWorker.removeAppWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_REVOKED_CONSENT, packageName);
+ mAppSearchConsentWorker.removeAppWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_CONSENT, packageName);
+ }
+
+ /**
+ * Saves information to the storage that notification was displayed for the first time to the
+ * user.
+ */
+ public void recordNotificationDisplayed() {
+ mAppSearchConsentWorker.recordNotificationDisplayed();
+ }
+
+ /**
+ * Retrieves if notification has been displayed.
+ *
+ * @return true if Consent Notification was displayed, otherwise false.
+ */
+ public Boolean wasNotificationDisplayed() {
+ return mAppSearchConsentWorker.wasNotificationDisplayed();
+ }
+
+ /**
+ * Saves information to the storage that GA UX notification was displayed for the first time to
+ * the user.
+ */
+ public void recordGaUxNotificationDisplayed() {
+ mAppSearchConsentWorker.recordGaUxNotificationDisplayed();
+ }
+
+ /**
+ * Retrieves if GA UX notification has been displayed.
+ *
+ * @return true if GA UX Consent Notification was displayed, otherwise false.
+ */
+ public Boolean wasGaUxNotificationDisplayed() {
+ return mAppSearchConsentWorker.wasGaUxNotificationDisplayed();
+ }
+
+ /** Get the current privacy sandbox feature. */
+ public PrivacySandboxFeatureType getCurrentPrivacySandboxFeature() {
+ return mAppSearchConsentWorker.getPrivacySandboxFeature();
+ }
+
+ /** Set the current privacy sandbox feature. */
+ public void setCurrentPrivacySandboxFeature(
+ @NonNull PrivacySandboxFeatureType currentFeatureType) {
+ Objects.requireNonNull(currentFeatureType);
+ mAppSearchConsentWorker.setCurrentPrivacySandboxFeature(currentFeatureType);
+ }
+
+ /**
+ * Returns information whether user interacted with consent manually.
+ *
+ * @return true if the user interacted with the consent manually, otherwise false.
+ */
+ public @ConsentManager.UserManualInteraction int getUserManualInteractionWithConsent() {
+ return mAppSearchConsentWorker.getUserManualInteractionWithConsent();
+ }
+
+ /** Saves information to the storage that user interacted with consent manually. */
+ public void recordUserManualInteractionWithConsent(
+ @ConsentManager.UserManualInteraction int interaction) {
+ mAppSearchConsentWorker.recordUserManualInteractionWithConsent(interaction);
+ }
+
+ /** Returns the list of packages installed on the device of the user. */
+ @NonNull
+ private Set<String> getInstalledPackages() {
+ return PackageManagerCompatUtils.getInstalledApplications(mContext.getPackageManager(), 0)
+ .stream()
+ .map(applicationInfo -> applicationInfo.packageName)
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentService.java b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentService.java
deleted file mode 100644
index f247da1..0000000
--- a/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentService.java
+++ /dev/null
@@ -1,171 +0,0 @@
-/*
- * 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.appsearch;
-
-import android.annotation.NonNull;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.content.pm.Signature;
-import android.os.Binder;
-import android.os.Build;
-import android.os.UserHandle;
-
-import androidx.annotation.RequiresApi;
-import androidx.appsearch.app.AppSearchSession;
-import androidx.appsearch.app.GlobalSearchSession;
-import androidx.appsearch.app.PackageIdentifier;
-import androidx.appsearch.platformstorage.PlatformStorage;
-
-import com.android.adservices.AdServicesCommon;
-import com.android.adservices.LogUtil;
-import com.android.adservices.concurrency.AdServicesExecutors;
-import com.android.adservices.service.consent.ConsentConstants;
-import com.android.internal.annotations.VisibleForTesting;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/**
- * This class provides an interface to read/write consent data to AppSearch. This is used as the
- * 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)
-public class AppSearchConsentService {
- // Timeout for AppSearch write query in milliseconds.
- private static final int TIMEOUT = 2000;
- // This is used to convert the current package name belonging to AdExtServices to the
- // corresponding package name for AdServices.
- private static final String EXTSERVICES_PACKAGE_NAME_SUBSTRING = "ext.";
- private static final String DATABASE_NAME = "adservices_consent";
-
- // Required for allowing AdServices apk access to read consent written by ExtServices module.
- private String mAdservicesPackageName;
- private static final String ADSERVICES_SHA =
- "686d5c450e00ebe600f979300a29234644eade42f24ede07a073f2bc6b94a3a2";
- private Context mContext;
-
- private ListenableFuture<AppSearchSession> mSearchSession;
-
- // When reading across APKs, a GlobalSearchSession is needed, hence we use it when reading.
- private ListenableFuture<GlobalSearchSession> mGlobalSearchSession;
- private Executor mExecutor = AdServicesExecutors.getBackgroundExecutor();
-
- private PackageIdentifier mPackageIdentifier;
-
- private AppSearchConsentService(Context context) {
- mContext = context;
- mSearchSession =
- PlatformStorage.createSearchSessionAsync(
- new PlatformStorage.SearchContext.Builder(mContext, DATABASE_NAME).build());
- mGlobalSearchSession =
- PlatformStorage.createGlobalSearchSessionAsync(
- new PlatformStorage.GlobalSearchContext.Builder(mContext).build());
- mAdservicesPackageName = getAdServicesPackageName(mContext);
- mPackageIdentifier =
- new PackageIdentifier(
- mAdservicesPackageName, new Signature(ADSERVICES_SHA).toByteArray());
- }
-
- /** Get an instance of AppSearchConsentService. */
- public static AppSearchConsentService getInstance(@NonNull Context context) {
- return new AppSearchConsentService(context);
- }
-
- /**
- * Get the consent for this user ID for this API type, as stored in AppSearch. Returns false if
- * the database doesn't exist in AppSearch.
- */
- public boolean getConsent(@NonNull String apiType) {
- return AppSearchConsentDao.readConsentData(
- mGlobalSearchSession, mExecutor, getUserIdentifierFromBinderCallingUid(), apiType);
- }
-
- /**
- * 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.
- */
- public void setConsent(@NonNull String apiType, @NonNull Boolean consented) {
- String uid = getUserIdentifierFromBinderCallingUid();
- // The ID of the row needs to unique per row. For a given user, we store multiple rows, one
- // per each apiType.
- AppSearchConsentDao dao =
- new AppSearchConsentDao(
- getRowId(uid, apiType),
- uid,
- AppSearchConsentDao.NAMESPACE,
- apiType,
- consented.toString());
- try {
- dao.writeConsentData(mSearchSession, mPackageIdentifier, mExecutor)
- .get(TIMEOUT, TimeUnit.MILLISECONDS);
- } catch (InterruptedException | TimeoutException | ExecutionException e) {
- LogUtil.e("Failed to write consent to AppSearch ", e);
- throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE);
- }
- }
-
- /** Returns the row ID that should be unique for the consent namespace. */
- @VisibleForTesting
- String getRowId(@NonNull String uid, @NonNull String apiType) {
- return uid + "_" + apiType;
- }
-
- /** Returns the User Identifier from the CallingUid. */
- @VisibleForTesting
- String getUserIdentifierFromBinderCallingUid() {
- return "" + UserHandle.getUserHandleForUid(Binder.getCallingUid()).getIdentifier();
- }
-
- /**
- * This method returns the package name of the AdServices APK from AdServices apex (T+). On an
- * S- device, it removes the "ext." substring from the package name.
- */
- @VisibleForTesting
- static String getAdServicesPackageName(Context context) {
- Intent serviceIntent = new Intent(AdServicesCommon.ACTION_TOPICS_SERVICE);
- List<ResolveInfo> resolveInfos =
- context.getPackageManager()
- .queryIntentServices(
- serviceIntent,
- PackageManager.GET_SERVICES
- | PackageManager.MATCH_SYSTEM_ONLY
- | PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
- final ServiceInfo serviceInfo =
- AdServicesCommon.resolveAdServicesService(resolveInfos, serviceIntent.getAction());
- if (serviceInfo != null) {
- // Return the AdServices package name based on the current package name.
- String packageName = serviceInfo.packageName;
- if (packageName == null || packageName.isEmpty()) {
- throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE);
- }
- return packageName.replace(EXTSERVICES_PACKAGE_NAME_SUBSTRING, "");
- }
- throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE);
- }
-}
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
new file mode 100644
index 0000000..2b381ba
--- /dev/null
+++ b/adservices/service-core/java/com/android/adservices/service/appsearch/AppSearchConsentWorker.java
@@ -0,0 +1,369 @@
+/*
+ * 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.appsearch;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.pm.Signature;
+import android.os.Binder;
+import android.os.Build;
+import android.os.UserHandle;
+
+import androidx.annotation.RequiresApi;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.GlobalSearchSession;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.platformstorage.PlatformStorage;
+
+import com.android.adservices.AdServicesCommon;
+import com.android.adservices.LogUtil;
+import com.android.adservices.concurrency.AdServicesExecutors;
+import com.android.adservices.service.common.feature.PrivacySandboxFeatureType;
+import com.android.adservices.service.consent.ConsentConstants;
+import com.android.adservices.service.consent.ConsentManager;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
+
+/**
+ * This class provides an interface to read/write consent data to AppSearch. This is used as the
+ * 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)
+public 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;
+
+ // This is used to convert the current package name belonging to AdExtServices to the
+ // corresponding package name for AdServices.
+ private static final String EXTSERVICES_PACKAGE_NAME_SUBSTRING = "ext.";
+ private static final String CONSENT_DATABASE_NAME = "adservices_consent";
+ private static final String APP_CONSENT_DATABASE_NAME = "adservices_app_consent";
+
+ // Required for allowing AdServices apk access to read consent written by ExtServices module.
+ private String mAdservicesPackageName;
+ private static final String ADSERVICES_SHA =
+ "686d5c450e00ebe600f979300a29234644eade42f24ede07a073f2bc6b94a3a2";
+ private Context mContext;
+
+ private ListenableFuture<AppSearchSession> mConsentSearchSession;
+ private ListenableFuture<AppSearchSession> mAppConsentSearchSession;
+
+ // When reading across APKs, a GlobalSearchSession is needed, hence we use it when reading.
+ private ListenableFuture<GlobalSearchSession> mGlobalSearchSession;
+ private Executor mExecutor = AdServicesExecutors.getBackgroundExecutor();
+
+ private PackageIdentifier mPackageIdentifier;
+ // 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 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)
+ .build());
+ mAppConsentSearchSession =
+ PlatformStorage.createSearchSessionAsync(
+ new PlatformStorage.SearchContext.Builder(
+ mContext, APP_CONSENT_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());
+
+ // 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);
+ mPackageIdentifier =
+ new PackageIdentifier(
+ mAdservicesPackageName, new Signature(ADSERVICES_SHA).toByteArray());
+ }
+
+ /** Get an instance of AppSearchConsentService. */
+ public static AppSearchConsentWorker getInstance(@NonNull Context context) {
+ Objects.requireNonNull(context);
+ return new AppSearchConsentWorker(context);
+ }
+
+ /**
+ * Get the consent for this user ID for this API type, as stored in AppSearch. Returns false if
+ * the database doesn't exist in AppSearch.
+ */
+ public boolean getConsent(@NonNull String apiType) {
+ Objects.requireNonNull(apiType);
+ READ_WRITE_LOCK.readLock().lock();
+ boolean result =
+ AppSearchConsentDao.readConsentData(mGlobalSearchSession, mExecutor, mUid, apiType);
+ READ_WRITE_LOCK.readLock().unlock();
+ return result;
+ }
+
+ /**
+ * 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.
+ */
+ public void setConsent(@NonNull String apiType, @NonNull Boolean consented) {
+ Objects.requireNonNull(apiType);
+ Objects.requireNonNull(consented);
+ READ_WRITE_LOCK.writeLock().lock();
+ // The ID of the row needs to unique per row. For a given user, we store multiple rows, one
+ // per each apiType.
+ AppSearchConsentDao dao =
+ new AppSearchConsentDao(
+ AppSearchConsentDao.getRowId(mUid, apiType),
+ mUid,
+ AppSearchConsentDao.NAMESPACE,
+ apiType,
+ consented.toString());
+ try {
+ dao.writeConsentData(mConsentSearchSession, mPackageIdentifier, mExecutor)
+ .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ LogUtil.e("Failed to write consent to AppSearch ", e);
+ throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE);
+ } finally {
+ READ_WRITE_LOCK.writeLock().unlock();
+ }
+ }
+
+ /**
+ * Get the apps with consent as stored in AppSearch. If no such list was stored, empty list is
+ * returned.
+ */
+ public List<String> getAppsWithConsent(@NonNull String consentType) {
+ Objects.requireNonNull(consentType);
+ READ_WRITE_LOCK.readLock().lock();
+ AppSearchAppConsentDao dao =
+ AppSearchAppConsentDao.readConsentData(
+ mGlobalSearchSession, mExecutor, mUid, consentType);
+ List result = (dao == null || dao.getApps() == null) ? List.of() : dao.getApps();
+ READ_WRITE_LOCK.readLock().unlock();
+ return result;
+ }
+
+ /** Clear app consent data for this user for the given type of consent. */
+ public void clearAppsWithConsent(@NonNull String consentType) {
+ Objects.requireNonNull(consentType);
+ READ_WRITE_LOCK.writeLock().lock();
+ try {
+ AppSearchDao.deleteConsentData(
+ AppSearchAppConsentDao.class,
+ mAppConsentSearchSession,
+ mExecutor,
+ AppSearchAppConsentDao.getRowId(mUid, consentType),
+ AppSearchAppConsentDao.NAMESPACE)
+ .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ LogUtil.e("Failed to delete consent to AppSearch ", e);
+ throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE);
+ } finally {
+ READ_WRITE_LOCK.writeLock().unlock();
+ }
+ }
+
+ /** Adds an app to the list of apps with this consentType for this user. */
+ public boolean addAppWithConsent(@NonNull String consentType, @NonNull String app) {
+ Objects.requireNonNull(consentType);
+ Objects.requireNonNull(app);
+ READ_WRITE_LOCK.writeLock().lock();
+
+ try {
+ // Since AppSearch doesn't support PATCH api, we need to do a {read, modify, write}. See
+ // b/274507022 for details.
+ AppSearchAppConsentDao dao =
+ AppSearchAppConsentDao.readConsentData(
+ mGlobalSearchSession, mExecutor, mUid, consentType);
+ // If there was no such row in the table, create one. Else, update existing one.
+ if (dao == null) {
+ dao =
+ new AppSearchAppConsentDao(
+ AppSearchAppConsentDao.getRowId(mUid, consentType),
+ mUid,
+ AppSearchAppConsentDao.NAMESPACE,
+ consentType,
+ List.of(app));
+ } else {
+ // If this app was already present in the consent list, no need to rewrite.
+ if (dao.getApps() != null && dao.getApps().contains(app)) {
+ return true;
+ }
+ List<String> apps =
+ dao.getApps() != null ? new ArrayList<>(dao.getApps()) : new ArrayList<>();
+ apps.add(app);
+ dao.setApps(apps);
+ }
+ dao.writeConsentData(mAppConsentSearchSession, mPackageIdentifier, mExecutor)
+ .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ LogUtil.e("Failed to write consent to AppSearch ", e);
+ return false;
+ } finally {
+ READ_WRITE_LOCK.writeLock().unlock();
+ }
+ return true;
+ }
+
+ /**
+ * 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.
+ */
+ public void removeAppWithConsent(@NonNull String consentType, @NonNull String app) {
+ Objects.requireNonNull(consentType);
+ Objects.requireNonNull(app);
+ READ_WRITE_LOCK.readLock().lock();
+
+ try {
+ // Since AppSearch doesn't support PATCH api, we need to do a {read, modify, write}. See
+ // b/274507022 for details.
+ AppSearchAppConsentDao dao =
+ AppSearchAppConsentDao.readConsentData(
+ mGlobalSearchSession, mExecutor, mUid, consentType);
+ // If there was no such row in the table, do nothing. Else, update existing one.
+ if (dao == null || dao.getApps() == null || !dao.getApps().contains(app)) {
+ return;
+ }
+ dao.setApps(
+ dao.getApps().stream()
+ .filter(filterApp -> !filterApp.equals(app))
+ .collect(Collectors.toList()));
+ dao.writeConsentData(mAppConsentSearchSession, mPackageIdentifier, mExecutor)
+ .get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException | TimeoutException | ExecutionException e) {
+ LogUtil.e("Failed to write consent to AppSearch ", e);
+ throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE);
+ } finally {
+ READ_WRITE_LOCK.readLock().unlock();
+ }
+ }
+
+ /** Returns whether the beta UX notification was displayed to this user on this device. */
+ public boolean wasNotificationDisplayed() {
+ // TODO(b/263297331): Implement.
+ return false;
+ }
+
+ /** Returns whether the GA UX notification was displayed to this user on this device. */
+ public boolean wasGaUxNotificationDisplayed() {
+ // TODO(b/263297331): Implement.
+ return false;
+ }
+
+ /** Record having shown the beta UX notification to this user on this device. */
+ public void recordNotificationDisplayed() {
+ // TODO(b/263297331): Implement.
+ }
+
+ /** Record having shown the GA UX notification to this user on this device. */
+ public void recordGaUxNotificationDisplayed() {
+ // TODO(b/263297331): Implement.
+ }
+
+ /**
+ * Returns the PrivacySandboxFeature recorded for this user on this device. Possible values are
+ * UNKNOWN, FIRST_CONSENT and RECONSENT.
+ */
+ public PrivacySandboxFeatureType getPrivacySandboxFeature() {
+ // TODO(b/263297331): Implement.
+ return null;
+ }
+
+ /** Record the current privacy sandbox feature. */
+ public void setCurrentPrivacySandboxFeature(PrivacySandboxFeatureType currentFeatureType) {
+ // TODO(b/263297331): Implement.
+ }
+
+ /**
+ * Returns information whether user interacted with consent manually.
+ *
+ * @return true if the user interacted with the consent manually, otherwise false.
+ */
+ public @ConsentManager.UserManualInteraction int getUserManualInteractionWithConsent() {
+ // TODO(b/263297331): Implement.
+ return ConsentManager.NO_MANUAL_INTERACTIONS_RECORDED;
+ }
+
+ /** Saves information to the storage that user interacted with consent manually. */
+ public void recordUserManualInteractionWithConsent(
+ @ConsentManager.UserManualInteraction int interaction) {
+ // TODO(b/263297331): Implement.
+ }
+
+ /** Returns the User Identifier from the CallingUid. */
+ @VisibleForTesting
+ String getUserIdentifierFromBinderCallingUid() {
+ return "" + UserHandle.getUserHandleForUid(Binder.getCallingUid()).getIdentifier();
+ }
+
+ /**
+ * This method returns the package name of the AdServices APK from AdServices apex (T+). On an
+ * S- device, it removes the "ext." substring from the package name.
+ */
+ @VisibleForTesting
+ static String getAdServicesPackageName(Context context) {
+ Intent serviceIntent = new Intent(AdServicesCommon.ACTION_TOPICS_SERVICE);
+ List<ResolveInfo> resolveInfos =
+ context.getPackageManager()
+ .queryIntentServices(
+ serviceIntent,
+ PackageManager.GET_SERVICES
+ | PackageManager.MATCH_SYSTEM_ONLY
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
+ final ServiceInfo serviceInfo =
+ AdServicesCommon.resolveAdServicesService(resolveInfos, serviceIntent.getAction());
+ if (serviceInfo != null) {
+ // Return the AdServices package name based on the current package name.
+ String packageName = serviceInfo.packageName;
+ if (packageName == null || packageName.isEmpty()) {
+ throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE);
+ }
+ return packageName.replace(EXTSERVICES_PACKAGE_NAME_SUBSTRING, "");
+ }
+ // If we don't know the AdServices package name, we can't do a write.
+ throw new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE);
+ }
+}
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 90dd44d..e4f770a 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,8 +16,10 @@
package com.android.adservices.service.appsearch;
+import android.annotation.NonNull;
import android.os.Build;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appsearch.app.AppSearchBatchResult;
import androidx.appsearch.app.AppSearchSession;
@@ -25,6 +27,7 @@
import androidx.appsearch.app.GlobalSearchSession;
import androidx.appsearch.app.PackageIdentifier;
import androidx.appsearch.app.PutDocumentsRequest;
+import androidx.appsearch.app.RemoveByDocumentIdRequest;
import androidx.appsearch.app.SearchResults;
import androidx.appsearch.app.SearchSpec;
import androidx.appsearch.app.SetSchemaRequest;
@@ -38,6 +41,7 @@
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
+import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
@@ -49,9 +53,9 @@
*/
// TODO(b/269798827): Enable for R.
@RequiresApi(Build.VERSION_CODES.S)
-public class AppSearchDao {
+class AppSearchDao {
// Timeout for AppSearch search query in milliseconds.
- private static final int TIMEOUT = 500;
+ private static final int TIMEOUT_MS = 500;
/**
* Iterate over the search results returned for the search query by AppSearch.
@@ -92,31 +96,40 @@
*
* @return the instance of subclass type that was read from AppSearch.
*/
+ @Nullable
protected static <T> T readConsentData(
- Class<T> cls,
- ListenableFuture<GlobalSearchSession> searchSession,
- Executor executor,
- String query) {
- // Query cannot be empty.
- if (query == null || query.isEmpty()) {
+ @NonNull Class<T> cls,
+ @NonNull ListenableFuture<GlobalSearchSession> searchSession,
+ @NonNull Executor executor,
+ @NonNull String namespace,
+ @NonNull String query) {
+ Objects.requireNonNull(cls);
+ Objects.requireNonNull(searchSession);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(namespace);
+
+ // Namespace and Query cannot be empty.
+ if (query == null || query.isEmpty() || namespace.isEmpty()) {
return null;
}
- SearchSpec searchSpec = new SearchSpec.Builder().build();
- ListenableFuture<SearchResults> searchFuture =
- Futures.transform(
- searchSession, session -> session.search(query, searchSpec), executor);
- FluentFuture<T> future =
- FluentFuture.from(searchFuture)
- .transformAsync(
- results -> iterateSearchResults(cls, results, executor), executor)
- .transform(result -> ((T) result), executor);
try {
- return future.get(TIMEOUT, TimeUnit.MILLISECONDS);
+ SearchSpec searchSpec = new SearchSpec.Builder().addFilterNamespaces(namespace).build();
+ ListenableFuture<SearchResults> searchFuture =
+ Futures.transform(
+ searchSession, session -> session.search(query, searchSpec), executor);
+ FluentFuture<T> future =
+ FluentFuture.from(searchFuture)
+ .transformAsync(
+ results -> iterateSearchResults(cls, results, executor),
+ executor)
+ .transform(result -> ((T) result), executor);
+ T result = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ return result;
} catch (ExecutionException | InterruptedException | TimeoutException e) {
LogUtil.e("getConsent() Appsearch lookup failed with: ", e);
- return null;
}
+ return null;
}
/**
@@ -127,10 +140,14 @@
*
* @return the result of the write.
*/
- public FluentFuture<AppSearchBatchResult<String, Void>> writeConsentData(
- ListenableFuture<AppSearchSession> appSearchSession,
- PackageIdentifier packageIdentifier,
- Executor executor) {
+ FluentFuture<AppSearchBatchResult<String, Void>> writeConsentData(
+ @NonNull ListenableFuture<AppSearchSession> appSearchSession,
+ @NonNull PackageIdentifier packageIdentifier,
+ @NonNull Executor executor) {
+ Objects.requireNonNull(appSearchSession);
+ Objects.requireNonNull(packageIdentifier);
+ Objects.requireNonNull(executor);
+
try {
SetSchemaRequest setSchemaRequest =
new SetSchemaRequest.Builder()
@@ -174,4 +191,61 @@
Futures.immediateFailedFuture(
new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE)));
}
+
+ /**
+ * Delete a row from the database.
+ *
+ * @return the result of the delete.
+ */
+ protected static <T> FluentFuture<AppSearchBatchResult<String, Void>> deleteConsentData(
+ @NonNull Class<T> cls,
+ @NonNull ListenableFuture<AppSearchSession> appSearchSession,
+ @NonNull Executor executor,
+ @NonNull String rowId,
+ @NonNull String namespace) {
+ Objects.requireNonNull(cls);
+ Objects.requireNonNull(appSearchSession);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(rowId);
+ Objects.requireNonNull(namespace);
+
+ try {
+ SetSchemaRequest setSchemaRequest =
+ new SetSchemaRequest.Builder().addDocumentClasses(cls).build();
+ RemoveByDocumentIdRequest deleteRequest =
+ new RemoveByDocumentIdRequest.Builder(namespace).addIds(rowId).build();
+ FluentFuture<AppSearchBatchResult<String, Void>> deleteFuture =
+ FluentFuture.from(appSearchSession)
+ .transformAsync(
+ session -> session.setSchemaAsync(setSchemaRequest), executor)
+ .transformAsync(
+ setSchemaResponse -> {
+ // If we get failures in schemaResponse then we cannot try
+ // to write.
+ if (!setSchemaResponse.getMigrationFailures().isEmpty()) {
+ LogUtil.e(
+ "SetSchemaResponse migration failure: "
+ + setSchemaResponse
+ .getMigrationFailures()
+ .get(0));
+ throw new RuntimeException(
+ ConsentConstants
+ .ERROR_MESSAGE_APPSEARCH_FAILURE);
+ }
+ // The database knows about this schemaType and write can
+ // occur.
+ return Futures.transformAsync(
+ appSearchSession,
+ session -> session.removeAsync(deleteRequest),
+ executor);
+ },
+ executor);
+ return deleteFuture;
+ } catch (AppSearchException e) {
+ LogUtil.e("Cannot instantiate AppSearch database: " + e.getMessage());
+ }
+ return FluentFuture.from(
+ Futures.immediateFailedFuture(
+ new RuntimeException(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE)));
+ }
}
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 2a926d9..b729b76 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
@@ -21,25 +21,26 @@
/** ConsentManager related Constants. */
public class ConsentConstants {
- static final String NOTIFICATION_DISPLAYED_ONCE = "NOTIFICATION-DISPLAYED-ONCE";
+ public static final String NOTIFICATION_DISPLAYED_ONCE = "NOTIFICATION-DISPLAYED-ONCE";
- static final String GA_UX_NOTIFICATION_DISPLAYED_ONCE = "GA-UX-NOTIFICATION-DISPLAYED-ONCE";
+ public static final String GA_UX_NOTIFICATION_DISPLAYED_ONCE =
+ "GA-UX-NOTIFICATION-DISPLAYED-ONCE";
- static final String DEFAULT_CONSENT = "DEFAULT_CONSENT";
+ public static final String DEFAULT_CONSENT = "DEFAULT_CONSENT";
- static final String TOPICS_DEFAULT_CONSENT = "TOPICS_DEFAULT_CONSENT";
+ public static final String TOPICS_DEFAULT_CONSENT = "TOPICS_DEFAULT_CONSENT";
- static final String FLEDGE_DEFAULT_CONSENT = "FLEDGE_DEFAULT_CONSENT";
+ public static final String FLEDGE_DEFAULT_CONSENT = "FLEDGE_DEFAULT_CONSENT";
- static final String MEASUREMENT_DEFAULT_CONSENT = "MEASUREMENT_DEFAULT_CONSENT";
+ public static final String MEASUREMENT_DEFAULT_CONSENT = "MEASUREMENT_DEFAULT_CONSENT";
- static final String DEFAULT_AD_ID_STATE = "DEFAULT_AD_ID_STATE";
+ public static final String DEFAULT_AD_ID_STATE = "DEFAULT_AD_ID_STATE";
@VisibleForTesting
static final String MANUAL_INTERACTION_WITH_CONSENT_RECORDED =
"MANUAL_INTERACTION_WITH_CONSENT_RECORDED";
- static final String CONSENT_KEY = "CONSENT";
+ public static final String CONSENT_KEY = "CONSENT";
// Internal datastore version
static final int STORAGE_VERSION = 1;
@@ -53,6 +54,10 @@
// happens again.
static final String SHARED_PREFS_CONSENT = "PPAPI_Consent";
+ // Shared preferences to mark whether consent data from AppSearch has migrated to AdServices.
+ static final String SHARED_PREFS_KEY_APPSEARCH_HAS_MIGRATED =
+ "CONSENT_HAS_MIGRATED_FROM_APPSEARCH";
+
// Shared preferences to mark whether PPAPI consent has been migrated to system server
static final String SHARED_PREFS_KEY_HAS_MIGRATED = "CONSENT_HAS_MIGRATED_TO_SYSTEM_SERVER";
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 3848c56..278f526 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
@@ -42,7 +42,7 @@
import com.android.adservices.data.topics.TopicsTables;
import com.android.adservices.service.Flags;
import com.android.adservices.service.FlagsFactory;
-import com.android.adservices.service.appsearch.AppSearchConsentService;
+import com.android.adservices.service.appsearch.AppSearchConsentManager;
import com.android.adservices.service.common.BackgroundJobsManager;
import com.android.adservices.service.common.feature.PrivacySandboxFeatureType;
import com.android.adservices.service.measurement.MeasurementImpl;
@@ -50,6 +50,7 @@
import com.android.adservices.service.topics.TopicsWorker;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
+import com.android.modules.utils.build.SdkLevel;
import com.google.common.collect.ImmutableList;
@@ -101,7 +102,7 @@
private final AppInstallDao mAppInstallDao;
private final AdServicesManager mAdServicesManager;
private final int mConsentSourceOfTruth;
- private final AppSearchConsentService mAppSearchConsentService;
+ private final AppSearchConsentManager mAppSearchConsentManager;
private static final Object LOCK = new Object();
@@ -116,7 +117,7 @@
@NonNull AppInstallDao appInstallDao,
@NonNull AdServicesManager adServicesManager,
@NonNull BooleanFileDatastore booleanFileDatastore,
- @NonNull AppSearchConsentService appSearchConsentService,
+ @NonNull AppSearchConsentManager appSearchConsentManager,
@NonNull Flags flags,
@Flags.ConsentSourceOfTruth int consentSourceOfTruth) {
Objects.requireNonNull(context);
@@ -134,7 +135,7 @@
}
if (flags.getEnableAppsearchConsentData()) {
- Objects.requireNonNull(appSearchConsentService);
+ Objects.requireNonNull(appSearchConsentManager);
}
mContext = context;
@@ -148,7 +149,7 @@
mAdSelectionEntryDao = adSelectionEntryDao;
mAppInstallDao = appInstallDao;
- mAppSearchConsentService = appSearchConsentService;
+ mAppSearchConsentManager = appSearchConsentManager;
mFlags = flags;
mConsentSourceOfTruth = consentSourceOfTruth;
}
@@ -169,15 +170,26 @@
int consentSourceOfTruth = FlagsFactory.getFlags().getConsentSourceOfTruth();
BooleanFileDatastore datastore = createAndInitializeDataStore(context);
AdServicesManager adServicesManager = AdServicesManager.getInstance(context);
+ AppConsentDao appConsentDao = AppConsentDao.getInstance(context);
handleConsentMigrationIfNeeded(
context, datastore, adServicesManager, consentSourceOfTruth);
+ AppSearchConsentManager appSearchConsentManager = null;
+ if (FlagsFactory.getFlags().getEnableAppsearchConsentData()) {
+ appSearchConsentManager = AppSearchConsentManager.getInstance(context);
+ handleConsentMigrationFromAppSearchIfNeeded(
+ context,
+ datastore,
+ appConsentDao,
+ appSearchConsentManager,
+ adServicesManager);
+ }
if (sConsentManager == null) {
sConsentManager =
new ConsentManager(
context,
TopicsWorker.getInstance(context),
- AppConsentDao.getInstance(context),
+ appConsentDao,
EnrollmentDao.getInstance(context),
MeasurementImpl.getInstance(context),
CustomAudienceDatabase.getInstance(context).customAudienceDao(),
@@ -185,7 +197,7 @@
SharedStorageDatabase.getInstance(context).appInstallDao(),
adServicesManager,
datastore,
- AppSearchConsentService.getInstance(context),
+ appSearchConsentManager,
// TODO(b/260601944): Remove Flag Instance.
FlagsFactory.getFlags(),
consentSourceOfTruth);
@@ -339,8 +351,11 @@
// This is the default for back compat. All consent data is written to and
// read from AppSearch on S- devices.
case Flags.APPSEARCH_ONLY:
- return AdServicesApiConsent.getConsent(
- mAppSearchConsentService.getConsent(ConsentConstants.CONSENT_KEY));
+ if (mFlags.getEnableAppsearchConsentData()) {
+ return AdServicesApiConsent.getConsent(
+ mAppSearchConsentManager.getConsent(
+ ConsentConstants.CONSENT_KEY));
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
return AdServicesApiConsent.REVOKED;
@@ -382,8 +397,11 @@
mAdServicesManager.getConsent(apiType.toConsentApiType());
return AdServicesApiConsent.getConsent(consentParcel.isIsGiven());
case Flags.APPSEARCH_ONLY:
- return AdServicesApiConsent.getConsent(
- mAppSearchConsentService.getConsent(apiType.toPpApiDatastoreKey()));
+ if (mFlags.getEnableAppsearchConsentData()) {
+ return AdServicesApiConsent.getConsent(
+ mAppSearchConsentManager.getConsent(
+ apiType.toPpApiDatastoreKey()));
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
return AdServicesApiConsent.REVOKED;
@@ -482,6 +500,9 @@
.map(App::create)
.collect(Collectors.toList()));
case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ return mAppSearchConsentManager.getKnownAppsWithConsent();
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
return ImmutableList.of();
@@ -523,6 +544,9 @@
.map(App::create)
.collect(Collectors.toList()));
case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ return mAppSearchConsentManager.getAppsWithRevokedConsent();
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
return ImmutableList.of();
@@ -563,6 +587,10 @@
true);
break;
case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ mAppSearchConsentManager.revokeConsentForApp(app);
+ break;
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
}
@@ -610,6 +638,10 @@
false);
break;
case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ mAppSearchConsentManager.restoreConsentForApp(app);
+ break;
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
}
@@ -641,6 +673,10 @@
mAdServicesManager.clearAllAppConsentData();
break;
case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ mAppSearchConsentManager.clearAllAppConsentData();
+ break;
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
}
@@ -679,6 +715,10 @@
mAdServicesManager.clearKnownAppsWithConsent();
break;
case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ mAppSearchConsentManager.clearKnownAppsWithConsent();
+ break;
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
}
@@ -738,6 +778,9 @@
return mAdServicesManager.isConsentRevokedForApp(
packageName, mAppConsentDao.getUidForInstalledPackageName(packageName));
case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ return mAppSearchConsentManager.isFledgeConsentRevokedForApp(packageName);
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
return true;
@@ -801,6 +844,10 @@
mAppConsentDao.getUidForInstalledPackageName(packageName),
false);
case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ return mAppSearchConsentManager
+ .isFledgeConsentRevokedForAppAfterSettingFledgeUse(packageName);
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
return true;
@@ -885,6 +932,12 @@
mAdServicesManager.clearConsentForUninstalledApp(packageName, packageUid);
break;
case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ // AppSearch is written only for S- where we don't have permission to
+ // receive UID info when package is uninstalled, so clear for all.
+ mAppSearchConsentManager.clearConsentForUninstalledApp(packageName);
+ break;
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
}
@@ -925,6 +978,11 @@
packageName);
}
break;
+ case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ mAppSearchConsentManager.clearConsentForUninstalledApp(packageName);
+ break;
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
}
@@ -968,7 +1026,10 @@
mAdServicesManager.recordNotificationDisplayed();
break;
case Flags.APPSEARCH_ONLY:
- break;
+ if (mFlags.getEnableAppsearchConsentData()) {
+ mAppSearchConsentManager.recordNotificationDisplayed();
+ break;
+ }
default:
throw new RuntimeException(
ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
@@ -998,6 +1059,9 @@
case Flags.PPAPI_AND_SYSTEM_SERVER:
return mAdServicesManager.wasNotificationDisplayed();
case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ return mAppSearchConsentManager.wasNotificationDisplayed();
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
return false;
@@ -1031,6 +1095,11 @@
mDatastore.put(ConsentConstants.GA_UX_NOTIFICATION_DISPLAYED_ONCE, true);
mAdServicesManager.recordGaUxNotificationDisplayed();
break;
+ case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ mAppSearchConsentManager.recordGaUxNotificationDisplayed();
+ break;
+ }
default:
throw new RuntimeException(
ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
@@ -1059,6 +1128,10 @@
// Intentional fallthrough
case Flags.PPAPI_AND_SYSTEM_SERVER:
return mAdServicesManager.wasGaUxNotificationDisplayed();
+ case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ return mAppSearchConsentManager.wasGaUxNotificationDisplayed();
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
return false;
@@ -1088,6 +1161,11 @@
// Intentional fallthrough
case Flags.PPAPI_AND_SYSTEM_SERVER:
return mAdServicesManager.getDefaultConsent();
+ case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ return mAppSearchConsentManager.getConsent(
+ ConsentConstants.DEFAULT_CONSENT);
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
return false;
@@ -1117,6 +1195,11 @@
// Intentional fallthrough
case Flags.PPAPI_AND_SYSTEM_SERVER:
return mAdServicesManager.getTopicsDefaultConsent();
+ case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ return mAppSearchConsentManager.getConsent(
+ ConsentConstants.TOPICS_DEFAULT_CONSENT);
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
return false;
@@ -1146,6 +1229,11 @@
// Intentional fallthrough
case Flags.PPAPI_AND_SYSTEM_SERVER:
return mAdServicesManager.getFledgeDefaultConsent();
+ case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ return mAppSearchConsentManager.getConsent(
+ ConsentConstants.FLEDGE_DEFAULT_CONSENT);
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
return false;
@@ -1175,6 +1263,9 @@
// Intentional fallthrough
case Flags.PPAPI_AND_SYSTEM_SERVER:
return mAdServicesManager.getMeasurementDefaultConsent();
+ case Flags.APPSEARCH_ONLY:
+ return mAppSearchConsentManager.getConsent(
+ ConsentConstants.MEASUREMENT_DEFAULT_CONSENT);
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
return false;
@@ -1204,6 +1295,11 @@
// Intentional fallthrough
case Flags.PPAPI_AND_SYSTEM_SERVER:
return mAdServicesManager.getDefaultAdIdState();
+ case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ return mAppSearchConsentManager.getConsent(
+ ConsentConstants.DEFAULT_AD_ID_STATE);
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
return false;
@@ -1235,6 +1331,12 @@
mDatastore.put(ConsentConstants.DEFAULT_CONSENT, defaultConsent);
mAdServicesManager.recordDefaultConsent(defaultConsent);
break;
+ case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ mAppSearchConsentManager.setConsent(
+ ConsentConstants.DEFAULT_CONSENT, defaultConsent);
+ break;
+ }
default:
throw new RuntimeException(
ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
@@ -1265,6 +1367,12 @@
mDatastore.put(ConsentConstants.TOPICS_DEFAULT_CONSENT, defaultConsent);
mAdServicesManager.recordTopicsDefaultConsent(defaultConsent);
break;
+ case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ mAppSearchConsentManager.setConsent(
+ ConsentConstants.TOPICS_DEFAULT_CONSENT, defaultConsent);
+ break;
+ }
default:
throw new RuntimeException(
ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
@@ -1295,6 +1403,12 @@
mDatastore.put(ConsentConstants.FLEDGE_DEFAULT_CONSENT, defaultConsent);
mAdServicesManager.recordFledgeDefaultConsent(defaultConsent);
break;
+ case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ mAppSearchConsentManager.setConsent(
+ ConsentConstants.FLEDGE_DEFAULT_CONSENT, defaultConsent);
+ break;
+ }
default:
throw new RuntimeException(
ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
@@ -1327,6 +1441,12 @@
ConsentConstants.MEASUREMENT_DEFAULT_CONSENT, defaultConsent);
mAdServicesManager.recordMeasurementDefaultConsent(defaultConsent);
break;
+ case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ mAppSearchConsentManager.setConsent(
+ ConsentConstants.MEASUREMENT_DEFAULT_CONSENT, defaultConsent);
+ break;
+ }
default:
throw new RuntimeException(
ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
@@ -1357,6 +1477,12 @@
mDatastore.put(ConsentConstants.DEFAULT_AD_ID_STATE, defaultAdIdState);
mAdServicesManager.recordDefaultAdIdState(defaultAdIdState);
break;
+ case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ mAppSearchConsentManager.setConsent(
+ ConsentConstants.DEFAULT_AD_ID_STATE, defaultAdIdState);
+ break;
+ }
default:
throw new RuntimeException(
ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
@@ -1403,6 +1529,12 @@
mAdServicesManager.setCurrentPrivacySandboxFeature(
currentFeatureType.name());
break;
+ case Flags.APPSEARCH_ONLY:
+ if (mFlags.getEnableAppsearchConsentData()) {
+ mAppSearchConsentManager.setCurrentPrivacySandboxFeature(
+ currentFeatureType);
+ break;
+ }
default:
throw new RuntimeException(
ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
@@ -1429,7 +1561,11 @@
mAdServicesManager.recordUserManualInteractionWithConsent(interaction);
break;
case Flags.APPSEARCH_ONLY:
- break;
+ if (mFlags.getEnableAppsearchConsentData()) {
+ mAppSearchConsentManager.recordUserManualInteractionWithConsent(
+ interaction);
+ break;
+ }
default:
throw new RuntimeException(
ConsentConstants.MANUAL_INTERACTION_WITH_CONSENT_RECORDED);
@@ -1471,7 +1607,9 @@
}
break;
case Flags.APPSEARCH_ONLY:
- break;
+ if (mFlags.getEnableAppsearchConsentData()) {
+ return mAppSearchConsentManager.getCurrentPrivacySandboxFeature();
+ }
default:
LogUtil.e(ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
return PrivacySandboxFeatureType.PRIVACY_SANDBOX_UNSUPPORTED;
@@ -1526,7 +1664,9 @@
case Flags.PPAPI_AND_SYSTEM_SERVER:
return mAdServicesManager.getUserManualInteractionWithConsent();
case Flags.APPSEARCH_ONLY:
- return UNKNOWN;
+ if (mFlags.getEnableAppsearchConsentData()) {
+ return mAppSearchConsentManager.getUserManualInteractionWithConsent();
+ }
default:
LogUtil.e(ConsentConstants.MANUAL_INTERACTION_WITH_CONSENT_RECORDED);
return UNKNOWN;
@@ -1603,6 +1743,8 @@
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;
}
@@ -1808,8 +1950,11 @@
setConsentToSystemServer(mAdServicesManager, isGiven);
break;
case Flags.APPSEARCH_ONLY:
- mAppSearchConsentService.setConsent(ConsentConstants.CONSENT_KEY, isGiven);
- break;
+ if (mFlags.getEnableAppsearchConsentData()) {
+ mAppSearchConsentManager.setConsent(
+ ConsentConstants.CONSENT_KEY, isGiven);
+ break;
+ }
default:
throw new RuntimeException(
ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
@@ -1841,8 +1986,11 @@
setAggregatedConsentToPpApi();
break;
case Flags.APPSEARCH_ONLY:
- mAppSearchConsentService.setConsent(apiType.toPpApiDatastoreKey(), isGiven);
- break;
+ if (mFlags.getEnableAppsearchConsentData()) {
+ mAppSearchConsentManager.setConsent(
+ apiType.toPpApiDatastoreKey(), isGiven);
+ break;
+ }
default:
throw new RuntimeException(
ConsentConstants.ERROR_MESSAGE_INVALID_CONSENT_SOURCE_OF_TRUTH);
@@ -1854,6 +2002,28 @@
}
/**
+ * 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 AppSearchConsentManager appSearchConsentManager,
+ @NonNull AdServicesManager adServicesManager) {
+ Objects.requireNonNull(context);
+ Objects.requireNonNull(datastore);
+ Objects.requireNonNull(appConsentDao);
+ Objects.requireNonNull(appSearchConsentManager);
+ if (SdkLevel.isAtLeastT()) {
+ Objects.requireNonNull(adServicesManager);
+ }
+ // TODO(b/263297331): Implement migration of AppSearch data to AdServices.
+ }
+
+ /**
* 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.
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/PhFlagsTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/PhFlagsTest.java
index 5e74af9..9792bfa 100644
--- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/PhFlagsTest.java
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/PhFlagsTest.java
@@ -32,7 +32,6 @@
import static com.android.adservices.service.Flags.COMPAT_LOGGING_KILL_SWITCH;
import static com.android.adservices.service.Flags.DEFAULT_BLOCKED_TOPICS_SOURCE_OF_TRUTH;
import static com.android.adservices.service.Flags.DEFAULT_CLASSIFIER_TYPE;
-import static com.android.adservices.service.Flags.DEFAULT_CONSENT_SOURCE_OF_TRUTH;
import static com.android.adservices.service.Flags.DEFAULT_MEASUREMENT_DEBUG_JOIN_KEY_ENROLLMENT_ALLOWLIST;
import static com.android.adservices.service.Flags.DEFAULT_MEASUREMENT_DEBUG_JOIN_KEY_HASH_LIMIT;
import static com.android.adservices.service.Flags.DEFAULT_NOTIFICATION_DISMISSED_ON_CLICK;
@@ -167,6 +166,7 @@
import static com.android.adservices.service.Flags.MEASUREMENT_REGISTRATION_JOB_TRIGGER_MAX_DELAY_MS;
import static com.android.adservices.service.Flags.MEASUREMENT_ROLLBACK_DELETION_KILL_SWITCH;
import static com.android.adservices.service.Flags.NUMBER_OF_EPOCHS_TO_KEEP_IN_HISTORY;
+import static com.android.adservices.service.Flags.PPAPI_AND_SYSTEM_SERVER;
import static com.android.adservices.service.Flags.PPAPI_APP_ALLOW_LIST;
import static com.android.adservices.service.Flags.PPAPI_APP_SIGNATURE_ALLOW_LIST;
import static com.android.adservices.service.Flags.PPAPI_ONLY;
@@ -369,6 +369,7 @@
import com.google.common.collect.ImmutableList;
+import org.junit.Assume;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.MockitoSession;
@@ -4561,9 +4562,23 @@
}
@Test
+ public void testDefaultConsentSourceOfTruth_isAtLeastT() {
+ Assume.assumeTrue(SdkLevel.isAtLeastT());
+ // On T+, default is PPAPI_AND_SYSTEM_SERVER.
+ assertThat(Flags.DEFAULT_CONSENT_SOURCE_OF_TRUTH).isEqualTo(PPAPI_AND_SYSTEM_SERVER);
+ }
+
+ @Test
+ public void testDefaultConsentSourceOfTruth_isS() {
+ Assume.assumeFalse(SdkLevel.isAtLeastT());
+ // On T+, default is PPAPI_AND_SYSTEM_SERVER.
+ assertThat(Flags.DEFAULT_CONSENT_SOURCE_OF_TRUTH).isEqualTo(PPAPI_ONLY);
+ }
+
+ @Test
public void testGetConsentSourceOfTruth() {
assertThat(FlagsFactory.getFlags().getConsentSourceOfTruth())
- .isEqualTo(DEFAULT_CONSENT_SOURCE_OF_TRUTH);
+ .isEqualTo(Flags.DEFAULT_CONSENT_SOURCE_OF_TRUTH);
final int phOverridingValue = PPAPI_ONLY;
DeviceConfig.setProperty(
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchAppConsentDaoTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchAppConsentDaoTest.java
new file mode 100644
index 0000000..2d74cfd
--- /dev/null
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchAppConsentDaoTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.appsearch;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+@SmallTest
+public class AppSearchAppConsentDaoTest {
+ private static final String ID = "1";
+ private static final String ID2 = "2";
+ private static final String NAMESPACE = "consent";
+ private static final List<String> APPS =
+ ImmutableList.of(ApplicationProvider.getApplicationContext().getPackageName());
+ private MockitoSession mStaticMockSession;
+
+ @Before
+ public void setup() {
+ mStaticMockSession =
+ ExtendedMockito.mockitoSession()
+ .mockStatic(AppSearchDao.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ }
+
+ @After
+ public void teardown() {
+ if (mStaticMockSession != null) {
+ mStaticMockSession.finishMocking();
+ }
+ }
+
+ @Test
+ public void testToString() {
+ AppSearchAppConsentDao dao =
+ new AppSearchAppConsentDao(
+ ID, ID, NAMESPACE, AppSearchAppConsentDao.APPS_WITH_CONSENT, APPS);
+ assertThat(dao.toString())
+ .isEqualTo(
+ "id="
+ + ID
+ + "; userId="
+ + ID
+ + "; consentType="
+ + AppSearchAppConsentDao.APPS_WITH_CONSENT
+ + "; namespace="
+ + NAMESPACE
+ + "; apps="
+ + Arrays.toString(APPS.toArray()));
+ }
+
+ @Test
+ public void testEquals() {
+ AppSearchAppConsentDao dao1 =
+ new AppSearchAppConsentDao(
+ ID, ID, NAMESPACE, AppSearchAppConsentDao.APPS_WITH_CONSENT, APPS);
+ AppSearchAppConsentDao dao2 =
+ new AppSearchAppConsentDao(
+ ID, ID, NAMESPACE, AppSearchAppConsentDao.APPS_WITH_CONSENT, APPS);
+ AppSearchAppConsentDao dao3 =
+ new AppSearchAppConsentDao(
+ ID, ID, NAMESPACE, AppSearchAppConsentDao.APPS_WITH_REVOKED_CONSENT, APPS);
+ assertThat(dao1.equals(dao2)).isTrue();
+ assertThat(dao1.equals(dao3)).isFalse();
+ assertThat(dao2.equals(dao3)).isFalse();
+ }
+
+ @Test
+ public void testGetQuery() {
+ String expected =
+ "userId:" + ID + " " + "consentType:" + AppSearchAppConsentDao.APPS_WITH_CONSENT;
+ assertThat(AppSearchAppConsentDao.getQuery(ID, AppSearchAppConsentDao.APPS_WITH_CONSENT))
+ .isEqualTo(expected);
+ }
+
+ @Test
+ public void testGetRowId() {
+ String consentType = AppSearchAppConsentDao.APPS_WITH_CONSENT;
+ String expected = ID + "_" + consentType;
+ assertThat(AppSearchAppConsentDao.getRowId(ID, consentType)).isEqualTo(expected);
+ }
+
+ @Test
+ public void testReadConsentData_null() {
+ String consentType = AppSearchAppConsentDao.APPS_WITH_REVOKED_CONSENT;
+ ListenableFuture mockSearchSession = Mockito.mock(ListenableFuture.class);
+ Executor mockExecutor = Mockito.mock(Executor.class);
+ ExtendedMockito.doReturn(null)
+ .when(() -> AppSearchDao.readConsentData(any(), any(), any(), any(), any()));
+ AppSearchAppConsentDao result =
+ AppSearchAppConsentDao.readConsentData(
+ mockSearchSession, mockExecutor, ID, consentType);
+ assertThat(result).isNull();
+ }
+
+ @Test
+ public void testReadConsentData() {
+ ListenableFuture mockSearchSession = Mockito.mock(ListenableFuture.class);
+ Executor mockExecutor = Mockito.mock(Executor.class);
+
+ String consentType = AppSearchAppConsentDao.APPS_WITH_CONSENT;
+ String query = "userId:" + ID + " " + "consentType:" + consentType;
+ AppSearchAppConsentDao dao = Mockito.mock(AppSearchAppConsentDao.class);
+ ExtendedMockito.doReturn(dao)
+ .when(() -> AppSearchDao.readConsentData(any(), any(), any(), any(), eq(query)));
+ AppSearchAppConsentDao result =
+ AppSearchAppConsentDao.readConsentData(
+ mockSearchSession, mockExecutor, ID, consentType);
+ assertThat(result).isEqualTo(dao);
+
+ // Confirm that the right value is returned even when it is true.
+ String consentType2 = AppSearchAppConsentDao.APPS_WITH_REVOKED_CONSENT;
+ String query2 = "userId:" + ID2 + " " + "consentType:" + consentType2;
+ ExtendedMockito.doReturn(dao)
+ .when(() -> AppSearchDao.readConsentData(any(), any(), any(), any(), eq(query2)));
+ AppSearchAppConsentDao result2 =
+ AppSearchAppConsentDao.readConsentData(
+ mockSearchSession, mockExecutor, ID2, consentType2);
+ assertThat(result2).isEqualTo(dao);
+ }
+}
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchConsentDaoTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchConsentDaoTest.java
index 1543bf3..5dd0c6a 100644
--- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchConsentDaoTest.java
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchConsentDaoTest.java
@@ -97,11 +97,17 @@
}
@Test
+ public void testGetRowId() {
+ String expected = ID + "_" + API_TYPE;
+ assertThat(AppSearchConsentDao.getRowId(ID, API_TYPE)).isEqualTo(expected);
+ }
+
+ @Test
public void testReadConsentData_null() {
ListenableFuture mockSearchSession = Mockito.mock(ListenableFuture.class);
Executor mockExecutor = Mockito.mock(Executor.class);
ExtendedMockito.doReturn(null)
- .when(() -> AppSearchDao.readConsentData(any(), any(), any(), any()));
+ .when(() -> AppSearchDao.readConsentData(any(), any(), any(), any(), any()));
boolean result =
AppSearchConsentDao.readConsentData(mockSearchSession, mockExecutor, ID, API_TYPE);
assertThat(result).isFalse();
@@ -116,7 +122,7 @@
AppSearchConsentDao dao = Mockito.mock(AppSearchConsentDao.class);
Mockito.when(dao.isConsented()).thenReturn(false);
ExtendedMockito.doReturn(dao)
- .when(() -> AppSearchDao.readConsentData(any(), any(), any(), eq(query)));
+ .when(() -> AppSearchDao.readConsentData(any(), any(), any(), any(), eq(query)));
boolean result =
AppSearchConsentDao.readConsentData(mockSearchSession, mockExecutor, ID, API_TYPE);
assertThat(result).isFalse();
@@ -125,7 +131,7 @@
String query2 = "userId:" + ID2 + " " + "apiType:" + API_TYPE2;
Mockito.when(dao.isConsented()).thenReturn(true);
ExtendedMockito.doReturn(dao)
- .when(() -> AppSearchDao.readConsentData(any(), any(), any(), eq(query2)));
+ .when(() -> AppSearchDao.readConsentData(any(), any(), any(), any(), eq(query2)));
boolean result2 =
AppSearchConsentDao.readConsentData(mockSearchSession, mockExecutor, ID, API_TYPE);
assertThat(result2).isTrue();
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchConsentManagerTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchConsentManagerTest.java
new file mode 100644
index 0000000..6fd5162
--- /dev/null
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchConsentManagerTest.java
@@ -0,0 +1,287 @@
+/*
+ * 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.appsearch;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import com.android.adservices.service.common.compat.PackageManagerCompatUtils;
+import com.android.adservices.service.common.feature.PrivacySandboxFeatureType;
+import com.android.adservices.service.consent.AdServicesApiType;
+import com.android.adservices.service.consent.App;
+import com.android.adservices.service.consent.ConsentManager;
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.util.List;
+
+@SmallTest
+public class AppSearchConsentManagerTest {
+ private Context mContext = ApplicationProvider.getApplicationContext();
+ private MockitoSession mStaticMockSession;
+ @Mock private AppSearchConsentWorker mAppSearchConsentWorker;
+ private AppSearchConsentManager mAppSearchConsentManager;
+ private static final String API_TYPE = AdServicesApiType.TOPICS.toPpApiDatastoreKey();
+ private static final String PACKAGE_NAME1 = "foo.bar.one";
+ private static final String PACKAGE_NAME2 = "foo.bar.two";
+ private static final String PACKAGE_NAME3 = "foo.bar.three";
+
+ @Before
+ public void setup() {
+ mStaticMockSession =
+ ExtendedMockito.mockitoSession()
+ .mockStatic(AppSearchConsentWorker.class)
+ .mockStatic(PackageManagerCompatUtils.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ ExtendedMockito.doReturn(mAppSearchConsentWorker)
+ .when(() -> AppSearchConsentWorker.getInstance(mContext));
+ mAppSearchConsentManager = AppSearchConsentManager.getInstance(mContext);
+ ApplicationInfo app1 = new ApplicationInfo();
+ app1.packageName = PACKAGE_NAME1;
+ ApplicationInfo app2 = new ApplicationInfo();
+ app2.packageName = PACKAGE_NAME2;
+ ApplicationInfo app3 = new ApplicationInfo();
+ app3.packageName = PACKAGE_NAME3;
+ ExtendedMockito.doReturn(List.of(app1, app2, app3))
+ .when(() -> PackageManagerCompatUtils.getInstalledApplications(any(), anyInt()));
+ }
+
+ @After
+ public void teardown() {
+ if (mStaticMockSession != null) {
+ mStaticMockSession.finishMocking();
+ }
+ }
+
+ @Test
+ public void testGetConsent() {
+ when(mAppSearchConsentWorker.getConsent(API_TYPE)).thenReturn(false);
+ assertThat(mAppSearchConsentManager.getConsent(API_TYPE)).isEqualTo(false);
+
+ when(mAppSearchConsentWorker.getConsent(API_TYPE)).thenReturn(true);
+ assertThat(mAppSearchConsentManager.getConsent(API_TYPE)).isEqualTo(true);
+ }
+
+ @Test
+ public void testSetConsent() {
+ mAppSearchConsentManager.setConsent(API_TYPE, true);
+ verify(mAppSearchConsentWorker).setConsent(API_TYPE, true);
+
+ mAppSearchConsentManager.setConsent(API_TYPE, false);
+ verify(mAppSearchConsentWorker).setConsent(API_TYPE, false);
+ }
+
+ @Test
+ public void testKnownAppsWithConsent() {
+ String consentType = AppSearchAppConsentDao.APPS_WITH_CONSENT;
+ when(mAppSearchConsentWorker.getAppsWithConsent(eq(consentType)))
+ .thenReturn(List.of(PACKAGE_NAME1, PACKAGE_NAME2));
+ List<App> result = mAppSearchConsentManager.getKnownAppsWithConsent();
+ assertThat(result.size()).isEqualTo(2);
+
+ String package1 = result.get(0).getPackageName();
+ String package2 = result.get(1).getPackageName();
+ assertThat(package1.equals(PACKAGE_NAME1) || package2.equals(PACKAGE_NAME1)).isTrue();
+ assertThat(package1.equals(PACKAGE_NAME2) || package2.equals(PACKAGE_NAME2)).isTrue();
+ }
+
+ @Test
+ public void testAppsWithRevokedConsent() {
+ String consentType = AppSearchAppConsentDao.APPS_WITH_REVOKED_CONSENT;
+ when(mAppSearchConsentWorker.getAppsWithConsent(eq(consentType)))
+ .thenReturn(List.of(PACKAGE_NAME1, PACKAGE_NAME2));
+ List<App> result = mAppSearchConsentManager.getAppsWithRevokedConsent();
+ assertThat(result.size()).isEqualTo(2);
+
+ String package1 = result.get(0).getPackageName();
+ String package2 = result.get(1).getPackageName();
+ assertThat(package1.equals(PACKAGE_NAME1) || package2.equals(PACKAGE_NAME1)).isTrue();
+ assertThat(package1.equals(PACKAGE_NAME2) || package2.equals(PACKAGE_NAME2)).isTrue();
+ }
+
+ @Test
+ public void testRevokeConsentForApp() {
+ App app = App.create(PACKAGE_NAME1);
+ mAppSearchConsentManager.revokeConsentForApp(app);
+ verify(mAppSearchConsentWorker)
+ .addAppWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_REVOKED_CONSENT, app.getPackageName());
+ verify(mAppSearchConsentWorker)
+ .removeAppWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_CONSENT, app.getPackageName());
+ }
+
+ @Test
+ public void testRestoreConsentForApp() {
+ App app = App.create(PACKAGE_NAME1);
+ mAppSearchConsentManager.restoreConsentForApp(app);
+ verify(mAppSearchConsentWorker)
+ .addAppWithConsent(AppSearchAppConsentDao.APPS_WITH_CONSENT, app.getPackageName());
+ verify(mAppSearchConsentWorker)
+ .removeAppWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_REVOKED_CONSENT, app.getPackageName());
+ }
+
+ @Test
+ public void testClearAllAppConsentData() {
+ mAppSearchConsentManager.clearAllAppConsentData();
+ verify(mAppSearchConsentWorker)
+ .clearAppsWithConsent(AppSearchAppConsentDao.APPS_WITH_CONSENT);
+ verify(mAppSearchConsentWorker)
+ .clearAppsWithConsent(AppSearchAppConsentDao.APPS_WITH_REVOKED_CONSENT);
+ }
+
+ @Test
+ public void testClearKnownAppsWithConsent() throws Exception {
+ mAppSearchConsentManager.clearKnownAppsWithConsent();
+ verify(mAppSearchConsentWorker)
+ .clearAppsWithConsent(AppSearchAppConsentDao.APPS_WITH_CONSENT);
+ }
+
+ @Test
+ public void testIsFledgeConsentRevokedForApp_consented() {
+ when(mAppSearchConsentWorker.getAppsWithConsent(AppSearchAppConsentDao.APPS_WITH_CONSENT))
+ .thenReturn(List.of(PACKAGE_NAME1));
+ when(mAppSearchConsentWorker.getAppsWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_REVOKED_CONSENT))
+ .thenReturn(List.of());
+ assertThat(mAppSearchConsentManager.isFledgeConsentRevokedForApp(PACKAGE_NAME1)).isFalse();
+ }
+
+ @Test
+ public void testIsFledgeConsentRevokedForApp_revoked() {
+ when(mAppSearchConsentWorker.getAppsWithConsent(AppSearchAppConsentDao.APPS_WITH_CONSENT))
+ .thenReturn(List.of(PACKAGE_NAME1));
+ when(mAppSearchConsentWorker.getAppsWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_REVOKED_CONSENT))
+ .thenReturn(List.of(PACKAGE_NAME1));
+ assertThat(mAppSearchConsentManager.isFledgeConsentRevokedForApp(PACKAGE_NAME1)).isTrue();
+ }
+
+ @Test
+ public void testIsFledgeConsentRevokedForAppAfterSettingFledgeUse() {
+ when(mAppSearchConsentWorker.addAppWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_CONSENT, PACKAGE_NAME1))
+ .thenReturn(true);
+ assertThat(
+ mAppSearchConsentManager.isFledgeConsentRevokedForAppAfterSettingFledgeUse(
+ PACKAGE_NAME1))
+ .isFalse();
+ }
+
+ @Test
+ public void testIsFledgeConsentRevokedForAppAfterSettingFledgeUse_revoked() {
+ when(mAppSearchConsentWorker.addAppWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_CONSENT, PACKAGE_NAME2))
+ .thenReturn(false);
+ assertThat(
+ mAppSearchConsentManager.isFledgeConsentRevokedForAppAfterSettingFledgeUse(
+ PACKAGE_NAME2))
+ .isTrue();
+ }
+
+ @Test
+ public void testClearConsentForUninstalledApp() {
+ mAppSearchConsentManager.clearConsentForUninstalledApp(PACKAGE_NAME1);
+ verify(mAppSearchConsentWorker)
+ .removeAppWithConsent(
+ AppSearchAppConsentDao.APPS_WITH_REVOKED_CONSENT, PACKAGE_NAME1);
+ verify(mAppSearchConsentWorker)
+ .removeAppWithConsent(AppSearchAppConsentDao.APPS_WITH_CONSENT, PACKAGE_NAME1);
+ }
+
+ @Test
+ public void testRecordNotificationDisplayed() {
+ mAppSearchConsentManager.recordNotificationDisplayed();
+ verify(mAppSearchConsentWorker).recordNotificationDisplayed();
+ }
+
+ @Test
+ public void testRecordGaUxNotificationDisplayed() {
+ mAppSearchConsentManager.recordGaUxNotificationDisplayed();
+ verify(mAppSearchConsentWorker).recordGaUxNotificationDisplayed();
+ }
+
+ @Test
+ public void testWasNotificationDisplayed() {
+ when(mAppSearchConsentWorker.wasNotificationDisplayed()).thenReturn(false);
+ assertThat(mAppSearchConsentManager.wasNotificationDisplayed()).isFalse();
+ verify(mAppSearchConsentWorker).wasNotificationDisplayed();
+ }
+
+ @Test
+ public void testWasGaUxNotificationDisplayed() {
+ when(mAppSearchConsentWorker.wasGaUxNotificationDisplayed()).thenReturn(false);
+ assertThat(mAppSearchConsentManager.wasGaUxNotificationDisplayed()).isFalse();
+ verify(mAppSearchConsentWorker).wasGaUxNotificationDisplayed();
+ }
+
+ @Test
+ public void testGetCurrentPrivacySandboxFeature() {
+ when(mAppSearchConsentWorker.getPrivacySandboxFeature())
+ .thenReturn(PrivacySandboxFeatureType.PRIVACY_SANDBOX_FIRST_CONSENT);
+ assertThat(mAppSearchConsentManager.getCurrentPrivacySandboxFeature())
+ .isEqualTo(PrivacySandboxFeatureType.PRIVACY_SANDBOX_FIRST_CONSENT);
+ verify(mAppSearchConsentWorker).getPrivacySandboxFeature();
+ }
+
+ @Test
+ public void testSetCurrentPrivacySandboxFeature() {
+ mAppSearchConsentManager.setCurrentPrivacySandboxFeature(
+ PrivacySandboxFeatureType.PRIVACY_SANDBOX_RECONSENT);
+ verify(mAppSearchConsentWorker)
+ .setCurrentPrivacySandboxFeature(
+ PrivacySandboxFeatureType.PRIVACY_SANDBOX_RECONSENT);
+ }
+
+ @Test
+ public void testGetUserManualInteractionsWithConsent() {
+ when(mAppSearchConsentWorker.getUserManualInteractionWithConsent())
+ .thenReturn(ConsentManager.MANUAL_INTERACTIONS_RECORDED);
+ assertThat(mAppSearchConsentManager.getUserManualInteractionWithConsent())
+ .isEqualTo(ConsentManager.MANUAL_INTERACTIONS_RECORDED);
+ verify(mAppSearchConsentWorker).getUserManualInteractionWithConsent();
+ }
+
+ @Test
+ public void testRecordUserManualInteractionWithConsent() {
+ mAppSearchConsentManager.recordUserManualInteractionWithConsent(
+ ConsentManager.NO_MANUAL_INTERACTIONS_RECORDED);
+ verify(mAppSearchConsentWorker)
+ .recordUserManualInteractionWithConsent(
+ ConsentManager.NO_MANUAL_INTERACTIONS_RECORDED);
+ }
+}
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchConsentServiceTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchConsentServiceTest.java
deleted file mode 100644
index e5bb00f..0000000
--- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchConsentServiceTest.java
+++ /dev/null
@@ -1,266 +0,0 @@
-/*
- * 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.appsearch;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.atMost;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.os.Binder;
-import android.os.UserHandle;
-
-import androidx.appsearch.app.AppSearchBatchResult;
-import androidx.appsearch.app.AppSearchResult;
-import androidx.appsearch.app.AppSearchSession;
-import androidx.appsearch.app.SetSchemaRequest;
-import androidx.appsearch.app.SetSchemaResponse;
-import androidx.appsearch.platformstorage.PlatformStorage;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.filters.SmallTest;
-
-import com.android.adservices.AdServicesCommon;
-import com.android.adservices.service.consent.ConsentConstants;
-import com.android.dx.mockito.inline.extended.ExtendedMockito;
-
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-
-import org.junit.Test;
-import org.mockito.Mockito;
-import org.mockito.MockitoSession;
-import org.mockito.quality.Strictness;
-
-import java.util.List;
-
-@SmallTest
-public class AppSearchConsentServiceTest {
- private Context mContext = ApplicationProvider.getApplicationContext();
- private static final String ADSERVICES_PACKAGE_NAME = "com.android.adservices.api";
- private static final String ADEXTSERVICES_PACKAGE_NAME = "com.android.ext.adservices.api";
- private static final String API_TYPE = "CONSENT-TOPICS";
- private static final Boolean CONSENTED = true;
- private static final String TEST = "test";
- private static final int UID = 55;
-
- @Test
- public void testGetConsent() {
- MockitoSession staticMockSessionLocal = null;
- try {
- staticMockSessionLocal =
- ExtendedMockito.mockitoSession()
- .mockStatic(AppSearchConsentDao.class)
- .strictness(Strictness.LENIENT)
- .initMocks(this)
- .startMocking();
- ExtendedMockito.doReturn(false)
- .when(
- () ->
- AppSearchConsentDao.readConsentData(
- /* globalSearchSession= */ any(ListenableFuture.class),
- /* executor= */ any(),
- /* userId= */ any(),
- eq(API_TYPE)));
- boolean result = AppSearchConsentService.getInstance(mContext).getConsent(API_TYPE);
- assertThat(result).isFalse();
-
- // Confirm that the right value is returned even when it is true.
- ExtendedMockito.doReturn(true)
- .when(
- () ->
- AppSearchConsentDao.readConsentData(
- /* globalSearchSession= */ any(ListenableFuture.class),
- /* executor= */ any(),
- /* userId= */ any(),
- eq(API_TYPE)));
- boolean result2 = AppSearchConsentService.getInstance(mContext).getConsent(API_TYPE);
- assertThat(result2).isTrue();
- } finally {
- if (staticMockSessionLocal != null) {
- staticMockSessionLocal.finishMocking();
- }
- }
- }
-
- @Test
- public void testSetContent_failure() {
- MockitoSession staticMockSessionLocal = null;
- try {
- staticMockSessionLocal =
- ExtendedMockito.mockitoSession()
- .spyStatic(PlatformStorage.class)
- .strictness(Strictness.LENIENT)
- .initMocks(this)
- .startMocking();
- AppSearchSession mockSession = Mockito.mock(AppSearchSession.class);
- ExtendedMockito.doReturn(Futures.immediateFuture(mockSession))
- .when(
- () ->
- PlatformStorage.createSearchSessionAsync(
- any(PlatformStorage.SearchContext.class)));
- verify(mockSession, atMost(1)).setSchemaAsync(any(SetSchemaRequest.class));
-
- SetSchemaResponse mockResponse = Mockito.mock(SetSchemaResponse.class);
- when(mockSession.setSchemaAsync(any(SetSchemaRequest.class)))
- .thenReturn(Futures.immediateFuture(mockResponse));
-
- AppSearchResult mockResult = Mockito.mock(AppSearchResult.class);
- SetSchemaResponse.MigrationFailure failure =
- new SetSchemaResponse.MigrationFailure(
- /* namespace= */ TEST,
- /* id= */ TEST,
- /* schemaType= */ TEST,
- /* appSearchResult= */ mockResult);
- when(mockResponse.getMigrationFailures()).thenReturn(List.of(failure));
- RuntimeException e =
- assertThrows(
- RuntimeException.class,
- () ->
- AppSearchConsentService.getInstance(mContext)
- .setConsent(API_TYPE, CONSENTED));
- assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE);
- } finally {
- if (staticMockSessionLocal != null) {
- staticMockSessionLocal.finishMocking();
- }
- }
- }
-
- @Test
- public void testSetContent() {
- MockitoSession staticMockSessionLocal = null;
- try {
- staticMockSessionLocal =
- ExtendedMockito.mockitoSession()
- .spyStatic(PlatformStorage.class)
- .strictness(Strictness.LENIENT)
- .initMocks(this)
- .startMocking();
- AppSearchSession mockSession = Mockito.mock(AppSearchSession.class);
- ExtendedMockito.doReturn(Futures.immediateFuture(mockSession))
- .when(
- () ->
- PlatformStorage.createSearchSessionAsync(
- any(PlatformStorage.SearchContext.class)));
- verify(mockSession, atMost(1)).setSchemaAsync(any(SetSchemaRequest.class));
-
- SetSchemaResponse mockResponse = Mockito.mock(SetSchemaResponse.class);
- when(mockSession.setSchemaAsync(any(SetSchemaRequest.class)))
- .thenReturn(Futures.immediateFuture(mockResponse));
- AppSearchBatchResult<String, Void> result = Mockito.mock(AppSearchBatchResult.class);
- when(mockSession.putAsync(any())).thenReturn(Futures.immediateFuture(result));
-
- verify(mockResponse, atMost(1)).getMigrationFailures();
- when(mockResponse.getMigrationFailures()).thenReturn(List.of());
- // Verify that no exception is thrown.
- AppSearchConsentService.getInstance(mContext).setConsent(API_TYPE, CONSENTED);
- } finally {
- if (staticMockSessionLocal != null) {
- staticMockSessionLocal.finishMocking();
- }
- }
- }
-
- @Test
- public void testGetUserIdentifierFromBinderCallingUid() {
- MockitoSession staticMockSessionLocal = null;
- try {
- staticMockSessionLocal =
- ExtendedMockito.mockitoSession()
- .spyStatic(UserHandle.class)
- .strictness(Strictness.LENIENT)
- .initMocks(this)
- .startMocking();
- UserHandle mockUserHandle = Mockito.mock(UserHandle.class);
- Mockito.when(UserHandle.getUserHandleForUid(Binder.getCallingUid()))
- .thenReturn(mockUserHandle);
- Mockito.when(mockUserHandle.getIdentifier()).thenReturn(UID);
- String result =
- AppSearchConsentService.getInstance(mContext)
- .getUserIdentifierFromBinderCallingUid();
- assertThat(result).isEqualTo("" + UID);
- } finally {
- if (staticMockSessionLocal != null) {
- staticMockSessionLocal.finishMocking();
- }
- }
- }
-
- @Test
- public void testGetAdServicesPackageName_null() {
- MockitoSession staticMockSessionLocal = null;
- try {
- staticMockSessionLocal =
- ExtendedMockito.mockitoSession()
- .spyStatic(AdServicesCommon.class)
- .strictness(Strictness.LENIENT)
- .initMocks(this)
- .startMocking();
- Context context = Mockito.mock(Context.class);
- PackageManager mockPackageManager = Mockito.mock(PackageManager.class);
- Mockito.when(context.getPackageManager()).thenReturn(mockPackageManager);
- Mockito.when(AdServicesCommon.resolveAdServicesService(any(), any())).thenReturn(null);
- RuntimeException e =
- assertThrows(
- RuntimeException.class,
- () -> AppSearchConsentService.getAdServicesPackageName(context));
- assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE);
- } finally {
- if (staticMockSessionLocal != null) {
- staticMockSessionLocal.finishMocking();
- }
- }
- }
-
- @Test
- public void testGetAdServicesPackageName() {
- Context context = Mockito.mock(Context.class);
- PackageManager mockPackageManager = Mockito.mock(PackageManager.class);
- // When the resolveInfo returns AdServices package name, that is returned.
- Mockito.when(context.getPackageManager()).thenReturn(mockPackageManager);
-
- ServiceInfo serviceInfo1 = new ServiceInfo();
- serviceInfo1.packageName = ADSERVICES_PACKAGE_NAME;
- ResolveInfo resolveInfo1 = new ResolveInfo();
- resolveInfo1.serviceInfo = serviceInfo1;
-
- ServiceInfo serviceInfo2 = new ServiceInfo();
- serviceInfo2.packageName = ADEXTSERVICES_PACKAGE_NAME;
- ResolveInfo resolveInfo2 = new ResolveInfo();
- resolveInfo2.serviceInfo = serviceInfo2;
- Mockito.when(mockPackageManager.queryIntentServices(any(), anyInt()))
- .thenReturn(List.of(resolveInfo1, resolveInfo2));
- assertThat(AppSearchConsentService.getAdServicesPackageName(context))
- .isEqualTo(ADSERVICES_PACKAGE_NAME);
-
- // When the resolveInfo returns AdExtServices package name, the AdServices package name
- // is returned.
- Mockito.when(mockPackageManager.queryIntentServices(any(), anyInt()))
- .thenReturn(List.of(resolveInfo2));
- assertThat(AppSearchConsentService.getAdServicesPackageName(context))
- .isEqualTo(ADSERVICES_PACKAGE_NAME);
- }
-}
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchConsentWorkerTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchConsentWorkerTest.java
new file mode 100644
index 0000000..058682f
--- /dev/null
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchConsentWorkerTest.java
@@ -0,0 +1,583 @@
+/*
+ * 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.appsearch;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.atMost;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Binder;
+import android.os.UserHandle;
+
+import androidx.appsearch.app.AppSearchBatchResult;
+import androidx.appsearch.app.AppSearchResult;
+import androidx.appsearch.app.AppSearchSession;
+import androidx.appsearch.app.SetSchemaRequest;
+import androidx.appsearch.app.SetSchemaResponse;
+import androidx.appsearch.platformstorage.PlatformStorage;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import com.android.adservices.AdServicesCommon;
+import com.android.adservices.service.consent.AdServicesApiType;
+import com.android.adservices.service.consent.ConsentConstants;
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.FluentFuture;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
+
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+@SmallTest
+public class AppSearchConsentWorkerTest {
+ private Context mContext = ApplicationProvider.getApplicationContext();
+ private static final String ADSERVICES_PACKAGE_NAME = "com.android.adservices.api";
+ private static final String ADEXTSERVICES_PACKAGE_NAME = "com.android.ext.adservices.api";
+ private static final String API_TYPE = AdServicesApiType.TOPICS.toPpApiDatastoreKey();
+ private static final Boolean CONSENTED = true;
+ private static final String TEST = "test";
+ private static final int UID = 55;
+
+ @Test
+ public void testGetConsent() {
+ MockitoSession staticMockSessionLocal = null;
+ try {
+ staticMockSessionLocal =
+ ExtendedMockito.mockitoSession()
+ .mockStatic(AppSearchConsentDao.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ ExtendedMockito.doReturn(false)
+ .when(
+ () ->
+ AppSearchConsentDao.readConsentData(
+ /* globalSearchSession= */ any(ListenableFuture.class),
+ /* executor= */ any(),
+ /* userId= */ any(),
+ eq(API_TYPE)));
+ boolean result = AppSearchConsentWorker.getInstance(mContext).getConsent(API_TYPE);
+ assertThat(result).isFalse();
+
+ // Confirm that the right value is returned even when it is true.
+ ExtendedMockito.doReturn(true)
+ .when(
+ () ->
+ AppSearchConsentDao.readConsentData(
+ /* globalSearchSession= */ any(ListenableFuture.class),
+ /* executor= */ any(),
+ /* userId= */ any(),
+ eq(API_TYPE)));
+ boolean result2 = AppSearchConsentWorker.getInstance(mContext).getConsent(API_TYPE);
+ assertThat(result2).isTrue();
+ } finally {
+ if (staticMockSessionLocal != null) {
+ staticMockSessionLocal.finishMocking();
+ }
+ }
+ }
+
+ @Test
+ public void testSetConsent_failure() {
+ MockitoSession staticMockSessionLocal = null;
+ try {
+ staticMockSessionLocal =
+ ExtendedMockito.mockitoSession()
+ .spyStatic(PlatformStorage.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ AppSearchSession mockSession = Mockito.mock(AppSearchSession.class);
+ ExtendedMockito.doReturn(Futures.immediateFuture(mockSession))
+ .when(
+ () ->
+ PlatformStorage.createSearchSessionAsync(
+ any(PlatformStorage.SearchContext.class)));
+ verify(mockSession, atMost(1)).setSchemaAsync(any(SetSchemaRequest.class));
+
+ SetSchemaResponse mockResponse = Mockito.mock(SetSchemaResponse.class);
+ when(mockSession.setSchemaAsync(any(SetSchemaRequest.class)))
+ .thenReturn(Futures.immediateFuture(mockResponse));
+
+ AppSearchResult mockResult = Mockito.mock(AppSearchResult.class);
+ SetSchemaResponse.MigrationFailure failure =
+ new SetSchemaResponse.MigrationFailure(
+ /* namespace= */ TEST,
+ /* id= */ TEST,
+ /* schemaType= */ TEST,
+ /* appSearchResult= */ mockResult);
+ when(mockResponse.getMigrationFailures()).thenReturn(List.of(failure));
+ RuntimeException e =
+ assertThrows(
+ RuntimeException.class,
+ () ->
+ AppSearchConsentWorker.getInstance(mContext)
+ .setConsent(API_TYPE, CONSENTED));
+ assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE);
+ } finally {
+ if (staticMockSessionLocal != null) {
+ staticMockSessionLocal.finishMocking();
+ }
+ }
+ }
+
+ @Test
+ public void testSetConsent() {
+ MockitoSession staticMockSessionLocal = null;
+ try {
+ staticMockSessionLocal =
+ ExtendedMockito.mockitoSession()
+ .spyStatic(PlatformStorage.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ AppSearchSession mockSession = Mockito.mock(AppSearchSession.class);
+ ExtendedMockito.doReturn(Futures.immediateFuture(mockSession))
+ .when(
+ () ->
+ PlatformStorage.createSearchSessionAsync(
+ any(PlatformStorage.SearchContext.class)));
+ verify(mockSession, atMost(1)).setSchemaAsync(any(SetSchemaRequest.class));
+
+ SetSchemaResponse mockResponse = Mockito.mock(SetSchemaResponse.class);
+ when(mockSession.setSchemaAsync(any(SetSchemaRequest.class)))
+ .thenReturn(Futures.immediateFuture(mockResponse));
+ AppSearchBatchResult<String, Void> result = Mockito.mock(AppSearchBatchResult.class);
+ when(mockSession.putAsync(any())).thenReturn(Futures.immediateFuture(result));
+
+ verify(mockResponse, atMost(1)).getMigrationFailures();
+ when(mockResponse.getMigrationFailures()).thenReturn(List.of());
+ // Verify that no exception is thrown.
+ AppSearchConsentWorker.getInstance(mContext).setConsent(API_TYPE, CONSENTED);
+ } finally {
+ if (staticMockSessionLocal != null) {
+ staticMockSessionLocal.finishMocking();
+ }
+ }
+ }
+
+ @Test
+ public void testGetUserIdentifierFromBinderCallingUid() {
+ MockitoSession staticMockSessionLocal = null;
+ try {
+ staticMockSessionLocal =
+ ExtendedMockito.mockitoSession()
+ .spyStatic(UserHandle.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ UserHandle mockUserHandle = Mockito.mock(UserHandle.class);
+ Mockito.when(UserHandle.getUserHandleForUid(Binder.getCallingUid()))
+ .thenReturn(mockUserHandle);
+ Mockito.when(mockUserHandle.getIdentifier()).thenReturn(UID);
+ String result =
+ AppSearchConsentWorker.getInstance(mContext)
+ .getUserIdentifierFromBinderCallingUid();
+ assertThat(result).isEqualTo("" + UID);
+ } finally {
+ if (staticMockSessionLocal != null) {
+ staticMockSessionLocal.finishMocking();
+ }
+ }
+ }
+
+ @Test
+ public void testGetAdServicesPackageName_null() {
+ MockitoSession staticMockSessionLocal = null;
+ try {
+ staticMockSessionLocal =
+ ExtendedMockito.mockitoSession()
+ .spyStatic(AdServicesCommon.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ Context context = Mockito.mock(Context.class);
+ PackageManager mockPackageManager = Mockito.mock(PackageManager.class);
+ Mockito.when(context.getPackageManager()).thenReturn(mockPackageManager);
+ Mockito.when(AdServicesCommon.resolveAdServicesService(any(), any())).thenReturn(null);
+ RuntimeException e =
+ assertThrows(
+ RuntimeException.class,
+ () -> AppSearchConsentWorker.getAdServicesPackageName(context));
+ assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE);
+ } finally {
+ if (staticMockSessionLocal != null) {
+ staticMockSessionLocal.finishMocking();
+ }
+ }
+ }
+
+ @Test
+ public void testGetAdServicesPackageName() {
+ Context context = Mockito.mock(Context.class);
+ PackageManager mockPackageManager = Mockito.mock(PackageManager.class);
+ // When the resolveInfo returns AdServices package name, that is returned.
+ Mockito.when(context.getPackageManager()).thenReturn(mockPackageManager);
+
+ ServiceInfo serviceInfo1 = new ServiceInfo();
+ serviceInfo1.packageName = ADSERVICES_PACKAGE_NAME;
+ ResolveInfo resolveInfo1 = new ResolveInfo();
+ resolveInfo1.serviceInfo = serviceInfo1;
+
+ ServiceInfo serviceInfo2 = new ServiceInfo();
+ serviceInfo2.packageName = ADEXTSERVICES_PACKAGE_NAME;
+ ResolveInfo resolveInfo2 = new ResolveInfo();
+ resolveInfo2.serviceInfo = serviceInfo2;
+ Mockito.when(mockPackageManager.queryIntentServices(any(), anyInt()))
+ .thenReturn(List.of(resolveInfo1, resolveInfo2));
+ assertThat(AppSearchConsentWorker.getAdServicesPackageName(context))
+ .isEqualTo(ADSERVICES_PACKAGE_NAME);
+
+ // When the resolveInfo returns AdExtServices package name, the AdServices package name
+ // is returned.
+ Mockito.when(mockPackageManager.queryIntentServices(any(), anyInt()))
+ .thenReturn(List.of(resolveInfo2));
+ assertThat(AppSearchConsentWorker.getAdServicesPackageName(context))
+ .isEqualTo(ADSERVICES_PACKAGE_NAME);
+ }
+
+ @Test
+ public void testGetAppsWithConsent_nullOrEmpty() {
+ MockitoSession staticMockSessionLocal = null;
+ try {
+ staticMockSessionLocal =
+ ExtendedMockito.mockitoSession()
+ .spyStatic(AppSearchAppConsentDao.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ // Null dao is returned.
+ ExtendedMockito.doReturn(null)
+ .when(() -> AppSearchAppConsentDao.readConsentData(any(), any(), any(), any()));
+ AppSearchConsentWorker appSearchConsentWorker =
+ AppSearchConsentWorker.getInstance(mContext);
+ assertThat(appSearchConsentWorker.getAppsWithConsent(TEST)).isNotNull();
+ assertThat(appSearchConsentWorker.getAppsWithConsent(TEST)).isEmpty();
+
+ // Dao is returned, but list is null.
+ AppSearchAppConsentDao mockDao = Mockito.mock(AppSearchAppConsentDao.class);
+ ExtendedMockito.doReturn(mockDao)
+ .when(() -> AppSearchAppConsentDao.readConsentData(any(), any(), any(), any()));
+ assertThat(appSearchConsentWorker.getAppsWithConsent(TEST)).isNotNull();
+ assertThat(appSearchConsentWorker.getAppsWithConsent(TEST)).isEmpty();
+ } finally {
+ if (staticMockSessionLocal != null) {
+ staticMockSessionLocal.finishMocking();
+ }
+ }
+ }
+
+ @Test
+ public void testGetAppsWithConsent() {
+ MockitoSession staticMockSessionLocal = null;
+ try {
+ staticMockSessionLocal =
+ ExtendedMockito.mockitoSession()
+ .spyStatic(AppSearchAppConsentDao.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ // Null dao is returned.
+ ExtendedMockito.doReturn(null)
+ .when(() -> AppSearchAppConsentDao.readConsentData(any(), any(), any(), any()));
+ AppSearchConsentWorker appSearchConsentWorker =
+ AppSearchConsentWorker.getInstance(mContext);
+ assertThat(appSearchConsentWorker.getAppsWithConsent(TEST)).isNotNull();
+ assertThat(appSearchConsentWorker.getAppsWithConsent(TEST)).isEmpty();
+
+ // Dao is returned, but list is null.
+ AppSearchAppConsentDao mockDao = Mockito.mock(AppSearchAppConsentDao.class);
+ List<String> apps = ImmutableList.of(TEST);
+ when(mockDao.getApps()).thenReturn(apps);
+ ExtendedMockito.doReturn(mockDao)
+ .when(() -> AppSearchAppConsentDao.readConsentData(any(), any(), any(), any()));
+ assertThat(appSearchConsentWorker.getAppsWithConsent(TEST)).isNotNull();
+ assertThat(appSearchConsentWorker.getAppsWithConsent(TEST)).isEqualTo(apps);
+ } finally {
+ if (staticMockSessionLocal != null) {
+ staticMockSessionLocal.finishMocking();
+ }
+ }
+ }
+
+ @Test
+ public void testClearAppsWithConsent_failure() {
+ MockitoSession staticMockSessionLocal = null;
+ try {
+ staticMockSessionLocal =
+ ExtendedMockito.mockitoSession()
+ .spyStatic(AppSearchDao.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ FluentFuture future =
+ FluentFuture.from(
+ Futures.immediateFailedFuture(new ExecutionException("test", null)));
+ ExtendedMockito.doReturn(future)
+ .when(() -> AppSearchDao.deleteConsentData(any(), any(), any(), any(), any()));
+ AppSearchConsentWorker appSearchConsentWorker =
+ AppSearchConsentWorker.getInstance(mContext);
+ RuntimeException e =
+ assertThrows(
+ RuntimeException.class,
+ () -> appSearchConsentWorker.clearAppsWithConsent(TEST));
+ assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE);
+ } finally {
+ if (staticMockSessionLocal != null) {
+ staticMockSessionLocal.finishMocking();
+ }
+ }
+ }
+
+ @Test
+ public void testClearAppsWithConsent() {
+ MockitoSession staticMockSessionLocal = null;
+ try {
+ staticMockSessionLocal =
+ ExtendedMockito.mockitoSession()
+ .spyStatic(AppSearchDao.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ AppSearchBatchResult<String, Void> result = Mockito.mock(AppSearchBatchResult.class);
+ FluentFuture future = FluentFuture.from(Futures.immediateFuture(result));
+ ExtendedMockito.doReturn(future)
+ .when(() -> AppSearchDao.deleteConsentData(any(), any(), any(), any(), any()));
+ AppSearchConsentWorker appSearchConsentWorker =
+ AppSearchConsentWorker.getInstance(mContext);
+ // No exceptions are thrown.
+ appSearchConsentWorker.clearAppsWithConsent(TEST);
+ } finally {
+ if (staticMockSessionLocal != null) {
+ staticMockSessionLocal.finishMocking();
+ }
+ }
+ }
+
+ @Test
+ public void testAddAppWithConsent_null() {
+ MockitoSession staticMockSessionLocal = null;
+ try {
+ String consentType = AppSearchAppConsentDao.APPS_WITH_CONSENT;
+ staticMockSessionLocal =
+ ExtendedMockito.mockitoSession()
+ .spyStatic(AppSearchAppConsentDao.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ ExtendedMockito.doReturn(null)
+ .when(() -> AppSearchAppConsentDao.readConsentData(any(), any(), any(), any()));
+ AppSearchConsentWorker appSearchConsentWorker =
+ AppSearchConsentWorker.getInstance(mContext);
+ // No exceptions are thrown.
+ assertThat(appSearchConsentWorker.addAppWithConsent(consentType, TEST)).isTrue();
+ ExtendedMockito.verify(
+ () -> AppSearchAppConsentDao.getRowId(any(), eq(consentType)), atLeastOnce());
+ } finally {
+ if (staticMockSessionLocal != null) {
+ staticMockSessionLocal.finishMocking();
+ }
+ }
+ }
+
+ @Test
+ public void testAddAppWithConsent_failure() {
+ MockitoSession staticMockSessionLocal = null;
+ try {
+ String consentType = AppSearchAppConsentDao.APPS_WITH_CONSENT;
+ staticMockSessionLocal =
+ ExtendedMockito.mockitoSession()
+ .spyStatic(AppSearchAppConsentDao.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ AppSearchAppConsentDao dao = Mockito.mock(AppSearchAppConsentDao.class);
+ ExtendedMockito.doReturn(dao)
+ .when(
+ () ->
+ AppSearchAppConsentDao.readConsentData(
+ any(), any(), any(), eq(consentType)));
+ when(dao.getApps()).thenReturn(List.of());
+ FluentFuture future =
+ FluentFuture.from(
+ Futures.immediateFailedFuture(new ExecutionException("test", null)));
+ when(dao.writeConsentData(any(), any(), any())).thenReturn(future);
+
+ AppSearchConsentWorker appSearchConsentWorker =
+ AppSearchConsentWorker.getInstance(mContext);
+ assertThat(appSearchConsentWorker.addAppWithConsent(consentType, TEST)).isFalse();
+ } finally {
+ if (staticMockSessionLocal != null) {
+ staticMockSessionLocal.finishMocking();
+ }
+ }
+ }
+
+ @Test
+ public void testAddAppWithConsent() {
+ MockitoSession staticMockSessionLocal = null;
+ try {
+ String consentType = AppSearchAppConsentDao.APPS_WITH_CONSENT;
+ staticMockSessionLocal =
+ ExtendedMockito.mockitoSession()
+ .spyStatic(AppSearchAppConsentDao.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ AppSearchAppConsentDao dao = Mockito.mock(AppSearchAppConsentDao.class);
+ ExtendedMockito.doReturn(dao)
+ .when(
+ () ->
+ AppSearchAppConsentDao.readConsentData(
+ any(), any(), any(), eq(consentType)));
+ when(dao.getApps()).thenReturn(List.of());
+ AppSearchBatchResult<String, Void> result = Mockito.mock(AppSearchBatchResult.class);
+ FluentFuture future = FluentFuture.from(Futures.immediateFuture(result));
+ when(dao.writeConsentData(any(), any(), any())).thenReturn(future);
+
+ AppSearchConsentWorker appSearchConsentWorker =
+ AppSearchConsentWorker.getInstance(mContext);
+ // No exceptions are thrown.
+ assertThat(appSearchConsentWorker.addAppWithConsent(consentType, TEST)).isTrue();
+ verify(dao, atLeastOnce()).getApps();
+ verify(dao, atLeastOnce()).writeConsentData(any(), any(), any());
+ } finally {
+ if (staticMockSessionLocal != null) {
+ staticMockSessionLocal.finishMocking();
+ }
+ }
+ }
+
+ @Test
+ public void testRemoveAppWithConsent_null() {
+ MockitoSession staticMockSessionLocal = null;
+ try {
+ String consentType = AppSearchAppConsentDao.APPS_WITH_CONSENT;
+ staticMockSessionLocal =
+ ExtendedMockito.mockitoSession()
+ .spyStatic(AppSearchAppConsentDao.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ ExtendedMockito.doReturn(null)
+ .when(() -> AppSearchAppConsentDao.readConsentData(any(), any(), any(), any()));
+ AppSearchConsentWorker appSearchConsentWorker =
+ AppSearchConsentWorker.getInstance(mContext);
+ // No exceptions are thrown.
+ appSearchConsentWorker.removeAppWithConsent(consentType, TEST);
+ ExtendedMockito.verify(
+ () -> AppSearchAppConsentDao.getRowId(any(), eq(consentType)), never());
+ } finally {
+ if (staticMockSessionLocal != null) {
+ staticMockSessionLocal.finishMocking();
+ }
+ }
+ }
+
+ @Test
+ public void testRemoveAppWithConsent_failure() {
+ MockitoSession staticMockSessionLocal = null;
+ try {
+ String consentType = AppSearchAppConsentDao.APPS_WITH_CONSENT;
+ staticMockSessionLocal =
+ ExtendedMockito.mockitoSession()
+ .spyStatic(AppSearchAppConsentDao.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ AppSearchAppConsentDao dao = Mockito.mock(AppSearchAppConsentDao.class);
+ ExtendedMockito.doReturn(dao)
+ .when(
+ () ->
+ AppSearchAppConsentDao.readConsentData(
+ any(), any(), any(), eq(consentType)));
+ when(dao.getApps()).thenReturn(List.of(TEST));
+ FluentFuture future =
+ FluentFuture.from(
+ Futures.immediateFailedFuture(new ExecutionException("test", null)));
+ when(dao.writeConsentData(any(), any(), any())).thenReturn(future);
+
+ AppSearchConsentWorker appSearchConsentWorker =
+ AppSearchConsentWorker.getInstance(mContext);
+ RuntimeException e =
+ assertThrows(
+ RuntimeException.class,
+ () -> appSearchConsentWorker.removeAppWithConsent(consentType, TEST));
+ assertThat(e.getMessage()).isEqualTo(ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE);
+ } finally {
+ if (staticMockSessionLocal != null) {
+ staticMockSessionLocal.finishMocking();
+ }
+ }
+ }
+
+ @Test
+ public void testRemoveAppWithConsent() {
+ MockitoSession staticMockSessionLocal = null;
+ try {
+ String consentType = AppSearchAppConsentDao.APPS_WITH_CONSENT;
+ staticMockSessionLocal =
+ ExtendedMockito.mockitoSession()
+ .spyStatic(AppSearchAppConsentDao.class)
+ .strictness(Strictness.WARN)
+ .initMocks(this)
+ .startMocking();
+ AppSearchAppConsentDao dao = Mockito.mock(AppSearchAppConsentDao.class);
+ ExtendedMockito.doReturn(dao)
+ .when(
+ () ->
+ AppSearchAppConsentDao.readConsentData(
+ any(), any(), any(), eq(consentType)));
+
+ when(dao.getApps()).thenReturn(List.of(TEST));
+ AppSearchBatchResult<String, Void> result = Mockito.mock(AppSearchBatchResult.class);
+ FluentFuture future = FluentFuture.from(Futures.immediateFuture(result));
+ when(dao.writeConsentData(any(), any(), any())).thenReturn(future);
+
+ AppSearchConsentWorker appSearchConsentWorker =
+ AppSearchConsentWorker.getInstance(mContext);
+ // No exceptions are thrown.
+ appSearchConsentWorker.removeAppWithConsent(consentType, TEST);
+ verify(dao, atLeastOnce()).getApps();
+ verify(dao, atLeastOnce()).writeConsentData(any(), any(), any());
+ } finally {
+ if (staticMockSessionLocal != null) {
+ staticMockSessionLocal.finishMocking();
+ }
+ }
+ }
+}
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchDaoTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchDaoTest.java
index 65c130c..4c791be 100644
--- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchDaoTest.java
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/appsearch/AppSearchDaoTest.java
@@ -112,21 +112,23 @@
@Test
public void testReadConsentData_emptyQuery() {
- assertThat(
- AppSearchDao.readConsentData(
- AppSearchConsentDao.class,
- Futures.immediateFuture(mGlobalSearchSession),
- mExecutor,
- null))
- .isEqualTo(null);
+ AppSearchDao dao =
+ AppSearchDao.readConsentData(
+ AppSearchConsentDao.class,
+ Futures.immediateFuture(mGlobalSearchSession),
+ mExecutor,
+ NAMESPACE,
+ null);
+ assertThat(dao).isEqualTo(null);
- assertThat(
- AppSearchDao.readConsentData(
- AppSearchConsentDao.class,
- Futures.immediateFuture(mGlobalSearchSession),
- mExecutor,
- ""))
- .isEqualTo(null);
+ AppSearchDao dao2 =
+ AppSearchDao.readConsentData(
+ AppSearchConsentDao.class,
+ Futures.immediateFuture(mGlobalSearchSession),
+ mExecutor,
+ NAMESPACE,
+ "");
+ assertThat(dao2).isEqualTo(null);
}
@Test
@@ -144,17 +146,18 @@
new SearchResult.Builder(TEST, TEST).setGenericDocument(document).build();
when(mMockPage.get(0)).thenReturn(searchResult);
when(mGlobalSearchSession.search(any(), any())).thenReturn(mSearchResults);
- assertThat(
- AppSearchDao.readConsentData(
- AppSearchConsentDao.class,
- Futures.immediateFuture(mGlobalSearchSession),
- mExecutor,
- TEST))
- .isEqualTo(dao);
+ AppSearchDao result =
+ AppSearchDao.readConsentData(
+ AppSearchConsentDao.class,
+ Futures.immediateFuture(mGlobalSearchSession),
+ mExecutor,
+ NAMESPACE,
+ TEST);
+ assertThat(result).isEqualTo(dao);
}
@Test
- public void testWriteConsentData_failure() throws Exception {
+ public void testWriteConsentData_failure() {
AppSearchSession mockSession = Mockito.mock(AppSearchSession.class);
verify(mockSession, atMost(1)).setSchemaAsync(any(SetSchemaRequest.class));
@@ -206,4 +209,64 @@
Futures.immediateFuture(mockSession), PACKAGE_IDENTIFIER, mExecutor);
assertThat(future.get()).isNotNull();
}
+
+ @Test
+ public void testDeleteConsentData_failure() {
+ AppSearchSession mockSession = Mockito.mock(AppSearchSession.class);
+ verify(mockSession, atMost(1)).setSchemaAsync(any(SetSchemaRequest.class));
+
+ SetSchemaResponse mockResponse = Mockito.mock(SetSchemaResponse.class);
+ when(mockSession.setSchemaAsync(any(SetSchemaRequest.class)))
+ .thenReturn(Futures.immediateFuture(mockResponse));
+
+ AppSearchResult mockResult = Mockito.mock(AppSearchResult.class);
+ SetSchemaResponse.MigrationFailure failure =
+ new SetSchemaResponse.MigrationFailure(
+ /* namespace= */ TEST,
+ /* id= */ TEST,
+ /* schemaType= */ TEST,
+ /* appSearchResult= */ mockResult);
+ when(mockResponse.getMigrationFailures()).thenReturn(List.of(failure));
+ // We can't use the base class instance since writing will fail without the necessary
+ // Document fields defined on the class, so we use a subclass instance.
+ FluentFuture<AppSearchBatchResult<String, Void>> result =
+ AppSearchDao.deleteConsentData(
+ AppSearchConsentDao.class,
+ Futures.immediateFuture(mockSession),
+ mExecutor,
+ NAMESPACE,
+ TEST);
+ ExecutionException e = assertThrows(ExecutionException.class, () -> result.get());
+ assertThat(e.getMessage())
+ .isEqualTo(
+ "java.lang.RuntimeException: "
+ + ConsentConstants.ERROR_MESSAGE_APPSEARCH_FAILURE);
+ }
+
+ @Test
+ public void testDeleteConsentData() throws Exception {
+ AppSearchSession mockSession = Mockito.mock(AppSearchSession.class);
+ verify(mockSession, atMost(1)).setSchemaAsync(any(SetSchemaRequest.class));
+
+ SetSchemaResponse mockResponse = Mockito.mock(SetSchemaResponse.class);
+ when(mockSession.setSchemaAsync(any(SetSchemaRequest.class)))
+ .thenReturn(Futures.immediateFuture(mockResponse));
+
+ verify(mockResponse, atMost(1)).getMigrationFailures();
+ when(mockResponse.getMigrationFailures()).thenReturn(List.of());
+ // We can't use the base class instance since writing will fail without the necessary
+ // Document fields defined on the class, so we use a subclass instance.
+ AppSearchBatchResult<String, Void> result = Mockito.mock(AppSearchBatchResult.class);
+ when(mockSession.removeAsync(any())).thenReturn(Futures.immediateFuture(result));
+
+ // Verify that no exception is thrown.
+ FluentFuture future =
+ AppSearchDao.deleteConsentData(
+ AppSearchConsentDao.class,
+ Futures.immediateFuture(mockSession),
+ mExecutor,
+ NAMESPACE,
+ TEST);
+ assertThat(future.get()).isNotNull();
+ }
}
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/consent/ConsentManagerTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/consent/ConsentManagerTest.java
index aae0fd8..5eb620e 100644
--- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/consent/ConsentManagerTest.java
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/consent/ConsentManagerTest.java
@@ -84,7 +84,7 @@
import com.android.adservices.service.Flags;
import com.android.adservices.service.FlagsFactory;
import com.android.adservices.service.MaintenanceJobService;
-import com.android.adservices.service.appsearch.AppSearchConsentService;
+import com.android.adservices.service.appsearch.AppSearchConsentManager;
import com.android.adservices.service.common.BackgroundJobsManager;
import com.android.adservices.service.common.compat.PackageManagerCompatUtils;
import com.android.adservices.service.common.feature.PrivacySandboxFeatureType;
@@ -152,7 +152,7 @@
@Mock private Flags mMockFlags;
@Mock private JobScheduler mJobSchedulerMock;
@Mock private IAdServicesManager mMockIAdServicesManager;
- @Mock private AppSearchConsentService mAppSearchConsentService;
+ @Mock private AppSearchConsentManager mAppSearchConsentManager;
private MockitoSession mStaticMockSession = null;
@Before
@@ -316,6 +316,7 @@
public void testConsentIsGivenAfterEnabling_AppSearchOnly() throws Exception {
boolean isGiven = true;
int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ doReturn(true).when(mMockFlags).getEnableAppsearchConsentData();
ConsentManager spyConsentManager =
getSpiedConsentManagerForMigrationTesting(isGiven, consentSourceOfTruth);
@@ -329,7 +330,7 @@
/* hasWrittenToPpApi */ false,
/* hasWrittenToSystemServer */ false,
/* hasReadFromSystemServer */ false);
- verify(mAppSearchConsentService, atLeastOnce()).getConsent(CONSENT_KEY);
+ verify(mAppSearchConsentManager, atLeastOnce()).getConsent(CONSENT_KEY);
verifyDataCleanup(spyConsentManager);
}
@@ -410,6 +411,7 @@
throws RemoteException, IOException {
boolean isGiven = false;
int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ doReturn(true).when(mMockFlags).getEnableAppsearchConsentData();
ConsentManager spyConsentManager =
getSpiedConsentManagerForMigrationTesting(isGiven, consentSourceOfTruth);
@@ -423,7 +425,7 @@
/* hasWrittenToPpApi */ false,
/* hasWrittenToSystemServer */ false,
/* hasReadFromSystemServer */ false);
- verify(mAppSearchConsentService, atLeastOnce()).getConsent(CONSENT_KEY);
+ verify(mAppSearchConsentManager, atLeastOnce()).getConsent(CONSENT_KEY);
verifyDataCleanup(spyConsentManager);
}
@@ -849,6 +851,46 @@
}
@Test
+ public void testIsFledgeConsentRevokedForAppWithFullApiConsentGaUxEnabled_appSearchOnly()
+ throws Exception {
+ runTestIsFledgeConsentRevokedForAppWithFullApiConsentAppSearchOnly(true);
+ }
+
+ @Test
+ public void testIsFledgeConsentRevokedForAppWithFullApiConsentGaUxDisabled_appSearchOnly()
+ throws Exception {
+ runTestIsFledgeConsentRevokedForAppWithFullApiConsentAppSearchOnly(false);
+ }
+
+ private void runTestIsFledgeConsentRevokedForAppWithFullApiConsentAppSearchOnly(
+ boolean isGaUxEnabled) throws Exception {
+ when(mMockFlags.getGaUxFeatureEnabled()).thenReturn(isGaUxEnabled);
+ when(mMockFlags.getEnableAppsearchConsentData()).thenReturn(true);
+ int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ mConsentManager = getConsentManagerByConsentSourceOfTruth(consentSourceOfTruth);
+ when(mAppSearchConsentManager.getConsent(any())).thenReturn(true);
+
+ mConsentManager.enable(mContextSpy, AdServicesApiType.FLEDGE);
+ ExtendedMockito.verify(
+ () -> UiStatsLogger.logOptInSelected(mContextSpy, AdServicesApiType.FLEDGE));
+
+ String app1 = AppConsentDaoFixture.APP10_PACKAGE_NAME;
+ String app2 = AppConsentDaoFixture.APP20_PACKAGE_NAME;
+ String app3 = AppConsentDaoFixture.APP30_PACKAGE_NAME;
+ mockGetPackageUid(app1, AppConsentDaoFixture.APP10_UID);
+ mockGetPackageUid(app2, AppConsentDaoFixture.APP20_UID);
+ mockGetPackageUid(app3, AppConsentDaoFixture.APP30_UID);
+
+ when(mAppSearchConsentManager.isFledgeConsentRevokedForApp(app1)).thenReturn(false);
+ when(mAppSearchConsentManager.isFledgeConsentRevokedForApp(app2)).thenReturn(true);
+ when(mAppSearchConsentManager.isFledgeConsentRevokedForApp(app3)).thenReturn(false);
+
+ assertFalse(mConsentManager.isFledgeConsentRevokedForApp(app1));
+ assertTrue(mConsentManager.isFledgeConsentRevokedForApp(app2));
+ assertFalse(mConsentManager.isFledgeConsentRevokedForApp(app3));
+ }
+
+ @Test
public void testIsFledgeConsentRevokedForAppWithFullApiConsentGaUxEnabled_ppApiAndSystemServer()
throws PackageManager.NameNotFoundException, RemoteException {
when(mMockFlags.getGaUxFeatureEnabled()).thenReturn(true);
@@ -1228,6 +1270,50 @@
AppConsentDaoFixture.APP30_PACKAGE_NAME));
}
+ // AppSearch test for isFledgeConsentRevokedForAppAfterSettingFledgeUse with GA UX disabled.
+ @Test
+ public void testIsFledgeConsentRevokedForAppAfterSetFledgeUseWithFullApiConsentGaUxDisabled_as()
+ throws Exception {
+ runTestIsFledgeConsentRevokedForAppAfterSetFledgeUseWithFullApiConsentAppSearch(false);
+ }
+
+ // AppSearch test for isFledgeConsentRevokedForAppAfterSettingFledgeUse with GA UX enabled.
+ @Test
+ public void testIsFledgeConsentRevokedForAppAfterSetFledgeUseWithFullApiConsentGaUxEnabled_as()
+ throws Exception {
+ runTestIsFledgeConsentRevokedForAppAfterSetFledgeUseWithFullApiConsentAppSearch(true);
+ }
+
+ private void runTestIsFledgeConsentRevokedForAppAfterSetFledgeUseWithFullApiConsentAppSearch(
+ boolean isGaUxEnabled) throws Exception {
+ mConsentManager = getConsentManagerByConsentSourceOfTruth(Flags.APPSEARCH_ONLY);
+ when(mMockFlags.getEnableAppsearchConsentData()).thenReturn(true);
+ when(mMockFlags.getGaUxFeatureEnabled()).thenReturn(isGaUxEnabled);
+ mConsentManager.enable(mContextSpy);
+ when(mAppSearchConsentManager.getConsent(any())).thenReturn(true);
+
+ ExtendedMockito.verify(() -> UiStatsLogger.logOptInSelected(mContextSpy));
+ ExtendedMockito.verify(() -> UiStatsLogger.logResetMeasurement(mContextSpy));
+
+ String app1 = AppConsentDaoFixture.APP10_PACKAGE_NAME;
+ String app2 = AppConsentDaoFixture.APP20_PACKAGE_NAME;
+ String app3 = AppConsentDaoFixture.APP30_PACKAGE_NAME;
+ mockGetPackageUid(app1, AppConsentDaoFixture.APP10_UID);
+ mockGetPackageUid(app2, AppConsentDaoFixture.APP20_UID);
+ mockGetPackageUid(app3, AppConsentDaoFixture.APP30_UID);
+
+ when(mAppSearchConsentManager.isFledgeConsentRevokedForAppAfterSettingFledgeUse(app1))
+ .thenReturn(false);
+ when(mAppSearchConsentManager.isFledgeConsentRevokedForAppAfterSettingFledgeUse(app2))
+ .thenReturn(true);
+ when(mAppSearchConsentManager.isFledgeConsentRevokedForAppAfterSettingFledgeUse(app3))
+ .thenReturn(false);
+
+ assertFalse(mConsentManager.isFledgeConsentRevokedForAppAfterSettingFledgeUse(app1));
+ assertTrue(mConsentManager.isFledgeConsentRevokedForAppAfterSettingFledgeUse(app2));
+ assertFalse(mConsentManager.isFledgeConsentRevokedForAppAfterSettingFledgeUse(app3));
+ }
+
@Test
public void
testIsFledgeConsentRevokedForAppAfterSetFledgeUseWithFullApiConsentGaUxEnabled_ppApi()
@@ -1797,6 +1883,41 @@
}
@Test
+ public void testGetKnownAppsWithConsent_appSearchOnly() {
+ mConsentManager = getConsentManagerByConsentSourceOfTruth(Flags.APPSEARCH_ONLY);
+ when(mMockFlags.getEnableAppsearchConsentData()).thenReturn(true);
+
+ ImmutableList<App> consentedAppsList =
+ ImmutableList.of(App.create(AppConsentDaoFixture.APP10_PACKAGE_NAME));
+ ImmutableList<App> revokedAppsList =
+ ImmutableList.of(
+ App.create(AppConsentDaoFixture.APP20_PACKAGE_NAME),
+ App.create(AppConsentDaoFixture.APP30_PACKAGE_NAME));
+
+ doReturn(consentedAppsList).when(mAppSearchConsentManager).getKnownAppsWithConsent();
+ doReturn(revokedAppsList).when(mAppSearchConsentManager).getAppsWithRevokedConsent();
+
+ ImmutableList<App> knownAppsWithConsent = mConsentManager.getKnownAppsWithConsent();
+ ImmutableList<App> appsWithRevokedConsent = mConsentManager.getAppsWithRevokedConsent();
+
+ verify(mAppSearchConsentManager).getKnownAppsWithConsent();
+ verify(mAppSearchConsentManager).getAppsWithRevokedConsent();
+
+ // Correct apps have received consent.
+ assertThat(knownAppsWithConsent).hasSize(1);
+ assertThat(knownAppsWithConsent.get(0).getPackageName())
+ .isEqualTo(AppConsentDaoFixture.APP10_PACKAGE_NAME);
+ assertThat(appsWithRevokedConsent).hasSize(2);
+ assertThat(
+ appsWithRevokedConsent.stream()
+ .map(app -> app.getPackageName())
+ .collect(Collectors.toList()))
+ .containsAtLeast(
+ AppConsentDaoFixture.APP20_PACKAGE_NAME,
+ AppConsentDaoFixture.APP30_PACKAGE_NAME);
+ }
+
+ @Test
public void testGetKnownAppsWithConsentAfterConsentForOneOfThemWasRevoked_ppApiOnly()
throws IOException, PackageManager.NameNotFoundException {
doNothing().when(mCustomAudienceDaoMock).deleteCustomAudienceDataByOwner(any());
@@ -1973,7 +2094,7 @@
ImmutableList<App> knownAppsWithConsent = mConsentManager.getKnownAppsWithConsent();
ImmutableList<App> appsWithRevokedConsent = mConsentManager.getAppsWithRevokedConsent();
- // all apps have received a consent
+ // Correct apps have received a consent
assertThat(knownAppsWithConsent).hasSize(2);
assertThat(appsWithRevokedConsent).hasSize(1);
App appWithRevokedConsent = appsWithRevokedConsent.get(0);
@@ -2080,6 +2201,24 @@
}
@Test
+ public void testSetConsentForApp_appSearchOnly() throws Exception {
+ mConsentManager = getConsentManagerByConsentSourceOfTruth(Flags.APPSEARCH_ONLY);
+ when(mMockFlags.getEnableAppsearchConsentData()).thenReturn(true);
+ mockGetPackageUid(AppConsentDaoFixture.APP10_PACKAGE_NAME, AppConsentDaoFixture.APP10_UID);
+
+ App app = App.create(AppConsentDaoFixture.APP10_PACKAGE_NAME);
+ mConsentManager.revokeConsentForApp(app);
+ verify(mAppSearchConsentManager).revokeConsentForApp(app);
+
+ mConsentManager.restoreConsentForApp(app);
+ verify(mAppSearchConsentManager).restoreConsentForApp(app);
+
+ // TODO (b/274035157): The process crashes with a ClassNotFound exception in static mocking
+ // occasionally. Need to add a Thread.sleep to prevent this crash.
+ Thread.sleep(250);
+ }
+
+ @Test
public void clearConsentForUninstalledApp_ppApiOnly()
throws PackageManager.NameNotFoundException, IOException {
mockGetPackageUid(AppConsentDaoFixture.APP10_PACKAGE_NAME, AppConsentDaoFixture.APP10_UID);
@@ -2123,6 +2262,17 @@
}
@Test
+ public void clearConsentForUninstalledApp_appSearchOnly() throws Exception {
+ mConsentManager = getConsentManagerByConsentSourceOfTruth(Flags.APPSEARCH_ONLY);
+ when(mMockFlags.getEnableAppsearchConsentData()).thenReturn(true);
+ String packageName = AppConsentDaoFixture.APP10_PACKAGE_NAME;
+ mockGetPackageUid(packageName, AppConsentDaoFixture.APP10_UID);
+
+ mConsentManager.clearConsentForUninstalledApp(packageName, AppConsentDaoFixture.APP10_UID);
+ verify(mAppSearchConsentManager).clearConsentForUninstalledApp(packageName);
+ }
+
+ @Test
public void clearConsentForUninstalledAppWithoutUid_ppApiOnly() throws IOException {
mDatastore.put(AppConsentDaoFixture.APP10_DATASTORE_KEY, true);
mDatastore.put(AppConsentDaoFixture.APP20_DATASTORE_KEY, true);
@@ -2431,6 +2581,15 @@
}
@Test
+ public void testResetAllAppConsentAndAppData_appSearchOnly() throws Exception {
+ mConsentManager = getConsentManagerByConsentSourceOfTruth(Flags.APPSEARCH_ONLY);
+ when(mMockFlags.getEnableAppsearchConsentData()).thenReturn(true);
+
+ mConsentManager.resetAppsAndBlockedApps();
+ verify(mAppSearchConsentManager).clearAllAppConsentData();
+ }
+
+ @Test
public void testResetAllowedAppConsentAndAppData_ppApiOnly()
throws IOException, PackageManager.NameNotFoundException {
doNothing().when(mCustomAudienceDaoMock).deleteAllCustomAudienceData();
@@ -2701,6 +2860,15 @@
}
@Test
+ public void testResetAllowedAppConsentAndAppData_appSearchOnly() throws Exception {
+ mConsentManager = getConsentManagerByConsentSourceOfTruth(Flags.APPSEARCH_ONLY);
+ when(mMockFlags.getEnableAppsearchConsentData()).thenReturn(true);
+
+ mConsentManager.resetApps();
+ verify(mAppSearchConsentManager).clearKnownAppsWithConsent();
+ }
+
+ @Test
public void testNotificationDisplayedRecorded_PpApiOnly() throws RemoteException {
int consentSourceOfTruth = Flags.PPAPI_ONLY;
ConsentManager spyConsentManager =
@@ -2768,6 +2936,27 @@
}
@Test
+ public void testNotificationDisplayedRecorded_appSearchOnly() throws RemoteException {
+ int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ when(mMockFlags.getEnableAppsearchConsentData()).thenReturn(true);
+ ConsentManager spyConsentManager =
+ getSpiedConsentManagerForMigrationTesting(
+ /* isGiven */ false, consentSourceOfTruth);
+
+ doReturn(false).when(mAppSearchConsentManager).wasNotificationDisplayed();
+ assertThat(spyConsentManager.wasNotificationDisplayed()).isFalse();
+ verify(mAppSearchConsentManager).wasNotificationDisplayed();
+
+ doReturn(true).when(mAppSearchConsentManager).wasNotificationDisplayed();
+ spyConsentManager.recordNotificationDisplayed();
+
+ assertThat(spyConsentManager.wasNotificationDisplayed()).isTrue();
+
+ verify(mAppSearchConsentManager, times(2)).wasNotificationDisplayed();
+ verify(mAppSearchConsentManager).recordNotificationDisplayed();
+ }
+
+ @Test
public void testGaUxNotificationDisplayedRecorded_PpApiOnly() throws RemoteException {
int consentSourceOfTruth = Flags.PPAPI_ONLY;
ConsentManager spyConsentManager =
@@ -2835,6 +3024,25 @@
assertThat(mConsentDatastore.get(GA_UX_NOTIFICATION_DISPLAYED_ONCE)).isTrue();
}
+ @Test
+ public void testGaUxNotificationDisplayedRecorded_appSearchOnly() throws RemoteException {
+ int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ when(mMockFlags.getEnableAppsearchConsentData()).thenReturn(true);
+ ConsentManager spyConsentManager =
+ getSpiedConsentManagerForMigrationTesting(
+ /* isGiven */ false, consentSourceOfTruth);
+
+ when(mAppSearchConsentManager.wasGaUxNotificationDisplayed()).thenReturn(false);
+ assertThat(spyConsentManager.wasGaUxNotificationDisplayed()).isFalse();
+ verify(mAppSearchConsentManager).wasGaUxNotificationDisplayed();
+
+ when(mAppSearchConsentManager.wasGaUxNotificationDisplayed()).thenReturn(true);
+ spyConsentManager.recordGaUxNotificationDisplayed();
+ assertThat(spyConsentManager.wasGaUxNotificationDisplayed()).isTrue();
+
+ verify(mAppSearchConsentManager, times(2)).wasGaUxNotificationDisplayed();
+ verify(mAppSearchConsentManager).recordGaUxNotificationDisplayed();
+ }
@Test
public void testNotificationDisplayedRecorded_notSupportedFlag() throws RemoteException {
@@ -3052,7 +3260,7 @@
mAppInstallDaoMock,
mAdServicesManager,
mConsentDatastore,
- mAppSearchConsentService,
+ mAppSearchConsentManager,
mMockFlags,
Flags.PPAPI_ONLY);
doNothing().when(mBlockedTopicsManager).blockTopic(any());
@@ -3159,9 +3367,9 @@
}
@Test
- public void testConsentPerApiIsGivenAfterEnabling_AppSearchOnly()
- throws RemoteException, IOException {
+ public void testConsentPerApiIsGivenAfterEnabling_AppSearchOnly() throws RemoteException {
when(mMockFlags.getGaUxFeatureEnabled()).thenReturn(true);
+ doReturn(true).when(mMockFlags).getEnableAppsearchConsentData();
boolean isGiven = true;
int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
AdServicesApiType apiType = AdServicesApiType.TOPICS;
@@ -3171,13 +3379,138 @@
spyConsentManager.enable(mContextSpy, AdServicesApiType.TOPICS);
verify(spyConsentManager)
.setPerApiConsentToSourceOfTruth(eq(/* isGiven */ true), eq(apiType));
- verify(mAppSearchConsentService).setConsent(eq(apiType.toPpApiDatastoreKey()), eq(isGiven));
- when(mAppSearchConsentService.getConsent(AdServicesApiType.CONSENT_TOPICS))
+ verify(mAppSearchConsentManager).setConsent(eq(apiType.toPpApiDatastoreKey()), eq(isGiven));
+ when(mAppSearchConsentManager.getConsent(AdServicesApiType.CONSENT_TOPICS))
.thenReturn(true);
assertThat(spyConsentManager.getConsent(AdServicesApiType.TOPICS).isGiven()).isTrue();
}
@Test
+ public void testGetDefaultConsent_AppSearchOnly() throws RemoteException {
+ doReturn(true).when(mMockFlags).getEnableAppsearchConsentData();
+ int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ ConsentManager spyConsentManager =
+ getSpiedConsentManagerForMigrationTesting(false, consentSourceOfTruth);
+
+ when(mAppSearchConsentManager.getConsent(eq(ConsentConstants.DEFAULT_CONSENT)))
+ .thenReturn(false);
+ assertThat(spyConsentManager.getDefaultConsent()).isFalse();
+ verify(mAppSearchConsentManager).getConsent(eq(ConsentConstants.DEFAULT_CONSENT));
+ }
+
+ @Test
+ public void testGetTopicsDefaultConsent_AppSearchOnly() throws RemoteException {
+ doReturn(true).when(mMockFlags).getEnableAppsearchConsentData();
+ int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ ConsentManager spyConsentManager =
+ getSpiedConsentManagerForMigrationTesting(false, consentSourceOfTruth);
+
+ when(mAppSearchConsentManager.getConsent(eq(ConsentConstants.TOPICS_DEFAULT_CONSENT)))
+ .thenReturn(false);
+ assertThat(spyConsentManager.getTopicsDefaultConsent()).isFalse();
+ verify(mAppSearchConsentManager).getConsent(eq(ConsentConstants.TOPICS_DEFAULT_CONSENT));
+ }
+
+ @Test
+ public void testGetFledgeDefaultConsent_AppSearchOnly() throws RemoteException {
+ doReturn(true).when(mMockFlags).getEnableAppsearchConsentData();
+ int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ ConsentManager spyConsentManager =
+ getSpiedConsentManagerForMigrationTesting(false, consentSourceOfTruth);
+
+ when(mAppSearchConsentManager.getConsent(eq(ConsentConstants.FLEDGE_DEFAULT_CONSENT)))
+ .thenReturn(false);
+ assertThat(spyConsentManager.getFledgeDefaultConsent()).isFalse();
+ verify(mAppSearchConsentManager).getConsent(eq(ConsentConstants.FLEDGE_DEFAULT_CONSENT));
+ }
+
+ @Test
+ public void testGetMeasurementDefaultConsent_AppSearchOnly() throws RemoteException {
+ doReturn(true).when(mMockFlags).getEnableAppsearchConsentData();
+ int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ ConsentManager spyConsentManager =
+ getSpiedConsentManagerForMigrationTesting(false, consentSourceOfTruth);
+
+ when(mAppSearchConsentManager.getConsent(eq(ConsentConstants.MEASUREMENT_DEFAULT_CONSENT)))
+ .thenReturn(false);
+ assertThat(spyConsentManager.getMeasurementDefaultConsent()).isFalse();
+ verify(mAppSearchConsentManager)
+ .getConsent(eq(ConsentConstants.MEASUREMENT_DEFAULT_CONSENT));
+ }
+
+ @Test
+ public void testGetDefaultAdIdState_AppSearchOnly() throws RemoteException {
+ doReturn(true).when(mMockFlags).getEnableAppsearchConsentData();
+ int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ ConsentManager spyConsentManager =
+ getSpiedConsentManagerForMigrationTesting(false, consentSourceOfTruth);
+
+ when(mAppSearchConsentManager.getConsent(eq(ConsentConstants.DEFAULT_AD_ID_STATE)))
+ .thenReturn(false);
+ assertThat(spyConsentManager.getDefaultAdIdState()).isFalse();
+ verify(mAppSearchConsentManager).getConsent(eq(ConsentConstants.DEFAULT_AD_ID_STATE));
+ }
+
+ @Test
+ public void testRecordDefaultConsent_AppSearchOnly() throws RemoteException {
+ doReturn(true).when(mMockFlags).getEnableAppsearchConsentData();
+ int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ ConsentManager spyConsentManager =
+ getSpiedConsentManagerForMigrationTesting(false, consentSourceOfTruth);
+
+ spyConsentManager.recordDefaultConsent(true);
+ verify(mAppSearchConsentManager).setConsent(eq(ConsentConstants.DEFAULT_CONSENT), eq(true));
+ }
+
+ @Test
+ public void testRecordTopicsDefaultConsent_AppSearchOnly() throws RemoteException {
+ doReturn(true).when(mMockFlags).getEnableAppsearchConsentData();
+ int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ ConsentManager spyConsentManager =
+ getSpiedConsentManagerForMigrationTesting(false, consentSourceOfTruth);
+
+ spyConsentManager.recordTopicsDefaultConsent(true);
+ verify(mAppSearchConsentManager)
+ .setConsent(eq(ConsentConstants.TOPICS_DEFAULT_CONSENT), eq(true));
+ }
+
+ @Test
+ public void testRecordFledgeDefaultConsent_AppSearchOnly() throws RemoteException {
+ doReturn(true).when(mMockFlags).getEnableAppsearchConsentData();
+ int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ ConsentManager spyConsentManager =
+ getSpiedConsentManagerForMigrationTesting(false, consentSourceOfTruth);
+
+ spyConsentManager.recordFledgeDefaultConsent(true);
+ verify(mAppSearchConsentManager)
+ .setConsent(eq(ConsentConstants.FLEDGE_DEFAULT_CONSENT), eq(true));
+ }
+
+ @Test
+ public void testRecordMeasurementDefaultConsent_AppSearchOnly() throws RemoteException {
+ doReturn(true).when(mMockFlags).getEnableAppsearchConsentData();
+ int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ ConsentManager spyConsentManager =
+ getSpiedConsentManagerForMigrationTesting(false, consentSourceOfTruth);
+
+ spyConsentManager.recordMeasurementDefaultConsent(true);
+ verify(mAppSearchConsentManager)
+ .setConsent(eq(ConsentConstants.MEASUREMENT_DEFAULT_CONSENT), eq(true));
+ }
+
+ @Test
+ public void testRecordDefaultAdIdState_AppSearchOnly() throws RemoteException {
+ doReturn(true).when(mMockFlags).getEnableAppsearchConsentData();
+ int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ ConsentManager spyConsentManager =
+ getSpiedConsentManagerForMigrationTesting(false, consentSourceOfTruth);
+
+ spyConsentManager.recordDefaultAdIdState(true);
+ verify(mAppSearchConsentManager)
+ .setConsent(eq(ConsentConstants.DEFAULT_AD_ID_STATE), eq(true));
+ }
+
+ @Test
public void testAllThreeConsentsPerApiAreGivenAggregatedConsentIsSet_PpApiOnly()
throws RemoteException, IOException {
when(mMockFlags.getGaUxFeatureEnabled()).thenReturn(true);
@@ -3340,6 +3673,31 @@
assertThat(mConsentDatastore.get(MANUAL_INTERACTION_WITH_CONSENT_RECORDED)).isTrue();
}
+ @Test
+ public void testManualInteractionWithConsentRecorded_appSearchOnly() throws RemoteException {
+ int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ when(mMockFlags.getEnableAppsearchConsentData()).thenReturn(true);
+ ConsentManager spyConsentManager =
+ getSpiedConsentManagerForMigrationTesting(
+ /* isGiven */ false, consentSourceOfTruth);
+
+ when(mAppSearchConsentManager.getUserManualInteractionWithConsent()).thenReturn(UNKNOWN);
+ assertThat(spyConsentManager.getUserManualInteractionWithConsent()).isEqualTo(UNKNOWN);
+ verify(mAppSearchConsentManager).getUserManualInteractionWithConsent();
+ verify(mMockIAdServicesManager, never()).getUserManualInteractionWithConsent();
+
+ spyConsentManager.recordUserManualInteractionWithConsent(MANUAL_INTERACTIONS_RECORDED);
+ verify(mAppSearchConsentManager)
+ .recordUserManualInteractionWithConsent(MANUAL_INTERACTIONS_RECORDED);
+ when(mAppSearchConsentManager.getUserManualInteractionWithConsent())
+ .thenReturn(MANUAL_INTERACTIONS_RECORDED);
+ assertThat(spyConsentManager.getUserManualInteractionWithConsent())
+ .isEqualTo(MANUAL_INTERACTIONS_RECORDED);
+
+ verify(mMockIAdServicesManager, never()).getUserManualInteractionWithConsent();
+ verify(mMockIAdServicesManager, never()).recordUserManualInteractionWithConsent(anyInt());
+ }
+
// Note this method needs to be invoked after other private variables are initialized.
private ConsentManager getConsentManagerByConsentSourceOfTruth(int consentSourceOfTruth) {
return new ConsentManager(
@@ -3353,7 +3711,7 @@
mAppInstallDaoMock,
mAdServicesManager,
mConsentDatastore,
- mAppSearchConsentService,
+ mAppSearchConsentManager,
mMockFlags,
consentSourceOfTruth);
}
@@ -3377,7 +3735,7 @@
doNothing().when(mMockIAdServicesManager).recordGaUxNotificationDisplayed();
doReturn(UNKNOWN).when(mMockIAdServicesManager).getUserManualInteractionWithConsent();
doNothing().when(mMockIAdServicesManager).recordUserManualInteractionWithConsent(anyInt());
- doReturn(isGiven).when(mAppSearchConsentService).getConsent(CONSENT_KEY);
+ doReturn(isGiven).when(mAppSearchConsentManager).getConsent(CONSENT_KEY);
return consentManager;
}
@@ -3406,7 +3764,7 @@
doNothing().when(mMockIAdServicesManager).recordGaUxNotificationDisplayed();
doReturn(UNKNOWN).when(mMockIAdServicesManager).getUserManualInteractionWithConsent();
doNothing().when(mMockIAdServicesManager).recordUserManualInteractionWithConsent(anyInt());
- doReturn(isGiven).when(mAppSearchConsentService).getConsent(any());
+ doReturn(isGiven).when(mAppSearchConsentManager).getConsent(any());
return consentManager;
}
@@ -3608,4 +3966,31 @@
PrivacySandboxFeatureType.PRIVACY_SANDBOX_RECONSENT.name()))
.isTrue();
}
+
+ @Test
+ public void testCurrentPrivacySandboxFeature_appSearchOnly() throws RemoteException {
+ int consentSourceOfTruth = Flags.APPSEARCH_ONLY;
+ when(mMockFlags.getEnableAppsearchConsentData()).thenReturn(true);
+ ConsentManager spyConsentManager =
+ getSpiedConsentManagerForMigrationTesting(
+ /* isGiven */ false, consentSourceOfTruth);
+
+ when(mAppSearchConsentManager.getCurrentPrivacySandboxFeature())
+ .thenReturn(PrivacySandboxFeatureType.PRIVACY_SANDBOX_UNSUPPORTED);
+ assertThat(spyConsentManager.getCurrentPrivacySandboxFeature())
+ .isEqualTo(PrivacySandboxFeatureType.PRIVACY_SANDBOX_UNSUPPORTED);
+
+ spyConsentManager.setCurrentPrivacySandboxFeature(
+ PrivacySandboxFeatureType.PRIVACY_SANDBOX_FIRST_CONSENT);
+ verify(mAppSearchConsentManager)
+ .setCurrentPrivacySandboxFeature(
+ eq(PrivacySandboxFeatureType.PRIVACY_SANDBOX_FIRST_CONSENT));
+ when(mAppSearchConsentManager.getCurrentPrivacySandboxFeature())
+ .thenReturn(PrivacySandboxFeatureType.PRIVACY_SANDBOX_FIRST_CONSENT);
+ assertThat(spyConsentManager.getCurrentPrivacySandboxFeature())
+ .isEqualTo(PrivacySandboxFeatureType.PRIVACY_SANDBOX_FIRST_CONSENT);
+
+ verify(mMockIAdServicesManager, never()).getCurrentPrivacySandboxFeature();
+ verify(mMockIAdServicesManager, never()).setCurrentPrivacySandboxFeature(anyString());
+ }
}