blob: 20f8d9672d304b5fcd110522c890117e6d80647c [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.adservices.service.topics;
import static com.android.adservices.AdServicesCommon.ADEXTSERVICES_PACKAGE_NAME_SUFFIX;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SHARED_PREF_RESET_FAILURE;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SHARED_PREF_UPDATE_FAILURE;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_CLEAR_ALL_BLOCKED_TOPICS_IN_SYSTEM_SERVER_FAILURE;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_GET_BLOCKED_TOPIC_FAILURE;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_INVALID_BLOCKED_TOPICS_SOURCE_OF_TRUTH;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_RECORD_BLOCKED_TOPICS_FAILURE;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_REMOVE_BLOCKED_TOPIC_FAILURE;
import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS;
import android.annotation.NonNull;
import android.app.adservices.AdServicesManager;
import android.app.adservices.topics.TopicParcel;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.android.adservices.LogUtil;
import com.android.adservices.LoggerFactory;
import com.android.adservices.data.topics.Topic;
import com.android.adservices.data.topics.TopicsDao;
import com.android.adservices.data.topics.TopicsTables;
import com.android.adservices.errorlogging.ErrorLogUtil;
import com.android.adservices.service.Flags;
import com.android.adservices.service.FlagsFactory;
import com.android.adservices.service.appsearch.AppSearchConsentManager;
import com.android.adservices.service.consent.ConsentConstants;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/** Class to manage blocked {@link Topic}s. */
// TODO(b/269798827): Enable for R.
@RequiresApi(Build.VERSION_CODES.S)
public class BlockedTopicsManager {
private static final LoggerFactory.Logger sLogger = LoggerFactory.getTopicsLogger();
private static BlockedTopicsManager sSingleton;
@VisibleForTesting static final String SHARED_PREFS_BLOCKED_TOPICS = "PPAPI_Blocked_Topics";
public static final String SHARED_PREFS_KEY_HAS_MIGRATED =
"BLOCKED_TOPICS_HAS_MIGRATED_TO_SYSTEM_SERVER";
@VisibleForTesting
static final String SHARED_PREFS_KEY_PPAPI_HAS_CLEARED = "BLOCKED_TOPICS_HAS_CLEARED_IN_PPAPI";
private static final String ERROR_MESSAGE_RECORD_BLOCKED_TOPIC =
"Failed to record the blocked topic.";
private static final String ERROR_MESSAGE_REMOVE_BLOCKED_TOPIC =
"Failed to remove the blocked topic.";
private static final String ERROR_MESSAGE_GET_BLOCKED_TOPICS =
"Failed to get all blocked topics.";
private static final String ERROR_MESSAGE_CLEAR_BLOCKED_TOPICS_IN_SYSTEM_SERVER =
"Failed to clear all blocked topics in system server.";
private static final Object LOCK = new Object();
private final TopicsDao mTopicsDao;
private final AdServicesManager mAdServicesManager;
private final int mBlockedTopicsSourceOfTruth;
private final boolean mEnableAppSearchConsent;
private final AppSearchConsentManager mAppSearchConsentManager;
@VisibleForTesting
BlockedTopicsManager(
@NonNull TopicsDao topicsDao,
@Nullable AdServicesManager adServicesManager,
@Nullable AppSearchConsentManager appSearchConsentManager,
@Flags.ConsentSourceOfTruth int blockedTopicsSourceOfTruth,
boolean enableAppSearchConsent) {
Objects.requireNonNull(topicsDao);
if (blockedTopicsSourceOfTruth == Flags.PPAPI_AND_SYSTEM_SERVER
|| blockedTopicsSourceOfTruth == Flags.SYSTEM_SERVER_ONLY) {
Objects.requireNonNull(adServicesManager);
} else if (blockedTopicsSourceOfTruth == Flags.APPSEARCH_ONLY) {
Objects.requireNonNull(appSearchConsentManager);
}
mTopicsDao = topicsDao;
mAdServicesManager = adServicesManager;
mAppSearchConsentManager = appSearchConsentManager;
mBlockedTopicsSourceOfTruth = blockedTopicsSourceOfTruth;
mEnableAppSearchConsent = enableAppSearchConsent;
}
/** Returns an instance of the {@link BlockedTopicsManager} given a context. */
@NonNull
public static BlockedTopicsManager getInstance(Context context) {
synchronized (LOCK) {
if (sSingleton == null) {
// Execute one-time migration of blocked topics if needed.
int blockedTopicsSourceOfTruth =
FlagsFactory.getFlags().getBlockedTopicsSourceOfTruth();
// It is possible that the old value of the flag lingers after OTA until the first
// PH sync. In that case, we should not use the stale value, but use the default
// instead. The next PH sync will restore the T+ value.
if (SdkLevel.isAtLeastT()) {
blockedTopicsSourceOfTruth = Flags.DEFAULT_BLOCKED_TOPICS_SOURCE_OF_TRUTH;
}
AdServicesManager adServicesManager = AdServicesManager.getInstance(context);
TopicsDao topicsDao = TopicsDao.getInstance(context);
handleBlockedTopicsMigrationIfNeeded(
context, topicsDao, adServicesManager, blockedTopicsSourceOfTruth);
boolean enableAppSearchConsent =
FlagsFactory.getFlags().getEnableAppsearchConsentData();
AppSearchConsentManager appSearchConsentManager =
enableAppSearchConsent ? AppSearchConsentManager.getInstance() : null;
sSingleton =
new BlockedTopicsManager(
topicsDao,
adServicesManager,
appSearchConsentManager,
blockedTopicsSourceOfTruth,
enableAppSearchConsent);
}
return sSingleton;
}
}
/**
* Revoke consent for provided {@link Topic} (block topic). This topic will not be returned by
* any of the {@link TopicsWorker} methods.
*
* @param topic {@link Topic} to block.
*/
public void blockTopic(@NonNull Topic topic) {
sLogger.v("BlockedTopicsManager.blockTopic");
synchronized (LOCK) {
try {
switch (mBlockedTopicsSourceOfTruth) {
case Flags.PPAPI_ONLY:
mTopicsDao.recordBlockedTopic(topic);
break;
case Flags.SYSTEM_SERVER_ONLY:
mAdServicesManager.recordBlockedTopic(
List.of(topic.convertTopicToTopicParcel()));
break;
case Flags.PPAPI_AND_SYSTEM_SERVER:
mTopicsDao.recordBlockedTopic(topic);
mAdServicesManager.recordBlockedTopic(
List.of(topic.convertTopicToTopicParcel()));
break;
case Flags.APPSEARCH_ONLY:
if (mEnableAppSearchConsent) {
mAppSearchConsentManager.blockTopic(topic);
}
break;
case Flags.PPAPI_AND_ADEXT_SERVICE:
// Topics not supported on Android R.
throw new IllegalStateException(
"Invalid state: Attempting to block topic using "
+ "PPAPI_AND_ADEXT_SERVICE consent source of "
+ "truth!");
default:
ErrorLogUtil.e(
AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_INVALID_BLOCKED_TOPICS_SOURCE_OF_TRUTH,
AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
throw new RuntimeException(
ConsentConstants
.ERROR_MESSAGE_INVALID_BLOCKED_TOPICS_SOURCE_OF_TRUTH);
}
} catch (RuntimeException e) {
ErrorLogUtil.e(
e,
AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_RECORD_BLOCKED_TOPICS_FAILURE,
AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
throw new RuntimeException(ERROR_MESSAGE_RECORD_BLOCKED_TOPIC, e);
}
}
}
/**
* Restore consent for provided {@link Topic} (unblock the topic). This topic can be returned by
* any of the {@link TopicsWorker} methods.
*
* @param topic {@link Topic} to restore consent for.
*/
public void unblockTopic(@NonNull Topic topic) {
sLogger.v("BlockedTopicsManager.unblockTopic");
synchronized (LOCK) {
try {
switch (mBlockedTopicsSourceOfTruth) {
case Flags.PPAPI_ONLY:
mTopicsDao.removeBlockedTopic(topic);
break;
case Flags.SYSTEM_SERVER_ONLY:
mAdServicesManager.removeBlockedTopic(topic.convertTopicToTopicParcel());
break;
case Flags.PPAPI_AND_SYSTEM_SERVER:
mTopicsDao.removeBlockedTopic(topic);
mAdServicesManager.removeBlockedTopic(topic.convertTopicToTopicParcel());
break;
case Flags.APPSEARCH_ONLY:
if (mEnableAppSearchConsent) {
mAppSearchConsentManager.unblockTopic(topic);
}
break;
case Flags.PPAPI_AND_ADEXT_SERVICE:
// Topics not supported on Android R.
throw new IllegalStateException(
"Invalid state: Attempting to unblock topic using "
+ "PPAPI_AND_ADEXT_SERVICE consent source of "
+ "truth!");
default:
ErrorLogUtil.e(
AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_INVALID_BLOCKED_TOPICS_SOURCE_OF_TRUTH,
AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
throw new RuntimeException(
ConsentConstants
.ERROR_MESSAGE_INVALID_BLOCKED_TOPICS_SOURCE_OF_TRUTH);
}
} catch (RuntimeException e) {
ErrorLogUtil.e(
e,
AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_REMOVE_BLOCKED_TOPIC_FAILURE,
AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
throw new RuntimeException(ERROR_MESSAGE_REMOVE_BLOCKED_TOPIC);
}
}
}
/**
* Get a {@link List} of {@link Topic}s which are blocked.
*
* @return {@link List} a {@link List} of blocked {@link Topic}s.
*/
@NonNull
public List<Topic> retrieveAllBlockedTopics() {
synchronized (LOCK) {
try {
switch (mBlockedTopicsSourceOfTruth) {
case Flags.PPAPI_ONLY:
return mTopicsDao.retrieveAllBlockedTopics();
case Flags.SYSTEM_SERVER_ONLY:
// Intentional fallthrough
// In case of PPAPI_AND_SYSTEM_SERVER, read from the system server.
case Flags.PPAPI_AND_SYSTEM_SERVER:
return mAdServicesManager.retrieveAllBlockedTopics().stream()
.map(this::convertTopicParcelToTopic)
.collect(Collectors.toList());
case Flags.APPSEARCH_ONLY:
if (mEnableAppSearchConsent) {
return mAppSearchConsentManager.retrieveAllBlockedTopics();
}
return List.of();
case Flags.PPAPI_AND_ADEXT_SERVICE:
// Topics not supported on Android R.
throw new IllegalStateException(
"Invalid state: Attempting to retrieve blocked topics using "
+ "PPAPI_AND_ADEXT_SERVICE consent source of "
+ "truth!");
default:
ErrorLogUtil.e(
AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_INVALID_BLOCKED_TOPICS_SOURCE_OF_TRUTH,
AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
throw new RuntimeException(
ConsentConstants
.ERROR_MESSAGE_INVALID_BLOCKED_TOPICS_SOURCE_OF_TRUTH);
}
} catch (RuntimeException e) {
ErrorLogUtil.e(
e,
AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_GET_BLOCKED_TOPIC_FAILURE,
AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
sLogger.e("Failed to retrieve blocked topics: ", e);
throw new RuntimeException(ERROR_MESSAGE_GET_BLOCKED_TOPICS);
}
}
}
/**
* Clear preserved blocked topics in system server when the blocked topic source of truth
* contains SYSTEM_SERVER or AppSearch.
*/
public void clearAllBlockedTopics() {
synchronized (LOCK) {
try {
switch (mBlockedTopicsSourceOfTruth) {
case Flags.PPAPI_ONLY:
// Return directly. PPAPI data is handled by
// mCacheManager.clearAllTopicsData() and this method is to only clear
// preserved blocked topics in system server or AppSearch.
break;
case Flags.SYSTEM_SERVER_ONLY:
// Intentional fallthrough
case Flags.PPAPI_AND_SYSTEM_SERVER:
mAdServicesManager.clearAllBlockedTopics();
break;
case Flags.APPSEARCH_ONLY:
if (mEnableAppSearchConsent) {
mAppSearchConsentManager.clearAllBlockedTopics();
}
break;
case Flags.PPAPI_AND_ADEXT_SERVICE:
// Topics not supported on Android R.
throw new IllegalStateException(
"Invalid state: Attempting to clear blocked topics using "
+ "PPAPI_AND_ADEXT_SERVICE consent source of "
+ "truth!");
default:
ErrorLogUtil.e(
AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_INVALID_BLOCKED_TOPICS_SOURCE_OF_TRUTH,
AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
throw new RuntimeException(
ConsentConstants
.ERROR_MESSAGE_INVALID_BLOCKED_TOPICS_SOURCE_OF_TRUTH);
}
} catch (RuntimeException e) {
ErrorLogUtil.e(
e,
AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_CLEAR_ALL_BLOCKED_TOPICS_IN_SYSTEM_SERVER_FAILURE,
AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
throw new RuntimeException(ERROR_MESSAGE_CLEAR_BLOCKED_TOPICS_IN_SYSTEM_SERVER);
}
}
}
// Handle different migration requests based on current blocked topics source of Truth
// PPAPI_ONLY: reset the shared preference to reset status of migrating blocked topics from
// PPAPI to system server. This allows the migration to happen after switching
// the source of truth to system server again.
// PPAPI_AND_SYSTEM_SERVER: migrate blocked topics from PPAPI to system server.
// SYSTEM_SERVER_ONLY: migrate blocked topics from PPAPI to system server and clear PPAPI
// blocked topics
@VisibleForTesting
static void handleBlockedTopicsMigrationIfNeeded(
@NonNull Context context,
@NonNull TopicsDao topicsDao,
@Nullable AdServicesManager adServicesManager,
@Flags.ConsentSourceOfTruth int blockedTopicsSourceOfTruth) {
Objects.requireNonNull(context);
Objects.requireNonNull(topicsDao);
// Migration of data to system server is not done on S-.
if (!SdkLevel.isAtLeastT()) {
return;
}
// On R/S, handleBlockedTopicsMigrationIfNeeded should never be executed.
// It is a T+ feature. On T+, this function should only execute if it's within the
// AdServices APK and not ExtServices. So check if it's within ExtServices,
// and bail out if that's the case on any platform.
String packageName = context.getPackageName();
if (packageName != null && packageName.endsWith(ADEXTSERVICES_PACKAGE_NAME_SUFFIX)) {
LogUtil.d("Aborting attempt to migrate blocked topics in ExtServices");
return;
}
if (blockedTopicsSourceOfTruth == Flags.SYSTEM_SERVER_ONLY
|| blockedTopicsSourceOfTruth == Flags.PPAPI_AND_SYSTEM_SERVER) {
Objects.requireNonNull(adServicesManager);
}
switch (blockedTopicsSourceOfTruth) {
case Flags.PPAPI_ONLY:
// Technically we only need to reset the SHARED_PREFS_KEY_HAS_MIGRATED bit once.
// What we need is clearIfSet operation which is not available in SP. So here we
// always reset the bit since otherwise we need to read the SP to read the value and
// the clear the value.
// The only flow we would do are:
// Case 1: DUAL-> PPAPI if there is a bug in System Server
// Case 2: DUAL -> SYSTEM_SERVER_ONLY: if everything goes smoothly.
resetSharedPreference(context, SHARED_PREFS_KEY_HAS_MIGRATED);
break;
case Flags.PPAPI_AND_SYSTEM_SERVER:
mayMigratePpApiBlockedTopicsToSystemService(context, topicsDao, adServicesManager);
break;
case Flags.SYSTEM_SERVER_ONLY:
mayMigratePpApiBlockedTopicsToSystemService(context, topicsDao, adServicesManager);
mayClearPpApiBlockedTopics(context, topicsDao);
break;
default:
break;
}
}
// Set the shared preference to false for given key.
@VisibleForTesting
static void resetSharedPreference(
@NonNull Context context, @NonNull String sharedPreferenceKey) {
Objects.requireNonNull(context);
Objects.requireNonNull(sharedPreferenceKey);
SharedPreferences sharedPreferences =
context.getSharedPreferences(SHARED_PREFS_BLOCKED_TOPICS, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.remove(sharedPreferenceKey);
if (editor.commit()) {
sLogger.d("Finish resetting shared preference for " + sharedPreferenceKey);
} else {
ErrorLogUtil.e(
AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SHARED_PREF_RESET_FAILURE,
AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
sLogger.e("Failed to reset shared preference for " + sharedPreferenceKey);
}
}
// Perform a one-time migration to migrate existing PPAPI blocked topics.
@VisibleForTesting
// Suppress lint warning for context.getUser in R since this code is unused in R
@SuppressWarnings("NewApi")
static void mayMigratePpApiBlockedTopicsToSystemService(
@NonNull Context context,
@NonNull TopicsDao topicsDao,
@NonNull AdServicesManager adServicesManager) {
Objects.requireNonNull(context);
Objects.requireNonNull(topicsDao);
Objects.requireNonNull(adServicesManager);
// Exit if migration has happened.
SharedPreferences sharedPreferences =
context.getSharedPreferences(SHARED_PREFS_BLOCKED_TOPICS, Context.MODE_PRIVATE);
if (sharedPreferences.getBoolean(SHARED_PREFS_KEY_HAS_MIGRATED, /* defValue */ false)) {
sLogger.v(
"Blocked topics migration has happened to user %d, skip...",
context.getUser().getIdentifier());
return;
}
sLogger.d("Start migrating blocked topics from PPAPI to System Service");
// Migrate blocked topics to System Service.
List<TopicParcel> topicParcels = new ArrayList<>();
for (Topic topic : topicsDao.retrieveAllBlockedTopics()) {
topicParcels.add(topic.convertTopicToTopicParcel());
}
adServicesManager.recordBlockedTopic(topicParcels);
// Save migration has happened into shared preferences.
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(SHARED_PREFS_KEY_HAS_MIGRATED, true);
if (editor.commit()) {
sLogger.d("Finish migrating blocked topics from PPAPI to System Service");
} else {
ErrorLogUtil.e(
AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SHARED_PREF_UPDATE_FAILURE,
AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
sLogger.e(
"Finish migrating blocked topics from PPAPI to System Service but shared"
+ " preference is not updated.");
}
}
// Clear PPAPI blocked topics if fully migrated to use system server blocked topics. This is
// because system blocked topics cannot be migrated back to PPAPI. This data clearing should
// only happen once.
@VisibleForTesting
static void mayClearPpApiBlockedTopics(@NonNull Context context, @NonNull TopicsDao topicsDao) {
// Exit if PPAPI blocked topics has cleared.
SharedPreferences sharedPreferences =
context.getSharedPreferences(SHARED_PREFS_BLOCKED_TOPICS, Context.MODE_PRIVATE);
if (sharedPreferences.getBoolean(
SHARED_PREFS_KEY_PPAPI_HAS_CLEARED, /* defValue */ false)) {
return;
}
sLogger.d("Start clearing blocked topics in PPAPI.");
topicsDao.deleteAllEntriesFromTable(TopicsTables.BlockedTopicsContract.TABLE);
// Save that PPAPI blocked topics has cleared into shared preferences.
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean(SHARED_PREFS_KEY_PPAPI_HAS_CLEARED, true);
if (editor.commit()) {
sLogger.d("Finish clearing blocked topics in PPAPI.");
} else {
ErrorLogUtil.e(
AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SHARED_PREF_UPDATE_FAILURE,
AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
sLogger.e(
"Finish clearing blocked topics in PPAPI but shared preference is not"
+ " updated.");
}
}
private Topic convertTopicParcelToTopic(@NonNull TopicParcel topicParcel) {
return Topic.create(
topicParcel.getTopicId(),
topicParcel.getTaxonomyVersion(),
topicParcel.getModelVersion());
}
}