blob: ceb6843a05ba5bba647bb82ec3c5fa2c5cde90cf [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.data.topics;
import android.annotation.NonNull;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.util.Pair;
import com.android.adservices.LogUtil;
import com.android.adservices.data.DbHelper;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/** Data Access Object for the Topics API. */
public class TopicsDao {
private static TopicsDao sSingleton;
// TODO(b/227393493): Should support a test to notify if new table is added.
private static final String[] ALL_TOPICS_TABLES = {
TopicsTables.TaxonomyContract.TABLE,
TopicsTables.AppClassificationTopicsContract.TABLE,
TopicsTables.AppUsageHistoryContract.TABLE,
TopicsTables.UsageHistoryContract.TABLE,
TopicsTables.CallerCanLearnTopicsContract.TABLE,
TopicsTables.ReturnedTopicContract.TABLE,
TopicsTables.TopTopicsContract.TABLE,
TopicsTables.BlockedTopicsContract.TABLE,
TopicsTables.EpochOriginContract.TABLE,
};
private final DbHelper mDbHelper; // Used in tests.
/**
* It's only public to unit test.
*
* @param dbHelper The database to query
*/
@VisibleForTesting
public TopicsDao(DbHelper dbHelper) {
mDbHelper = dbHelper;
}
/** Returns an instance of the TopicsDAO given a context. */
@NonNull
public static TopicsDao getInstance(@NonNull Context context) {
synchronized (TopicsDao.class) {
if (sSingleton == null) {
sSingleton = new TopicsDao(DbHelper.getInstance(context));
}
return sSingleton;
}
}
/**
* Persist the apps and their classification topics.
*
* @param epochId the epoch ID to persist
* @param appClassificationTopicsMap Map of app -> classified topics
*/
@VisibleForTesting
public void persistAppClassificationTopics(
long epochId, @NonNull Map<String, List<Topic>> appClassificationTopicsMap) {
Objects.requireNonNull(appClassificationTopicsMap);
SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
if (db == null) {
return;
}
for (Map.Entry<String, List<Topic>> entry : appClassificationTopicsMap.entrySet()) {
String app = entry.getKey();
// save each topic in the list by app -> topic mapping in the DB
for (Topic topic : entry.getValue()) {
ContentValues values = new ContentValues();
values.put(TopicsTables.AppClassificationTopicsContract.EPOCH_ID, epochId);
values.put(TopicsTables.AppClassificationTopicsContract.APP, app);
values.put(
TopicsTables.AppClassificationTopicsContract.TAXONOMY_VERSION,
topic.getTaxonomyVersion());
values.put(
TopicsTables.AppClassificationTopicsContract.MODEL_VERSION,
topic.getModelVersion());
values.put(TopicsTables.AppClassificationTopicsContract.TOPIC, topic.getTopic());
try {
db.insert(
TopicsTables.AppClassificationTopicsContract.TABLE,
/* nullColumnHack */ null,
values);
} catch (SQLException e) {
LogUtil.e("Failed to persist classified Topics. Exception : " + e.getMessage());
}
}
}
}
/**
* Get the map of apps and their classification topics.
*
* @param epochId the epoch ID to retrieve
* @return {@link Map} a map of app -> topics
*/
@VisibleForTesting
@NonNull
public Map<String, List<Topic>> retrieveAppClassificationTopics(long epochId) {
SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
Map<String, List<Topic>> appTopicsMap = new HashMap<>();
if (db == null) {
return appTopicsMap;
}
String[] projection = {
TopicsTables.AppClassificationTopicsContract.APP,
TopicsTables.AppClassificationTopicsContract.TAXONOMY_VERSION,
TopicsTables.AppClassificationTopicsContract.MODEL_VERSION,
TopicsTables.AppClassificationTopicsContract.TOPIC,
};
String selection = TopicsTables.AppClassificationTopicsContract.EPOCH_ID + " = ?";
String[] selectionArgs = {String.valueOf(epochId)};
try (Cursor cursor =
db.query(
TopicsTables.AppClassificationTopicsContract.TABLE, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // The sort order
)) {
while (cursor.moveToNext()) {
String app =
cursor.getString(
cursor.getColumnIndexOrThrow(
TopicsTables.AppClassificationTopicsContract.APP));
long taxonomyVersion =
cursor.getLong(
cursor.getColumnIndexOrThrow(
TopicsTables.AppClassificationTopicsContract
.TAXONOMY_VERSION));
long modelVersion =
cursor.getLong(
cursor.getColumnIndexOrThrow(
TopicsTables.AppClassificationTopicsContract
.MODEL_VERSION));
int topicId =
cursor.getInt(
cursor.getColumnIndexOrThrow(
TopicsTables.AppClassificationTopicsContract.TOPIC));
Topic topic = Topic.create(topicId, taxonomyVersion, modelVersion);
List<Topic> list = appTopicsMap.getOrDefault(app, new ArrayList<>());
list.add(topic);
appTopicsMap.put(app, list);
}
}
return appTopicsMap;
}
/**
* Persist the list of Top Topics in this epoch to DB.
*
* @param epochId ID of current epoch
* @param topTopics the topics list to persist into DB
*/
@VisibleForTesting
public void persistTopTopics(long epochId, @NonNull List<Topic> topTopics) {
// topTopics the Top Topics: a list of 5 top topics and the 6th topic
// which was selected randomly. We can refer this 6th topic as the random-topic.
Objects.requireNonNull(topTopics);
Preconditions.checkArgument(topTopics.size() == 6);
SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
if (db == null) {
return;
}
ContentValues values = new ContentValues();
values.put(TopicsTables.TopTopicsContract.EPOCH_ID, epochId);
values.put(TopicsTables.TopTopicsContract.TOPIC1, topTopics.get(0).getTopic());
values.put(TopicsTables.TopTopicsContract.TOPIC2, topTopics.get(1).getTopic());
values.put(TopicsTables.TopTopicsContract.TOPIC3, topTopics.get(2).getTopic());
values.put(TopicsTables.TopTopicsContract.TOPIC4, topTopics.get(3).getTopic());
values.put(TopicsTables.TopTopicsContract.TOPIC5, topTopics.get(4).getTopic());
values.put(TopicsTables.TopTopicsContract.RANDOM_TOPIC, topTopics.get(5).getTopic());
// Taxonomy version and model version of all top topics should be the same.
// Therefore, get it from the first top topic.
values.put(
TopicsTables.TopTopicsContract.TAXONOMY_VERSION,
topTopics.get(0).getTaxonomyVersion());
values.put(
TopicsTables.TopTopicsContract.MODEL_VERSION, topTopics.get(0).getModelVersion());
try {
db.insert(TopicsTables.TopTopicsContract.TABLE, /* nullColumnHack */ null, values);
} catch (SQLException e) {
LogUtil.e("Failed to persist Top Topics. Exception : " + e.getMessage());
}
}
/**
* Return the Top Topics. This will retrieve a list of 5 top topics and the 6th random topic
* from DB.
*
* @param epochId the epochId to retrieve the top topics.
* @return {@link List} a {@link List} of {@link Topic}
*/
@VisibleForTesting
@NonNull
public List<Topic> retrieveTopTopics(long epochId) {
SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
if (db == null) {
return new ArrayList<>();
}
String[] projection = {
TopicsTables.TopTopicsContract.TOPIC1,
TopicsTables.TopTopicsContract.TOPIC2,
TopicsTables.TopTopicsContract.TOPIC3,
TopicsTables.TopTopicsContract.TOPIC4,
TopicsTables.TopTopicsContract.TOPIC5,
TopicsTables.TopTopicsContract.RANDOM_TOPIC,
TopicsTables.TopTopicsContract.TAXONOMY_VERSION,
TopicsTables.TopTopicsContract.MODEL_VERSION
};
String selection = TopicsTables.AppClassificationTopicsContract.EPOCH_ID + " = ?";
String[] selectionArgs = {String.valueOf(epochId)};
try (Cursor cursor =
db.query(
TopicsTables.TopTopicsContract.TABLE, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // The sort order
)) {
if (cursor.moveToNext()) {
int topicId1 =
cursor.getInt(
cursor.getColumnIndexOrThrow(
TopicsTables.TopTopicsContract.TOPIC1));
int topicId2 =
cursor.getInt(
cursor.getColumnIndexOrThrow(
TopicsTables.TopTopicsContract.TOPIC2));
int topicId3 =
cursor.getInt(
cursor.getColumnIndexOrThrow(
TopicsTables.TopTopicsContract.TOPIC3));
int topicId4 =
cursor.getInt(
cursor.getColumnIndexOrThrow(
TopicsTables.TopTopicsContract.TOPIC4));
int topicId5 =
cursor.getInt(
cursor.getColumnIndexOrThrow(
TopicsTables.TopTopicsContract.TOPIC5));
int randomTopicId =
cursor.getInt(
cursor.getColumnIndexOrThrow(
TopicsTables.TopTopicsContract.RANDOM_TOPIC));
long taxonomyVersion =
cursor.getLong(
cursor.getColumnIndexOrThrow(
TopicsTables.TopTopicsContract.TAXONOMY_VERSION));
long modelVersion =
cursor.getLong(
cursor.getColumnIndexOrThrow(
TopicsTables.TopTopicsContract.MODEL_VERSION));
Topic topic1 = Topic.create(topicId1, taxonomyVersion, modelVersion);
Topic topic2 = Topic.create(topicId2, taxonomyVersion, modelVersion);
Topic topic3 = Topic.create(topicId3, taxonomyVersion, modelVersion);
Topic topic4 = Topic.create(topicId4, taxonomyVersion, modelVersion);
Topic topic5 = Topic.create(topicId5, taxonomyVersion, modelVersion);
Topic randomTopic = Topic.create(randomTopicId, taxonomyVersion, modelVersion);
return Arrays.asList(topic1, topic2, topic3, topic4, topic5, randomTopic);
}
}
return new ArrayList<>();
}
/**
* Record the App and SDK into the Usage History table.
*
* @param epochId epochId epoch id to record
* @param app app name
* @param sdk sdk name
*/
public void recordUsageHistory(long epochId, @NonNull String app, @NonNull String sdk) {
Objects.requireNonNull(app);
Objects.requireNonNull(sdk);
Preconditions.checkStringNotEmpty(app);
SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
if (db == null) {
return;
}
// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(TopicsTables.UsageHistoryContract.APP, app);
values.put(TopicsTables.UsageHistoryContract.SDK, sdk);
values.put(TopicsTables.UsageHistoryContract.EPOCH_ID, epochId);
try {
db.insert(TopicsTables.UsageHistoryContract.TABLE, /* nullColumnHack */ null, values);
} catch (SQLException e) {
LogUtil.e("Failed to record App-Sdk usage history." + e.getMessage());
}
}
/**
* Record the usage history for app only
*
* @param epochId epoch id to record
* @param app app name
*/
public void recordAppUsageHistory(long epochId, @NonNull String app) {
Objects.requireNonNull(app);
Preconditions.checkStringNotEmpty(app);
SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
if (db == null) {
return;
}
// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(TopicsTables.AppUsageHistoryContract.APP, app);
values.put(TopicsTables.AppUsageHistoryContract.EPOCH_ID, epochId);
try {
db.insert(
TopicsTables.AppUsageHistoryContract.TABLE, /* nullColumnHack */ null, values);
} catch (SQLException e) {
LogUtil.e("Failed to record App Only usage history." + e.getMessage());
}
}
/**
* Return all apps and their SDKs that called Topics API in the epoch.
*
* @param epochId the epoch to retrieve the app and sdk usage for.
* @return Return Map<App, List<SDK>>.
*/
@NonNull
public Map<String, List<String>> retrieveAppSdksUsageMap(long epochId) {
Map<String, List<String>> appSdksUsageMap = new HashMap<>();
SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
if (db == null) {
return appSdksUsageMap;
}
String[] projection = {
TopicsTables.UsageHistoryContract.APP, TopicsTables.UsageHistoryContract.SDK,
};
String selection = TopicsTables.UsageHistoryContract.EPOCH_ID + " = ?";
String[] selectionArgs = {String.valueOf(epochId)};
try (Cursor cursor =
db.query(
/* distinct = */ true,
TopicsTables.UsageHistoryContract.TABLE,
projection,
selection,
selectionArgs,
null,
null,
null,
null)) {
while (cursor.moveToNext()) {
String app =
cursor.getString(
cursor.getColumnIndexOrThrow(
TopicsTables.UsageHistoryContract.APP));
String sdk =
cursor.getString(
cursor.getColumnIndexOrThrow(
TopicsTables.UsageHistoryContract.SDK));
if (!appSdksUsageMap.containsKey(app)) {
appSdksUsageMap.put(app, new ArrayList<>());
}
appSdksUsageMap.get(app).add(sdk);
}
}
return appSdksUsageMap;
}
/**
* Get topic api usage of an app in an epoch.
*
* @param epochId the epoch to retrieve the app usage for.
* @return Map<App, UsageCount>, how many times an app called topics API in this epoch
*/
@NonNull
public Map<String, Integer> retrieveAppUsageMap(long epochId) {
Map<String, Integer> appUsageMap = new HashMap<>();
SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
if (db == null) {
return appUsageMap;
}
String[] projection = {
TopicsTables.AppUsageHistoryContract.APP,
};
String selection = TopicsTables.AppUsageHistoryContract.EPOCH_ID + " = ?";
String[] selectionArgs = {String.valueOf(epochId)};
try (Cursor cursor =
db.query(
TopicsTables.AppUsageHistoryContract.TABLE,
projection,
selection,
selectionArgs,
null,
null,
null,
null)) {
while (cursor.moveToNext()) {
String app =
cursor.getString(
cursor.getColumnIndexOrThrow(
TopicsTables.AppUsageHistoryContract.APP));
appUsageMap.put(app, appUsageMap.getOrDefault(app, 0) + 1);
}
}
return appUsageMap;
}
/**
* Return the list of distinct apps from the table.
*
* @param tableName the table name
* @param appColumnName app Column name for given table
* @return a {@link Set} of unique apps in the table
*/
@NonNull
public Set<String> retrieveDistinctAppsFromTable(
@NonNull String tableName, @NonNull String appColumnName) {
Set<String> apps = new HashSet<>();
SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
if (db == null) {
return apps;
}
String[] projection = {appColumnName};
try (Cursor cursor =
db.query(
/* distinct */ true,
tableName,
projection,
null,
null,
null,
null,
null,
null)) {
while (cursor.moveToNext()) {
String app = cursor.getString(cursor.getColumnIndexOrThrow(appColumnName));
apps.add(app);
}
}
return apps;
}
// TODO(b/236764602): Create a Caller Class.
/**
* Persist the Callers can learn topic map to DB.
*
* @param epochId the epoch ID.
* @param callerCanLearnMap callerCanLearnMap = {@code Map<Topic, Set<Caller>>} This is a Map
* from Topic to set of App or Sdk (Caller = App or Sdk) that can learn about that topic.
* This is similar to the table Can Learn Topic in the explainer.
*/
public void persistCallerCanLearnTopics(
long epochId, @NonNull Map<Topic, Set<String>> callerCanLearnMap) {
SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
if (db == null) {
return;
}
for (Map.Entry<Topic, Set<String>> entry : callerCanLearnMap.entrySet()) {
Topic topic = entry.getKey();
Set<String> callers = entry.getValue();
for (String caller : callers) {
ContentValues values = new ContentValues();
values.put(TopicsTables.CallerCanLearnTopicsContract.CALLER, caller);
values.put(TopicsTables.CallerCanLearnTopicsContract.TOPIC, topic.getTopic());
values.put(TopicsTables.CallerCanLearnTopicsContract.EPOCH_ID, epochId);
values.put(
TopicsTables.CallerCanLearnTopicsContract.TAXONOMY_VERSION,
topic.getTaxonomyVersion());
values.put(
TopicsTables.CallerCanLearnTopicsContract.MODEL_VERSION,
topic.getModelVersion());
try {
db.insert(
TopicsTables.CallerCanLearnTopicsContract.TABLE,
/* nullColumnHack */ null,
values);
} catch (SQLException e) {
LogUtil.e(e, "Failed to record can learn topic.");
}
}
}
}
/**
* Retrieve the CallersCanLearnTopicsMap This is a Map from Topic to set of App or Sdk (Caller =
* App or Sdk) that can learn about that topic. This is similar to the table Can Learn Topic in
* the explainer. We will look back numberOfLookBackEpochs epochs. The current explainer uses 3
* past epochs. Basically we select epochId between [epochId - numberOfLookBackEpochs + 1,
* epochId]
*
* @param epochId the epochId
* @param numberOfLookBackEpochs Look back numberOfLookBackEpochs.
* @return {@link Map} a Map<Topic, Set<Caller>> where Caller = App or Sdk.
*/
@VisibleForTesting
@NonNull
public Map<Topic, Set<String>> retrieveCallerCanLearnTopicsMap(
long epochId, int numberOfLookBackEpochs) {
Preconditions.checkArgumentPositive(
numberOfLookBackEpochs, "numberOfLookBackEpochs must be positive!");
Map<Topic, Set<String>> callerCanLearnMap = new HashMap<>();
SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
if (db == null) {
return callerCanLearnMap;
}
String[] projection = {
TopicsTables.CallerCanLearnTopicsContract.CALLER,
TopicsTables.CallerCanLearnTopicsContract.TOPIC,
TopicsTables.CallerCanLearnTopicsContract.TAXONOMY_VERSION,
TopicsTables.CallerCanLearnTopicsContract.MODEL_VERSION,
};
// Select epochId between [epochId - numberOfLookBackEpochs + 1, epochId]
String selection =
" ? <= "
+ TopicsTables.CallerCanLearnTopicsContract.EPOCH_ID
+ " AND "
+ TopicsTables.CallerCanLearnTopicsContract.EPOCH_ID
+ " <= ?";
String[] selectionArgs = {
String.valueOf(epochId - numberOfLookBackEpochs + 1), String.valueOf(epochId)
};
try (Cursor cursor =
db.query(
/* distinct = */ true,
TopicsTables.CallerCanLearnTopicsContract.TABLE,
projection,
selection,
selectionArgs,
null,
null,
null,
null)) {
if (cursor == null) {
return callerCanLearnMap;
}
while (cursor.moveToNext()) {
String caller =
cursor.getString(
cursor.getColumnIndexOrThrow(
TopicsTables.CallerCanLearnTopicsContract.CALLER));
int topicId =
cursor.getInt(
cursor.getColumnIndexOrThrow(
TopicsTables.CallerCanLearnTopicsContract.TOPIC));
long taxonomyVersion =
cursor.getLong(
cursor.getColumnIndexOrThrow(
TopicsTables.CallerCanLearnTopicsContract
.TAXONOMY_VERSION));
long modelVersion =
cursor.getLong(
cursor.getColumnIndexOrThrow(
TopicsTables.CallerCanLearnTopicsContract.MODEL_VERSION));
Topic topic = Topic.create(topicId, taxonomyVersion, modelVersion);
if (!callerCanLearnMap.containsKey(topic)) {
callerCanLearnMap.put(topic, new HashSet<>());
}
callerCanLearnMap.get(topic).add(caller);
}
}
return callerCanLearnMap;
}
// TODO(b/236759629): Add a validation to ensure same topic for an app.
/**
* Persist the Apps, Sdks returned topics to DB.
*
* @param epochId the epoch ID
* @param returnedAppSdkTopics {@link Map} a Map<Pair<app, sdk>, Topic>
*/
public void persistReturnedAppTopicsMap(
long epochId, @NonNull Map<Pair<String, String>, Topic> returnedAppSdkTopics) {
SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
if (db == null) {
return;
}
for (Map.Entry<Pair<String, String>, Topic> app : returnedAppSdkTopics.entrySet()) {
// Entry: Key = <Pair<App, Sdk>, Value = Topic.
ContentValues values = new ContentValues();
values.put(TopicsTables.ReturnedTopicContract.EPOCH_ID, epochId);
values.put(TopicsTables.ReturnedTopicContract.APP, app.getKey().first);
values.put(TopicsTables.ReturnedTopicContract.SDK, app.getKey().second);
values.put(TopicsTables.ReturnedTopicContract.TOPIC, app.getValue().getTopic());
values.put(
TopicsTables.ReturnedTopicContract.TAXONOMY_VERSION,
app.getValue().getTaxonomyVersion());
values.put(
TopicsTables.ReturnedTopicContract.MODEL_VERSION,
app.getValue().getModelVersion());
try {
db.insert(
TopicsTables.ReturnedTopicContract.TABLE,
/* nullColumnHack */ null,
values);
} catch (SQLException e) {
LogUtil.e(e, "Failed to record returned topic.");
}
}
}
/**
* Retrieve from the Topics ReturnedTopics Table and populate into the map. Will return topics
* for epoch with epochId in [epochId - numberOfLookBackEpochs + 1, epochId]
*
* @param epochId the current epochId
* @param numberOfLookBackEpochs How many epoch to look back. The current explainer uses 3
* epochs
* @return a {@link Map} in type {@code Map<EpochId, Map < Pair < App, Sdk>, Topic>}
*/
@NonNull
public Map<Long, Map<Pair<String, String>, Topic>> retrieveReturnedTopics(
long epochId, int numberOfLookBackEpochs) {
Map<Long, Map<Pair<String, String>, Topic>> topicsMap = new HashMap<>();
SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
if (db == null) {
return topicsMap;
}
String[] projection = {
TopicsTables.ReturnedTopicContract.EPOCH_ID,
TopicsTables.ReturnedTopicContract.APP,
TopicsTables.ReturnedTopicContract.SDK,
TopicsTables.ReturnedTopicContract.TAXONOMY_VERSION,
TopicsTables.ReturnedTopicContract.MODEL_VERSION,
TopicsTables.ReturnedTopicContract.TOPIC,
};
// Select epochId between [epochId - numberOfLookBackEpochs + 1, epochId]
String selection =
" ? <= "
+ TopicsTables.ReturnedTopicContract.EPOCH_ID
+ " AND "
+ TopicsTables.ReturnedTopicContract.EPOCH_ID
+ " <= ?";
String[] selectionArgs = {
String.valueOf(epochId - numberOfLookBackEpochs + 1), String.valueOf(epochId)
};
try (Cursor cursor =
db.query(
TopicsTables.ReturnedTopicContract.TABLE, // The table to query
projection, // The array of columns to return (pass null to get all)
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // The sort order
)) {
if (cursor == null) {
return topicsMap;
}
while (cursor.moveToNext()) {
long cursorEpochId =
cursor.getLong(
cursor.getColumnIndexOrThrow(
TopicsTables.ReturnedTopicContract.EPOCH_ID));
String app =
cursor.getString(
cursor.getColumnIndexOrThrow(
TopicsTables.ReturnedTopicContract.APP));
String sdk =
cursor.getString(
cursor.getColumnIndexOrThrow(
TopicsTables.ReturnedTopicContract.SDK));
long taxonomyVersion =
cursor.getLong(
cursor.getColumnIndexOrThrow(
TopicsTables.ReturnedTopicContract.TAXONOMY_VERSION));
long modelVersion =
cursor.getInt(
cursor.getColumnIndexOrThrow(
TopicsTables.ReturnedTopicContract.MODEL_VERSION));
int topicId =
cursor.getInt(
cursor.getColumnIndexOrThrow(
TopicsTables.ReturnedTopicContract.TOPIC));
// Building Map<EpochId, Map<Pair<AppId, AdTechId>, Topic>
if (!topicsMap.containsKey(cursorEpochId)) {
topicsMap.put(cursorEpochId, new HashMap<>());
}
Topic topic = Topic.create(topicId, taxonomyVersion, modelVersion);
topicsMap.get(cursorEpochId).put(Pair.create(app, sdk), topic);
}
}
return topicsMap;
}
/**
* Record {@link Topic} which should be blocked.
*
* @param topic {@link Topic} to block.
*/
public void recordBlockedTopic(@NonNull Topic topic) {
Objects.requireNonNull(topic);
SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
if (db == null) {
return;
}
// Create a new map of values, where column names are the keys
ContentValues values = getContentValuesForBlockedTopic(topic);
try {
db.insert(TopicsTables.BlockedTopicsContract.TABLE, /* nullColumnHack */ null, values);
} catch (SQLException e) {
LogUtil.e("Failed to record blocked topic." + e.getMessage());
}
}
@NonNull
private ContentValues getContentValuesForBlockedTopic(@NonNull Topic topic) {
// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(TopicsTables.BlockedTopicsContract.TOPIC, topic.getTopic());
values.put(TopicsTables.BlockedTopicsContract.TAXONOMY_VERSION, topic.getTaxonomyVersion());
values.put(TopicsTables.BlockedTopicsContract.MODEL_VERSION, topic.getModelVersion());
return values;
}
/**
* Remove blocked {@link Topic}.
*
* @param topic blocked {@link Topic} to remove.
*/
public void removeBlockedTopic(@NonNull Topic topic) {
Objects.requireNonNull(topic);
SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
if (db == null) {
return;
}
// Where statement for triplet: topics, taxonomyVersion, modelVersion
String whereClause =
" ? = "
+ TopicsTables.BlockedTopicsContract.TOPIC
+ " AND "
+ TopicsTables.BlockedTopicsContract.TAXONOMY_VERSION
+ " = ?"
+ " AND "
+ TopicsTables.BlockedTopicsContract.MODEL_VERSION
+ " = ?";
String[] whereArgs = {
String.valueOf(topic.getTopic()),
String.valueOf(topic.getTaxonomyVersion()),
String.valueOf(topic.getModelVersion())
};
try {
db.delete(TopicsTables.BlockedTopicsContract.TABLE, whereClause, whereArgs);
} catch (SQLException e) {
LogUtil.e("Failed to record blocked topic." + e.getMessage());
}
}
/**
* Get a {@link List} of {@link Topic}s which are blocked.
*
* @return {@link List} a {@link List} of blocked {@link Topic}s.s
*/
@NonNull
public List<Topic> retrieveAllBlockedTopics() {
SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
List<Topic> blockedTopics = new ArrayList<>();
if (db == null) {
return blockedTopics;
}
try (Cursor cursor =
db.query(
/* distinct = */ true,
TopicsTables.BlockedTopicsContract.TABLE, // The table to query
null, // Get all columns (null for all)
null, // Select all columns (null for all)
null, // Select all columns (null for all)
null, // Don't group the rows
null, // Don't filter by row groups
null, // don't sort
null // don't limit
)) {
while (cursor.moveToNext()) {
long taxonomyVersion =
cursor.getLong(
cursor.getColumnIndexOrThrow(
TopicsTables.BlockedTopicsContract.TAXONOMY_VERSION));
long modelVersion =
cursor.getLong(
cursor.getColumnIndexOrThrow(
TopicsTables.BlockedTopicsContract.MODEL_VERSION));
int topicInt =
cursor.getInt(
cursor.getColumnIndexOrThrow(
TopicsTables.BlockedTopicsContract.TOPIC));
Topic topic = Topic.create(topicInt, taxonomyVersion, modelVersion);
blockedTopics.add(topic);
}
}
return blockedTopics;
}
/**
* Delete from epoch-related tables for data older than/equal to certain epoch in DB.
*
* @param tableName the table to delete data from
* @param epochColumnName epoch Column name for given table
* @param epochToDeleteFrom the epoch to delete starting from (inclusive)
*/
public void deleteDataOfOldEpochs(
@NonNull String tableName, @NonNull String epochColumnName, long epochToDeleteFrom) {
SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
if (db == null) {
return;
}
// Delete epochId before epochToDeleteFrom (including epochToDeleteFrom)
String deletion = " " + epochColumnName + " <= ?";
String[] deletionArgs = {String.valueOf(epochToDeleteFrom)};
try {
db.delete(tableName, deletion, deletionArgs);
} catch (SQLException e) {
LogUtil.e(e, "Failed to delete old epochs' data.");
}
}
/**
* Delete all data generated by Topics API, except for tables in the exclusion list.
*
* @param tablesToExclude a {@link List} of tables that won't be deleted.
*/
public void deleteAllTopicsTables(@NonNull List<String> tablesToExclude) {
SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
if (db == null) {
return;
}
// Handle this in a transaction.
db.beginTransaction();
try {
for (String table : ALL_TOPICS_TABLES) {
if (!tablesToExclude.contains(table)) {
db.delete(table, /* whereClause = */ null, /* whereArgs = */ null);
}
}
// Mark the transaction successful.
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
/**
* Erase all data in a table that associates with a certain application
*
* @param tableName the table to remove data from
* @param appColumnName the column name in the table that represents the name of an application
* @param appNames a {@link List} of apps to wipe out data for
*/
public void deleteAppFromTable(
@NonNull String tableName,
@NonNull String appColumnName,
@NonNull List<String> appNames) {
Objects.requireNonNull(tableName);
Objects.requireNonNull(appColumnName);
Objects.requireNonNull(appNames);
SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
if (db == null || appNames.isEmpty()) {
return;
}
// Construct the "IN" part of SQL Query
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("(?");
for (int i = 0; i < appNames.size() - 1; i++) {
stringBuilder.append(",?");
}
stringBuilder.append(')');
String whereClause = appColumnName + " IN " + stringBuilder;
String[] whereArgs = appNames.toArray(new String[0]);
try {
db.delete(tableName, whereClause, whereArgs);
} catch (SQLException e) {
LogUtil.e(e, String.format("Failed to delete %s in table %s.", appNames, tableName));
}
}
/**
* Persist the origin's timestamp of epoch service in milliseconds into database.
*
* @param originTimestampMs the timestamp user first calls Topics API
*/
public void persistEpochOrigin(long originTimestampMs) {
SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
if (db == null) {
return;
}
ContentValues values = new ContentValues();
values.put(TopicsTables.EpochOriginContract.ORIGIN, originTimestampMs);
try {
db.insert(TopicsTables.EpochOriginContract.TABLE, /* nullColumnHack */ null, values);
} catch (SQLException e) {
LogUtil.e("Failed to persist epoch origin." + e.getMessage());
}
}
/**
* Retrieve origin's timestamp of epoch service in milliseconds. If there is no origin persisted
* in database, return -1;
*
* @return the origin's timestamp of epoch service in milliseconds. Return -1 if no origin is
* persisted.
*/
public long retrieveEpochOrigin() {
long origin = -1L;
SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
if (db == null) {
return origin;
}
String[] projection = {
TopicsTables.EpochOriginContract.ORIGIN,
};
try (Cursor cursor =
db.query(
TopicsTables.EpochOriginContract.TABLE, // The table to query
projection, // The array of columns to return (pass null to get all)
null, // The columns for the WHERE clause
null, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
null // The sort order
)) {
// Return the only entry in this table if existed.
if (cursor.moveToNext()) {
origin =
cursor.getLong(
cursor.getColumnIndexOrThrow(
TopicsTables.EpochOriginContract.ORIGIN));
}
}
return origin;
}
}