summaryrefslogtreecommitdiff
path: root/adservices/tests/unittest/service-core/src/com/android/adservices/service/topics/AppUpdateManagerTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'adservices/tests/unittest/service-core/src/com/android/adservices/service/topics/AppUpdateManagerTest.java')
-rw-r--r--adservices/tests/unittest/service-core/src/com/android/adservices/service/topics/AppUpdateManagerTest.java701
1 files changed, 543 insertions, 158 deletions
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/service/topics/AppUpdateManagerTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/service/topics/AppUpdateManagerTest.java
index 392dc77a54..6e3028e92e 100644
--- a/adservices/tests/unittest/service-core/src/com/android/adservices/service/topics/AppUpdateManagerTest.java
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/service/topics/AppUpdateManagerTest.java
@@ -16,14 +16,22 @@
package com.android.adservices.service.topics;
+import static com.android.adservices.service.topics.EpochManager.PADDED_TOP_TOPICS_STRING;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -57,15 +65,18 @@ import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
-import java.util.stream.Collectors;
/** Unit tests for {@link com.android.adservices.service.topics.AppUpdateManager} */
public class AppUpdateManagerTest {
@SuppressWarnings({"unused"})
private static final String TAG = "AppInstallationInfoManagerTest";
+ private static final String EMPTY_SDK = "";
+ private static final long TAXONOMY_VERSION = 1L;
+ private static final long MODEL_VERSION = 1L;
+
private final Context mContext = spy(ApplicationProvider.getApplicationContext());
- private final DbHelper mDbHelper = DbTestUtil.getDbHelperForTest();
+ private final DbHelper mDbHelper = spy(DbTestUtil.getDbHelperForTest());
private AppUpdateManager mAppUpdateManager;
private TopicsDao mTopicsDao;
@@ -89,8 +100,9 @@ public class AppUpdateManagerTest {
DbTestUtil.deleteTable(TopicsTables.ReturnedTopicContract.TABLE);
DbTestUtil.deleteTable(TopicsTables.UsageHistoryContract.TABLE);
DbTestUtil.deleteTable(TopicsTables.AppUsageHistoryContract.TABLE);
+ DbTestUtil.deleteTable(TopicsTables.TopicContributorsContract.TABLE);
- mAppUpdateManager = new AppUpdateManager(mTopicsDao, new Random(), mMockFlags);
+ mAppUpdateManager = new AppUpdateManager(mDbHelper, mTopicsDao, new Random(), mMockFlags);
}
@Test
@@ -110,13 +122,11 @@ public class AppUpdateManagerTest {
// Begin to persist data into database
// Handle AppClassificationTopicsContract
- final long taxonomyVersion = 1L;
- final long modelVersion = 1L;
final long epochId1 = 1L;
final int topicId1 = 1;
final int numberOfLookBackEpochs = 1;
- Topic topic1 = Topic.create(topicId1, taxonomyVersion, modelVersion);
+ Topic topic1 = Topic.create(topicId1, TAXONOMY_VERSION, MODEL_VERSION);
Map<String, List<Topic>> appClassificationTopicsMap1 = new HashMap<>();
appClassificationTopicsMap1.put(app1, Collections.singletonList(topic1));
@@ -130,9 +140,9 @@ public class AppUpdateManagerTest {
// Handle UsageHistoryContract
final String sdk1 = "sdk1";
- mTopicsDao.recordUsageHistory(epochId1, app1, "");
+ mTopicsDao.recordUsageHistory(epochId1, app1, EMPTY_SDK);
mTopicsDao.recordUsageHistory(epochId1, app1, sdk1);
- mTopicsDao.recordUsageHistory(epochId1, app2, "");
+ mTopicsDao.recordUsageHistory(epochId1, app2, EMPTY_SDK);
mTopicsDao.recordUsageHistory(epochId1, app2, sdk1);
// Verify UsageHistoryContract has both apps
@@ -160,16 +170,16 @@ public class AppUpdateManagerTest {
// Handle ReturnedTopicContract
Map<Pair<String, String>, Topic> returnedAppSdkTopics = new HashMap<>();
- returnedAppSdkTopics.put(Pair.create(app1, /* sdk */ ""), topic1);
+ returnedAppSdkTopics.put(Pair.create(app1, EMPTY_SDK), topic1);
returnedAppSdkTopics.put(Pair.create(app1, sdk1), topic1);
- returnedAppSdkTopics.put(Pair.create(app2, /* sdk */ ""), topic1);
+ returnedAppSdkTopics.put(Pair.create(app2, EMPTY_SDK), topic1);
returnedAppSdkTopics.put(Pair.create(app2, sdk1), topic1);
mTopicsDao.persistReturnedAppTopicsMap(epochId1, returnedAppSdkTopics);
Map<Pair<String, String>, Topic> expectedReturnedTopics = new HashMap<>();
- expectedReturnedTopics.put(Pair.create(app1, /* sdk */ ""), topic1);
+ expectedReturnedTopics.put(Pair.create(app1, EMPTY_SDK), topic1);
expectedReturnedTopics.put(Pair.create(app1, sdk1), topic1);
- expectedReturnedTopics.put(Pair.create(app2, /* sdk */ ""), topic1);
+ expectedReturnedTopics.put(Pair.create(app2, EMPTY_SDK), topic1);
expectedReturnedTopics.put(Pair.create(app2, sdk1), topic1);
// Verify ReturnedTopicContract has both apps
@@ -180,7 +190,7 @@ public class AppUpdateManagerTest {
.isEqualTo(expectedReturnedTopics);
// Reconcile uninstalled applications
- mAppUpdateManager.reconcileUninstalledApps(mContext);
+ mAppUpdateManager.reconcileUninstalledApps(mContext, epochId1);
verify(mContext).getPackageManager();
verify(mMockPackageManager).getInstalledApplications(Mockito.any());
@@ -200,7 +210,7 @@ public class AppUpdateManagerTest {
.doesNotContain(app2);
// Returned Topics Map contains only App1 paris
Map<Pair<String, String>, Topic> expectedReturnedTopicsAfterWiping = new HashMap<>();
- expectedReturnedTopicsAfterWiping.put(Pair.create(app1, /* sdk */ ""), topic1);
+ expectedReturnedTopicsAfterWiping.put(Pair.create(app1, EMPTY_SDK), topic1);
expectedReturnedTopicsAfterWiping.put(Pair.create(app1, sdk1), topic1);
assertThat(
mTopicsDao
@@ -210,6 +220,183 @@ public class AppUpdateManagerTest {
}
@Test
+ public void testReconcileUninstalledApps_handleTopicsWithoutContributor() {
+ // Test Setup:
+ // * Both app1 and app2 have usages in database. app2 won't be current installed app list
+ // that is returned by mocked Package Manager, so it'll be regarded as an unhandled
+ // uninstalled app.
+ // * In Epoch1, app1 is classified to topic1, topic2. app2 is classified to topic1, topic3.
+ // Both app1 and app2 have topic3 as returned topic as they both call Topics API via sdk.
+ // * In Epoch2, both app1 and app2 are classified to topic1, topic3. (verify epoch basis)
+ // * In Epoch3, both app2 and app3 are classified to topic1. app4 learns topic1 from sdk and
+ // also returns topic1. After app2 and app4 are uninstalled, topic1 should be removed for
+ // epoch3 and app3 should have no returned topic. (verify consecutive deletion on a topic)
+ // * In Epoch4, app2 is uninstalled. topic3 will be removed in Epoch1 as it has app2 as the
+ // only contributor, while topic3 will stay in Epoch2 as app2 contributes to it.
+ final String app1 = "app1";
+ final String app2 = "app2";
+ final String sdk = "sdk";
+ final long epoch1 = 1L;
+ final long epoch2 = 2L;
+ final long epoch4 = 4L;
+ final int numberOfLookBackEpochs = 3;
+
+ Topic topic1 = Topic.create(1, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic2 = Topic.create(2, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic3 = Topic.create(3, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic4 = Topic.create(4, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic5 = Topic.create(5, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic6 = Topic.create(6, TAXONOMY_VERSION, MODEL_VERSION);
+
+ // Mock Package Manager for installed applications
+ ApplicationInfo appInfo1 = new ApplicationInfo();
+ appInfo1.packageName = app1;
+
+ when(mMockPackageManager.getInstalledApplications(Mockito.any()))
+ .thenReturn(List.of(appInfo1));
+
+ // Persist to AppClassificationTopics table
+ mTopicsDao.persistAppClassificationTopics(
+ epoch1, Map.of(app1, List.of(topic1, topic2), app2, List.of(topic1, topic3)));
+ mTopicsDao.persistAppClassificationTopics(
+ epoch2, Map.of(app1, List.of(topic1, topic3), app2, List.of(topic1, topic3)));
+
+ // Persist to TopTopics table
+ mTopicsDao.persistTopTopics(
+ epoch1, List.of(topic1, topic2, topic3, topic4, topic5, topic6));
+ mTopicsDao.persistTopTopics(
+ epoch2, List.of(topic1, topic2, topic3, topic4, topic5, topic6));
+
+ // Persist to TopicContributors table
+ mTopicsDao.persistTopicContributors(epoch1, Map.of(topic1.getTopic(), Set.of(app1, app2)));
+ mTopicsDao.persistTopicContributors(epoch1, Map.of(topic2.getTopic(), Set.of(app1)));
+ mTopicsDao.persistTopicContributors(epoch1, Map.of(topic3.getTopic(), Set.of(app2)));
+ mTopicsDao.persistTopicContributors(epoch2, Map.of(topic1.getTopic(), Set.of(app1, app2)));
+ mTopicsDao.persistTopicContributors(epoch2, Map.of(topic3.getTopic(), Set.of(app1, app2)));
+
+ // Persist to ReturnedTopics table
+ mTopicsDao.persistReturnedAppTopicsMap(epoch1, Map.of(Pair.create(app1, sdk), topic3));
+ mTopicsDao.persistReturnedAppTopicsMap(epoch1, Map.of(Pair.create(app2, sdk), topic3));
+ mTopicsDao.persistReturnedAppTopicsMap(epoch2, Map.of(Pair.create(app1, sdk), topic3));
+ mTopicsDao.persistReturnedAppTopicsMap(epoch2, Map.of(Pair.create(app2, sdk), topic3));
+
+ // Mock flag value to remove dependency of actual flag value
+ when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
+ // Enable the feature
+ when(mDbHelper.supportsTopicContributorsTable()).thenReturn(true);
+ when(mMockFlags.getEnableTopicContributorsCheck()).thenReturn(true);
+
+ // Execute reconciliation to handle app2
+ mAppUpdateManager.reconcileUninstalledApps(mContext, epoch4);
+
+ // Verify Returned Topics in [1, 3]. app2 should have no returnedTopics as it's uninstalled.
+ // app1 only has returned topic at Epoch2 as topic3 is removed from Epoch1.
+ Map<Long, Map<Pair<String, String>, Topic>> expectedReturnedTopicsMap =
+ Map.of(epoch2, Map.of(Pair.create(app1, sdk), topic3));
+ assertThat(mTopicsDao.retrieveReturnedTopics(epoch4 - 1, numberOfLookBackEpochs))
+ .isEqualTo(expectedReturnedTopicsMap);
+
+ // Verify TopicContributors Map is updated: app1 should be removed after the uninstallation.
+ assertThat(mTopicsDao.retrieveTopicToContributorsMap(epoch1))
+ .isEqualTo(
+ Map.of(topic1.getTopic(), Set.of(app1), topic2.getTopic(), Set.of(app1)));
+ assertThat(mTopicsDao.retrieveTopicToContributorsMap(epoch2))
+ .isEqualTo(
+ Map.of(topic1.getTopic(), Set.of(app1), topic3.getTopic(), Set.of(app1)));
+ }
+
+ @Test
+ public void testReconcileUninstalledApps_contributorDeletionsToSameTopic() {
+ // Test Setup:
+ // * app1 has usages in database. Both app2 and app3 won't be current installed app list
+ // that is returned by mocked Package Manager, so they'll be regarded as an unhandled
+ // uninstalled apps.
+ // * Both app2 and app3 are contributors to topic1 and return topic1. app1 is not the
+ // contributor but also returns topic1, learnt via same SDK.
+ final String app1 = "app1";
+ final String app2 = "app2";
+ final String app3 = "app3";
+ final String sdk = "sdk";
+ final long epoch1 = 1L;
+ final long epoch2 = 2L;
+ final int numberOfLookBackEpochs = 3;
+
+ Topic topic1 = Topic.create(1, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic2 = Topic.create(2, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic3 = Topic.create(3, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic4 = Topic.create(4, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic5 = Topic.create(5, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic6 = Topic.create(6, TAXONOMY_VERSION, MODEL_VERSION);
+
+ // Mock Package Manager for installed applications
+ ApplicationInfo appInfo1 = new ApplicationInfo();
+ appInfo1.packageName = app1;
+
+ when(mMockPackageManager.getInstalledApplications(Mockito.any()))
+ .thenReturn(List.of(appInfo1));
+
+ // Persist to AppClassificationTopics table
+ mTopicsDao.persistAppClassificationTopics(
+ epoch1, Map.of(app2, List.of(topic1), app3, List.of(topic1)));
+
+ // Persist to TopTopics table
+ mTopicsDao.persistTopTopics(
+ epoch1, List.of(topic1, topic2, topic3, topic4, topic5, topic6));
+
+ // Persist to TopicContributors table
+ mTopicsDao.persistTopicContributors(epoch1, Map.of(topic1.getTopic(), Set.of(app2, app3)));
+
+ // Persist to ReturnedTopics table
+ mTopicsDao.persistReturnedAppTopicsMap(epoch1, Map.of(Pair.create(app1, sdk), topic1));
+ mTopicsDao.persistReturnedAppTopicsMap(epoch1, Map.of(Pair.create(app2, sdk), topic1));
+ mTopicsDao.persistReturnedAppTopicsMap(epoch1, Map.of(Pair.create(app3, sdk), topic1));
+
+ // Mock flag value to remove dependency of actual flag value
+ when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
+ // Enable the feature
+ doReturn(true).when(mDbHelper).supportsTopicContributorsTable();
+ when(mMockFlags.getEnableTopicContributorsCheck()).thenReturn(true);
+
+ // Execute reconciliation to handle app2 and app3
+ mAppUpdateManager.reconcileUninstalledApps(mContext, epoch2);
+
+ // Verify Returned Topics in epoch 1. app2 and app3 are uninstalled, so they definitely
+ // don't have a returned topic. As topic1 has no contributors after uninstallations of app2
+ // and app3, it's removed from database. Therefore, app1 should have no returned topics as
+ // well.
+ assertThat(mTopicsDao.retrieveTopicToContributorsMap(epoch1)).isEmpty();
+ assertThat(mTopicsDao.retrieveReturnedTopics(epoch1, numberOfLookBackEpochs)).isEmpty();
+ }
+
+ @Test
+ public void testReconcileUninstalledApps_disableTopicContributorsCheck() {
+ AppUpdateManager appUpdateManager =
+ spy(new AppUpdateManager(mDbHelper, mTopicsDao, new Random(), mMockFlags));
+
+ // Do not check actual usage of related methods.
+ doNothing().when(appUpdateManager).handleTopTopicsWithoutContributors(anyLong(), any());
+ doNothing().when(appUpdateManager).deleteAppDataFromTableByApps(any());
+ doReturn(Set.of()).when(appUpdateManager).getCurrentInstalledApps(any());
+ doReturn(Set.of("anyValue")).when(appUpdateManager).getUnhandledUninstalledApps(any());
+ doReturn(3).when(mMockFlags).getTopicsNumberOfLookBackEpochs();
+
+ // verify feature is enabled
+ doReturn(false).when(appUpdateManager).supportsTopicContributorFeature();
+ appUpdateManager.reconcileUninstalledApps(mContext, /* any positive long */ 1L);
+ // handleTopTopicsWithoutContributors() is not invoked.
+ verify(appUpdateManager, never()).handleTopTopicsWithoutContributors(anyLong(), any());
+ verify(appUpdateManager).deleteAppDataFromTableByApps(any());
+
+ // verify feature is disabled
+ doReturn(true).when(appUpdateManager).supportsTopicContributorFeature();
+ appUpdateManager.reconcileUninstalledApps(mContext, /* any positive long */ 1L);
+ // handleTopTopicsWithoutContributors() is invoked.
+ verify(appUpdateManager, atLeastOnce())
+ .handleTopTopicsWithoutContributors(anyLong(), any());
+ verify(appUpdateManager, times(2)).deleteAppDataFromTableByApps(any());
+ }
+
+ @Test
public void testGetUnhandledUninstalledApps() {
final long epochId = 1L;
Set<String> currentInstalledApps = Set.of("app1", "app2", "app5");
@@ -222,10 +409,10 @@ public class AppUpdateManagerTest {
mTopicsDao.persistReturnedAppTopicsMap(
epochId,
Map.of(
- Pair.create("app2", ""),
+ Pair.create("app2", EMPTY_SDK),
Topic.create(
/* topic ID */ 1, /* taxonomyVersion */ 1L, /* model version */ 1L),
- Pair.create("app4", ""),
+ Pair.create("app4", EMPTY_SDK),
Topic.create(
/* topic ID */ 1, /* taxonomyVersion */
1L, /* model version */
@@ -252,10 +439,10 @@ public class AppUpdateManagerTest {
mTopicsDao.persistReturnedAppTopicsMap(
epochId,
Map.of(
- Pair.create("app2", ""),
+ Pair.create("app2", EMPTY_SDK),
Topic.create(
/* topic ID */ 1, /* taxonomyVersion */ 1L, /* model version */ 1L),
- Pair.create("app6", ""),
+ Pair.create("app6", EMPTY_SDK),
Topic.create(
/* topic ID */ 1, /* taxonomyVersion */
1L, /* model version */
@@ -280,13 +467,11 @@ public class AppUpdateManagerTest {
// Therefore, database will only contain app1's data.
// Handle AppClassificationTopicsContract
- final long taxonomyVersion = 1L;
- final long modelVersion = 1L;
final long epochId1 = 1L;
final int topicId1 = 1;
final int numberOfLookBackEpochs = 1;
- Topic topic1 = Topic.create(topicId1, taxonomyVersion, modelVersion);
+ Topic topic1 = Topic.create(topicId1, TAXONOMY_VERSION, MODEL_VERSION);
mTopicsDao.persistAppClassificationTopics(epochId1, Map.of(app1, List.of(topic1)));
mTopicsDao.persistAppClassificationTopics(epochId1, Map.of(app2, List.of(topic1)));
@@ -298,11 +483,11 @@ public class AppUpdateManagerTest {
// Handle UsageHistoryContract
final String sdk1 = "sdk1";
- mTopicsDao.recordUsageHistory(epochId1, app1, "");
+ mTopicsDao.recordUsageHistory(epochId1, app1, EMPTY_SDK);
mTopicsDao.recordUsageHistory(epochId1, app1, sdk1);
- mTopicsDao.recordUsageHistory(epochId1, app2, "");
+ mTopicsDao.recordUsageHistory(epochId1, app2, EMPTY_SDK);
mTopicsDao.recordUsageHistory(epochId1, app2, sdk1);
- mTopicsDao.recordUsageHistory(epochId1, app3, "");
+ mTopicsDao.recordUsageHistory(epochId1, app3, EMPTY_SDK);
mTopicsDao.recordUsageHistory(epochId1, app3, sdk1);
// Verify UsageHistoryContract has both apps
@@ -332,11 +517,11 @@ public class AppUpdateManagerTest {
// Handle ReturnedTopicContract
Map<Pair<String, String>, Topic> returnedAppSdkTopics = new HashMap<>();
- returnedAppSdkTopics.put(Pair.create(app1, /* sdk */ ""), topic1);
+ returnedAppSdkTopics.put(Pair.create(app1, EMPTY_SDK), topic1);
returnedAppSdkTopics.put(Pair.create(app1, sdk1), topic1);
- returnedAppSdkTopics.put(Pair.create(app2, /* sdk */ ""), topic1);
+ returnedAppSdkTopics.put(Pair.create(app2, EMPTY_SDK), topic1);
returnedAppSdkTopics.put(Pair.create(app2, sdk1), topic1);
- returnedAppSdkTopics.put(Pair.create(app3, /* sdk */ ""), topic1);
+ returnedAppSdkTopics.put(Pair.create(app3, EMPTY_SDK), topic1);
returnedAppSdkTopics.put(Pair.create(app3, sdk1), topic1);
mTopicsDao.persistReturnedAppTopicsMap(epochId1, returnedAppSdkTopics);
@@ -366,7 +551,7 @@ public class AppUpdateManagerTest {
.isEqualTo(Set.of(app1, sdk1));
// Returned Topics Map contains only App1 paris
Map<Pair<String, String>, Topic> expectedReturnedTopicsAfterWiping = new HashMap<>();
- expectedReturnedTopicsAfterWiping.put(Pair.create(app1, /* sdk */ ""), topic1);
+ expectedReturnedTopicsAfterWiping.put(Pair.create(app1, EMPTY_SDK), topic1);
expectedReturnedTopicsAfterWiping.put(Pair.create(app1, sdk1), topic1);
assertThat(
mTopicsDao
@@ -376,47 +561,43 @@ public class AppUpdateManagerTest {
}
@Test
- public void testDeleteAppDataFromTableByApp_nullUninstalledAppName() {
+ public void testDeleteAppDataFromTableByApps_nullUninstalledAppName() {
assertThrows(
NullPointerException.class,
() -> mAppUpdateManager.deleteAppDataFromTableByApps(null));
}
@Test
- public void testDeleteAppDataFromTableByApp_nonExistingUninstalledAppName() {
+ public void testDeleteAppDataFromTableByApps_nonExistingUninstalledAppName() {
// To test it won't throw by calling the method with non-existing application name
mAppUpdateManager.deleteAppDataFromTableByApps(List.of("app"));
}
@Test
- public void testDeleteAppDataByUri() {
- // Mock AppUpdateManager to check the invocation of deleteAppDataByUri() because
- // the functionality has already been tested in testDeleteAppDataFromTableByApp
- AppUpdateManager appUpdateManager =
- Mockito.spy(new AppUpdateManager(mTopicsDao, new Random(), mMockFlags));
+ public void testDeleteAppDataFromTableByApps_topicContributorsTable() {
+ final long epoch1 = 1L;
- final String appName = "app";
- Uri packageUri = Uri.parse(appName);
-
- // Only verify the invocation of deleteAppDataFromTableByApp()
- doNothing().when(appUpdateManager).deleteAppDataFromTableByApps(eq(List.of(appName)));
-
- appUpdateManager.deleteAppDataByUri(packageUri);
-
- verify(appUpdateManager).deleteAppDataByUri(eq(packageUri));
- verify(appUpdateManager).deleteAppDataFromTableByApps(eq(List.of(appName)));
- }
-
- @Test
- public void testDeleteAppDataByUri_nullUri() {
- assertThrows(NullPointerException.class, () -> mAppUpdateManager.deleteAppDataByUri(null));
- }
-
- @Test
- public void testDeleteAppDataByUri_nonExistingUninstalledAppName() {
- // To test it won't throw by calling the method with Uri containing
- // non-existing application name.
- mAppUpdateManager.deleteAppDataByUri(Uri.parse("app"));
+ final String app = "app";
+ final int topicId = 1;
+
+ Map<Integer, Set<String>> topicContributorsMap = Map.of(topicId, Set.of(app));
+ mTopicsDao.persistTopicContributors(epoch1, topicContributorsMap);
+
+ // Enable Database Version 3
+ when(mDbHelper.supportsTopicContributorsTable()).thenReturn(true);
+
+ // Feature flag is Off
+ when(mMockFlags.getEnableTopicContributorsCheck()).thenReturn(false);
+ mAppUpdateManager.deleteAppDataFromTableByApps(List.of(app));
+ // Table should not be cleared
+ assertThat(mTopicsDao.retrieveTopicToContributorsMap(epoch1))
+ .isEqualTo(topicContributorsMap);
+
+ // Feature flag is On
+ when(mMockFlags.getEnableTopicContributorsCheck()).thenReturn(true);
+ mAppUpdateManager.deleteAppDataFromTableByApps(List.of(app));
+ // Table should be cleared
+ assertThat(mTopicsDao.retrieveTopicToContributorsMap(epoch1)).isEmpty();
}
@Test
@@ -424,11 +605,8 @@ public class AppUpdateManagerTest {
final String app1 = "app1";
final String app2 = "app2";
final long currentEpochId = 4L;
- final long taxonomyVersion = 1L;
- final long modelVersion = 1L;
final int numOfLookBackEpochs = 3;
final int topicsNumberOfTopTopics = 5;
- final int topicsNumberOfRandomTopics = 1;
final int topicsPercentageForRandomTopic = 5;
// As selectAssignedTopicFromTopTopics() randomly assigns a top topic, pass in a Mocked
@@ -445,14 +623,13 @@ public class AppUpdateManagerTest {
topicsPercentageForRandomTopic, // Will select a regular topic
1, // Index of second topic
0, // Will select a random topic
- topicsNumberOfRandomTopics - 1 // Select the last random topic
+ 0, // Select the first random topic
});
AppUpdateManager appUpdateManager =
- new AppUpdateManager(mTopicsDao, mockRandom, mMockFlags);
+ new AppUpdateManager(mDbHelper, mTopicsDao, mockRandom, mMockFlags);
// Mock Flags to get an independent result
when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numOfLookBackEpochs);
when(mMockFlags.getTopicsNumberOfTopTopics()).thenReturn(topicsNumberOfTopTopics);
- when(mMockFlags.getTopicsNumberOfRandomTopics()).thenReturn(topicsNumberOfRandomTopics);
when(mMockFlags.getTopicsPercentageForRandomTopic())
.thenReturn(topicsPercentageForRandomTopic);
@@ -465,12 +642,12 @@ public class AppUpdateManagerTest {
when(mMockPackageManager.getInstalledApplications(Mockito.any()))
.thenReturn(List.of(appInfo1, appInfo2));
- Topic topic1 = Topic.create(/* topic */ 1, taxonomyVersion, modelVersion);
- Topic topic2 = Topic.create(/* topic */ 2, taxonomyVersion, modelVersion);
- Topic topic3 = Topic.create(/* topic */ 3, taxonomyVersion, modelVersion);
- Topic topic4 = Topic.create(/* topic */ 4, taxonomyVersion, modelVersion);
- Topic topic5 = Topic.create(/* topic */ 5, taxonomyVersion, modelVersion);
- Topic topic6 = Topic.create(/* topic */ 6, taxonomyVersion, modelVersion);
+ Topic topic1 = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic2 = Topic.create(/* topic */ 2, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic3 = Topic.create(/* topic */ 3, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic4 = Topic.create(/* topic */ 4, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic5 = Topic.create(/* topic */ 5, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic6 = Topic.create(/* topic */ 6, TAXONOMY_VERSION, MODEL_VERSION);
List<Topic> topTopics = List.of(topic1, topic2, topic3, topic4, topic5, topic6);
// Begin to persist data into database
@@ -479,13 +656,18 @@ public class AppUpdateManagerTest {
// installed app.
mTopicsDao.recordAppUsageHistory(currentEpochId - 1, app1);
// Unused but to mimic what happens in reality
- mTopicsDao.recordUsageHistory(currentEpochId - 1, app1, /* sdk */ "sdk");
+ mTopicsDao.recordUsageHistory(currentEpochId - 1, app1, "sdk");
// Persist top topics into database for last 3 epochs
for (long epochId = currentEpochId - 1;
epochId >= currentEpochId - numOfLookBackEpochs;
epochId--) {
mTopicsDao.persistTopTopics(epochId, topTopics);
+ // Persist topics to TopicContributors Table avoid being filtered out
+ for (Topic topic : topTopics) {
+ mTopicsDao.persistTopicContributors(
+ epochId, Map.of(topic.getTopic(), Set.of(app1, app2)));
+ }
}
// Assign topics to past epochs
@@ -493,27 +675,22 @@ public class AppUpdateManagerTest {
Map<Long, Map<Pair<String, String>, Topic>> expectedReturnedTopics = new HashMap<>();
expectedReturnedTopics.put(
- currentEpochId - 1, Map.of(Pair.create(app2, /* sdk */ ""), topic1));
+ currentEpochId - 1, Map.of(Pair.create(app2, EMPTY_SDK), topic1));
expectedReturnedTopics.put(
- currentEpochId - 2, Map.of(Pair.create(app2, /* sdk */ ""), topic2));
+ currentEpochId - 2, Map.of(Pair.create(app2, EMPTY_SDK), topic2));
expectedReturnedTopics.put(
- currentEpochId - 3, Map.of(Pair.create(app2, /* sdk */ ""), topic6));
+ currentEpochId - 3, Map.of(Pair.create(app2, EMPTY_SDK), topic6));
assertThat(mTopicsDao.retrieveReturnedTopics(currentEpochId - 1, numOfLookBackEpochs))
.isEqualTo(expectedReturnedTopics);
verify(mMockFlags).getTopicsNumberOfLookBackEpochs();
verify(mMockFlags).getTopicsNumberOfTopTopics();
- verify(mMockFlags).getTopicsNumberOfRandomTopics();
verify(mMockFlags).getTopicsPercentageForRandomTopic();
}
@Test
public void testSelectAssignedTopicFromTopTopics() {
- final long taxonomyVersion = 1L;
- final long modelVersion = 1L;
- final int topicsNumberOfTopTopics = 5;
- final int topicsNumberOfRandomTopics = 1;
final int topicsPercentageForRandomTopic = 5;
// Test the randomness with pre-defined values
@@ -521,75 +698,42 @@ public class AppUpdateManagerTest {
new MockRandom(
new long[] {
0, // Will select a random topic
- topicsNumberOfRandomTopics - 1, // Select the last random topic
+ 0, // Select the first random topic
topicsPercentageForRandomTopic, // Will select a regular topic
0 // Select the first regular topic
});
AppUpdateManager appUpdateManager =
- new AppUpdateManager(mTopicsDao, mockRandom, mMockFlags);
+ new AppUpdateManager(mDbHelper, mTopicsDao, mockRandom, mMockFlags);
- Topic topic1 = Topic.create(/* topic */ 1, taxonomyVersion, modelVersion);
- Topic topic2 = Topic.create(/* topic */ 2, taxonomyVersion, modelVersion);
- Topic topic3 = Topic.create(/* topic */ 3, taxonomyVersion, modelVersion);
- Topic topic4 = Topic.create(/* topic */ 4, taxonomyVersion, modelVersion);
- Topic topic5 = Topic.create(/* topic */ 5, taxonomyVersion, modelVersion);
- Topic topic6 = Topic.create(/* topic */ 6, taxonomyVersion, modelVersion);
+ Topic topic1 = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic2 = Topic.create(/* topic */ 2, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic3 = Topic.create(/* topic */ 3, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic6 = Topic.create(/* topic */ 6, TAXONOMY_VERSION, MODEL_VERSION);
- List<Topic> topTopics = Arrays.asList(topic1, topic2, topic3, topic4, topic5, topic6);
+ List<Topic> regularTopics = List.of(topic1, topic2, topic3);
+ List<Topic> randomTopics = List.of(topic6);
- // In the first invocation, mockRandom returns a 0 that indicates a regular top topic will
- // be returned, and following by another 0 to select the first regular top topic.
- Topic regularTopTopic =
- appUpdateManager.selectAssignedTopicFromTopTopics(
- topTopics,
- topicsNumberOfTopTopics,
- topicsNumberOfRandomTopics,
- topicsPercentageForRandomTopic);
- assertThat(regularTopTopic).isEqualTo(topic6);
-
- // In the second invocation, mockRandom returns a 5 that indicates a random top topic will
- // be returned, and following by a 0 to select the first(only) random top topic.
+ // In the first invocation, mockRandom returns a 0 that indicates a random top topic will
+ // be returned, and followed by another 0 to select the first(only) random top topic.
Topic randomTopTopic =
appUpdateManager.selectAssignedTopicFromTopTopics(
- topTopics,
- topicsNumberOfTopTopics,
- topicsNumberOfRandomTopics,
- topicsPercentageForRandomTopic);
- assertThat(randomTopTopic).isEqualTo(topic1);
- }
+ regularTopics, randomTopics, topicsPercentageForRandomTopic);
+ assertThat(randomTopTopic).isEqualTo(topic6);
- @Test
- public void testSelectAssignedTopicFromTopTopics_invalidSize() {
- List<Integer> intTopics = Arrays.asList(1, 2, 3, 4, 5, 6);
- List<Topic> topTopics =
- intTopics.stream()
- .map(
- intTopic ->
- Topic.create(
- intTopic,
- /* Taxonomy Version */ 1L, /* Model Version */
- 1L))
- .collect(Collectors.toList());
- assertThrows(
- IllegalArgumentException.class,
- () ->
- mAppUpdateManager.selectAssignedTopicFromTopTopics(
- topTopics,
- /* topicsNumberOfTopTopics */ 4,
- /* topicsNumberOfRandomTopics */ 1,
- /* topicsPercentageForRandomTopic */ 5));
+ // In the second invocation, mockRandom returns a 5 that indicates a regular top topic will
+ // be returned, and following by a 0 to select the first regular top topic.
+ Topic regularTopTopic =
+ appUpdateManager.selectAssignedTopicFromTopTopics(
+ regularTopics, randomTopics, topicsPercentageForRandomTopic);
+ assertThat(regularTopTopic).isEqualTo(topic1);
}
@Test
public void testAssignTopicsToNewlyInstalledApps() {
final String appName = "app";
- Uri packageUri = Uri.parse(appName);
final long currentEpochId = 4L;
- final long taxonomyVersion = 1L;
- final long modelVersion = 1L;
final int numOfLookBackEpochs = 3;
final int topicsNumberOfTopTopics = 5;
- final int topicsNumberOfRandomTopics = 1;
final int topicsPercentageForRandomTopic = 5;
// As selectAssignedTopicFromTopTopics() randomly assigns a top topic, pass in a Mocked
@@ -606,26 +750,28 @@ public class AppUpdateManagerTest {
topicsPercentageForRandomTopic, // Will select a regular topic
1, // Index of second topic
0, // Will select a random topic
- topicsNumberOfRandomTopics - 1 // Select the last random topic
+ 0, // Select the first random topic
});
// Spy an instance of AppUpdateManager in order to mock selectAssignedTopicFromTopTopics()
// to avoid randomness.
AppUpdateManager appUpdateManager =
- new AppUpdateManager(mTopicsDao, mockRandom, mMockFlags);
+ new AppUpdateManager(mDbHelper, mTopicsDao, mockRandom, mMockFlags);
// Mock Flags to get an independent result
when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numOfLookBackEpochs);
when(mMockFlags.getTopicsNumberOfTopTopics()).thenReturn(topicsNumberOfTopTopics);
- when(mMockFlags.getTopicsNumberOfRandomTopics()).thenReturn(topicsNumberOfRandomTopics);
when(mMockFlags.getTopicsPercentageForRandomTopic())
.thenReturn(topicsPercentageForRandomTopic);
-
- Topic topic1 = Topic.create(/* topic */ 1, taxonomyVersion, modelVersion);
- Topic topic2 = Topic.create(/* topic */ 2, taxonomyVersion, modelVersion);
- Topic topic3 = Topic.create(/* topic */ 3, taxonomyVersion, modelVersion);
- Topic topic4 = Topic.create(/* topic */ 4, taxonomyVersion, modelVersion);
- Topic topic5 = Topic.create(/* topic */ 5, taxonomyVersion, modelVersion);
- Topic topic6 = Topic.create(/* topic */ 6, taxonomyVersion, modelVersion);
+ // Enable TopContributors check
+ when(mDbHelper.supportsTopicContributorsTable()).thenReturn(true);
+ when(mMockFlags.getEnableTopicContributorsCheck()).thenReturn(true);
+
+ Topic topic1 = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic2 = Topic.create(/* topic */ 2, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic3 = Topic.create(/* topic */ 3, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic4 = Topic.create(/* topic */ 4, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic5 = Topic.create(/* topic */ 5, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic6 = Topic.create(/* topic */ 6, TAXONOMY_VERSION, MODEL_VERSION);
List<Topic> topTopics = List.of(topic1, topic2, topic3, topic4, topic5, topic6);
// Persist top topics into database for last 3 epochs
@@ -633,29 +779,79 @@ public class AppUpdateManagerTest {
epochId >= currentEpochId - numOfLookBackEpochs;
epochId--) {
mTopicsDao.persistTopTopics(epochId, topTopics);
+
+ // Persist topics to TopicContributors Table avoid being filtered out
+ for (Topic topic : topTopics) {
+ mTopicsDao.persistTopicContributors(
+ epochId, Map.of(topic.getTopic(), Set.of(appName)));
+ }
}
// Assign topics to past epochs
- appUpdateManager.assignTopicsToNewlyInstalledApps(packageUri, currentEpochId);
+ appUpdateManager.assignTopicsToNewlyInstalledApps(appName, currentEpochId);
Map<Long, Map<Pair<String, String>, Topic>> expectedReturnedTopics = new HashMap<>();
expectedReturnedTopics.put(
- currentEpochId - 1, Map.of(Pair.create(appName, /* sdk */ ""), topic1));
+ currentEpochId - 1, Map.of(Pair.create(appName, EMPTY_SDK), topic1));
expectedReturnedTopics.put(
- currentEpochId - 2, Map.of(Pair.create(appName, /* sdk */ ""), topic2));
+ currentEpochId - 2, Map.of(Pair.create(appName, EMPTY_SDK), topic2));
expectedReturnedTopics.put(
- currentEpochId - 3, Map.of(Pair.create(appName, /* sdk */ ""), topic6));
+ currentEpochId - 3, Map.of(Pair.create(appName, EMPTY_SDK), topic6));
assertThat(mTopicsDao.retrieveReturnedTopics(currentEpochId - 1, numOfLookBackEpochs))
.isEqualTo(expectedReturnedTopics);
verify(mMockFlags).getTopicsNumberOfLookBackEpochs();
verify(mMockFlags).getTopicsNumberOfTopTopics();
- verify(mMockFlags).getTopicsNumberOfRandomTopics();
verify(mMockFlags).getTopicsPercentageForRandomTopic();
}
@Test
+ public void testAssignTopicsToNewlyInstalledApps_disableTopicContributorsCheck() {
+ AppUpdateManager appUpdateManager =
+ spy(new AppUpdateManager(mDbHelper, mTopicsDao, new Random(), mMockFlags));
+
+ Topic topic1 = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic2 = Topic.create(/* topic */ 2, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic3 = Topic.create(/* topic */ 3, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic4 = Topic.create(/* topic */ 4, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic5 = Topic.create(/* topic */ 5, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic6 = Topic.create(/* topic */ 6, TAXONOMY_VERSION, MODEL_VERSION);
+ List<Topic> topTopics = List.of(topic1, topic2, topic3, topic4, topic5, topic6);
+ final long epochId = 1L;
+ final int numberOfLookBackEpochs = 3;
+ final int numberOfTopTopics = 5;
+ final int percentageForRandomTopic = 5;
+
+ // Do not check actual usage of related methods.
+ doReturn(List.of())
+ .when(appUpdateManager)
+ .filterRegularTopicsWithoutContributors(any(), anyLong());
+ doReturn(topic1)
+ .when(appUpdateManager)
+ .selectAssignedTopicFromTopTopics(any(), any(), eq(percentageForRandomTopic));
+ mTopicsDao.persistTopTopics(epochId, topTopics);
+ doReturn(numberOfLookBackEpochs).when(mMockFlags).getTopicsNumberOfLookBackEpochs();
+ doReturn(numberOfTopTopics).when(mMockFlags).getTopicsNumberOfTopTopics();
+ doReturn(percentageForRandomTopic).when(mMockFlags).getTopicsPercentageForRandomTopic();
+
+ // verify feature flag is off
+ doReturn(false).when(appUpdateManager).supportsTopicContributorFeature();
+ appUpdateManager.assignTopicsToNewlyInstalledApps("anyApp", epochId + 1);
+ // The filter method is not invoked.
+ verify(appUpdateManager, never()).filterRegularTopicsWithoutContributors(any(), anyLong());
+ verify(appUpdateManager, atLeastOnce())
+ .selectAssignedTopicFromTopTopics(any(), any(), eq(percentageForRandomTopic));
+
+ // verify feature flag is on
+ doReturn(true).when(appUpdateManager).supportsTopicContributorFeature();
+ appUpdateManager.assignTopicsToNewlyInstalledApps("anyApp", epochId + 1);
+ // The filter method is invoked.
+ verify(appUpdateManager, atLeastOnce())
+ .filterRegularTopicsWithoutContributors(any(), anyLong());
+ }
+
+ @Test
public void testAssignTopicsToSdkForAppInstallation() {
final String app = "app";
final String sdk = "sdk";
@@ -664,7 +860,7 @@ public class AppUpdateManagerTest {
final long taxonomyVersion = 1L;
final long modelVersion = 1L;
- Pair<String, String> appOnlyCaller = Pair.create(app, /* sdk */ "");
+ Pair<String, String> appOnlyCaller = Pair.create(app, EMPTY_SDK);
Pair<String, String> appSdkCaller = Pair.create(app, sdk);
Topic topic1 = Topic.create(/* topic */ 1, taxonomyVersion, modelVersion);
@@ -713,14 +909,12 @@ public class AppUpdateManagerTest {
final String sdk = ""; // App calls Topics API directly
final int numberOfLookBackEpochs = 3;
final long currentEpochId = 5L;
- final long taxonomyVersion = 1L;
- final long modelVersion = 1L;
- Pair<String, String> appOnlyCaller = Pair.create(app, /* sdk */ "");
+ Pair<String, String> appOnlyCaller = Pair.create(app, EMPTY_SDK);
- Topic topic1 = Topic.create(/* topic */ 1, taxonomyVersion, modelVersion);
- Topic topic2 = Topic.create(/* topic */ 2, taxonomyVersion, modelVersion);
- Topic topic3 = Topic.create(/* topic */ 3, taxonomyVersion, modelVersion);
+ Topic topic1 = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic2 = Topic.create(/* topic */ 2, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic3 = Topic.create(/* topic */ 3, TAXONOMY_VERSION, MODEL_VERSION);
Topic[] topics = {topic1, topic2, topic3};
when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
@@ -742,15 +936,13 @@ public class AppUpdateManagerTest {
public void testAssignTopicsToSdkForAppInstallation_unsatisfiedApp() {
final String app = "app";
final String sdk = "sdk";
- final long taxonomyVersion = 1L;
- final long modelVersion = 1L;
final int numberOfLookBackEpochs = 1;
Pair<String, String> appOnlyCaller = Pair.create(app, /* sdk */ "");
Pair<String, String> otherAppOnlyCaller = Pair.create("otherApp", /* sdk */ "");
Pair<String, String> appSdkCaller = Pair.create(app, sdk);
- Topic topic = Topic.create(/* topic */ 1, taxonomyVersion, modelVersion);
+ Topic topic = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
@@ -790,14 +982,12 @@ public class AppUpdateManagerTest {
final String app = "app";
final String sdk = "sdk";
final String otherSDK = "otherSdk";
- final long taxonomyVersion = 1L;
- final long modelVersion = 1L;
final int numberOfLookBackEpochs = 1;
- Pair<String, String> appOnlyCaller = Pair.create(app, /* sdk */ "");
+ Pair<String, String> appOnlyCaller = Pair.create(app, EMPTY_SDK);
Pair<String, String> appSdkCaller = Pair.create(app, sdk);
- Topic topic = Topic.create(/* topic */ 1, taxonomyVersion, modelVersion);
+ Topic topic = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
@@ -834,4 +1024,199 @@ public class AppUpdateManagerTest {
/* epochId */ 2L,
Map.of(appOnlyCaller, topic, appSdkCaller, topic)));
}
+
+ @Test
+ public void testSupportsTopicContributorFeature() {
+ // Both on
+ when(mDbHelper.supportsTopicContributorsTable()).thenReturn(true);
+ when(mMockFlags.getEnableTopicContributorsCheck()).thenReturn(true);
+ assertThat(mAppUpdateManager.supportsTopicContributorFeature()).isTrue();
+
+ // On and Off
+ when(mDbHelper.supportsTopicContributorsTable()).thenReturn(true);
+ when(mMockFlags.getEnableTopicContributorsCheck()).thenReturn(false);
+ assertThat(mAppUpdateManager.supportsTopicContributorFeature()).isFalse();
+
+ when(mDbHelper.supportsTopicContributorsTable()).thenReturn(false);
+ when(mMockFlags.getEnableTopicContributorsCheck()).thenReturn(true);
+ assertThat(mAppUpdateManager.supportsTopicContributorFeature()).isFalse();
+
+ // Both off
+ when(mDbHelper.supportsTopicContributorsTable()).thenReturn(false);
+ when(mMockFlags.getEnableTopicContributorsCheck()).thenReturn(false);
+ assertThat(mAppUpdateManager.supportsTopicContributorFeature()).isFalse();
+ }
+
+ @Test
+ public void testConvertUriToAppName() {
+ final String samplePackageName = "com.example.measurement.sampleapp";
+ final String packageScheme = "package:";
+
+ Uri uri = Uri.parse(packageScheme + samplePackageName);
+ assertThat(mAppUpdateManager.convertUriToAppName(uri)).isEqualTo(samplePackageName);
+ }
+
+ @Test
+ public void testHandleTopTopicsWithoutContributors() {
+ final long epochId1 = 1;
+ final long epochId2 = 2;
+ final String app1 = "app1";
+ final String app2 = "app2";
+ final String sdk = "sdk";
+ Topic topic1 = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic2 = Topic.create(/* topic */ 2, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic3 = Topic.create(/* topic */ 3, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic4 = Topic.create(/* topic */ 4, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic5 = Topic.create(/* topic */ 5, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic6 = Topic.create(/* topic */ 6, TAXONOMY_VERSION, MODEL_VERSION);
+
+ // Both app1 and app2 have usage in the epoch and all 6 topics are top topics
+ // Both Topic1 and Topic2 have 2 contributors, app1, and app2. Topic3 has the only
+ // contributor app1.
+ // Therefore, Topic3 will be removed from ReturnedTopics if app1 is uninstalled.
+ mTopicsDao.persistTopTopics(
+ epochId1, List.of(topic1, topic2, topic3, topic4, topic5, topic6));
+ mTopicsDao.persistAppClassificationTopics(
+ epochId1,
+ Map.of(
+ app1, List.of(topic1, topic2, topic3),
+ app2, List.of(topic1, topic2)));
+ mTopicsDao.persistTopicContributors(
+ epochId1,
+ Map.of(
+ topic1.getTopic(), Set.of(app1, app2),
+ topic2.getTopic(), Set.of(app1, app2),
+ topic3.getTopic(), Set.of(app1)));
+ mTopicsDao.persistReturnedAppTopicsMap(
+ epochId1,
+ Map.of(
+ Pair.create(app1, EMPTY_SDK), topic3,
+ Pair.create(app1, sdk), topic3,
+ Pair.create(app2, EMPTY_SDK), topic2,
+ Pair.create(app2, sdk), topic1));
+
+ // Copy data of Epoch1 to Epoch2 to verify the removal is on epoch basis
+ mTopicsDao.persistTopTopics(epochId2, mTopicsDao.retrieveTopTopics(epochId1));
+ mTopicsDao.persistAppClassificationTopics(
+ epochId2, mTopicsDao.retrieveAppClassificationTopics(epochId1));
+ mTopicsDao.persistTopicContributors(
+ epochId2, mTopicsDao.retrieveTopicToContributorsMap(epochId1));
+ mTopicsDao.persistTopicContributors(
+ epochId2, mTopicsDao.retrieveTopicToContributorsMap(epochId1));
+ mTopicsDao.persistReturnedAppTopicsMap(
+ epochId2,
+ mTopicsDao
+ .retrieveReturnedTopics(epochId1, /* numberOfLookBackEpochs */ 1)
+ .get(epochId1));
+
+ when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(1);
+ mAppUpdateManager.handleTopTopicsWithoutContributors(
+ /* only handle past epochs */ epochId2, app1);
+
+ // Only observe current epoch per the setup of this test
+ // Topic3 should be removed from returnedTopics
+ assertThat(
+ mTopicsDao
+ .retrieveReturnedTopics(epochId1, /* numberOfLookBackEpochs */ 1)
+ .get(epochId1))
+ .isEqualTo(
+ Map.of(
+ Pair.create(app2, EMPTY_SDK), topic2,
+ Pair.create(app2, sdk), topic1));
+ // Epoch2 has no changes.
+ assertThat(
+ mTopicsDao
+ .retrieveReturnedTopics(epochId2, /* numberOfLookBackEpochs */ 1)
+ .get(epochId2))
+ .isEqualTo(
+ Map.of(
+ Pair.create(app1, EMPTY_SDK), topic3,
+ Pair.create(app1, sdk), topic3,
+ Pair.create(app2, EMPTY_SDK), topic2,
+ Pair.create(app2, sdk), topic1));
+ }
+
+ @Test
+ public void testFilterRegularTopicsWithoutContributors() {
+ final long epochId = 1;
+ final String app = "app";
+
+ Topic topic1 = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic2 = Topic.create(/* topic */ 2, TAXONOMY_VERSION, MODEL_VERSION);
+ Topic topic3 = Topic.create(/* topic */ 3, TAXONOMY_VERSION, MODEL_VERSION);
+
+ List<Topic> regularTopics = List.of(topic1, topic2, topic3);
+ // topic1 has a contributor. topic2 has empty contributor set and topic3 is annotated with
+ // PADDED_TOP_TOPICS_STRING. (See EpochManager#PADDED_TOP_TOPICS_STRING for details)
+ mTopicsDao.persistTopicContributors(
+ epochId,
+ Map.of(
+ topic1.getTopic(),
+ Set.of(app),
+ topic2.getTopic(),
+ Set.of(),
+ topic3.getTopic(),
+ Set.of(PADDED_TOP_TOPICS_STRING)));
+
+ // topic2 is filtered out.
+ assertThat(mAppUpdateManager.filterRegularTopicsWithoutContributors(regularTopics, epochId))
+ .isEqualTo(List.of(topic1, topic3));
+ }
+
+ // The actual e2e logic is tested in TopicsWorkerTest "testHandleAppUninstallation" tests.
+ // Methods invoked are tested respectively.
+ @Test
+ public void testHandleAppUninstallationInRealTime_enableTopicContributors() {
+ final String app = "app";
+ final long epochId = 1L;
+
+ AppUpdateManager appUpdateManager =
+ spy(new AppUpdateManager(mDbHelper, mTopicsDao, new Random(), mMockFlags));
+
+ // Enable Topic Contributors feature
+ doReturn(true).when(mDbHelper).supportsTopicContributorsTable();
+ when(mMockFlags.getEnableTopicContributorsCheck()).thenReturn(true);
+
+ appUpdateManager.handleAppUninstallationInRealTime(Uri.parse(app), epochId);
+
+ verify(appUpdateManager).convertUriToAppName(Uri.parse(app));
+ verify(appUpdateManager).handleTopTopicsWithoutContributors(epochId, app);
+ verify(appUpdateManager).deleteAppDataFromTableByApps(List.of(app));
+ }
+
+ // The actual e2e logic is tested in TopicsWorkerTest "testHandleAppUninstallation" tests.
+ // Methods invoked are tested respectively in this test class.
+ @Test
+ public void testHandleAppUninstallationInRealTime_disableTopicContributors() {
+ final String app = "app";
+ final long epochId = 1L;
+
+ AppUpdateManager appUpdateManager =
+ spy(new AppUpdateManager(mDbHelper, mTopicsDao, new Random(), mMockFlags));
+
+ // Disable Topic Contributors feature
+ doReturn(false).when(mDbHelper).supportsTopicContributorsTable();
+ when(mMockFlags.getEnableTopicContributorsCheck()).thenReturn(true);
+
+ appUpdateManager.handleAppUninstallationInRealTime(Uri.parse(app), epochId);
+
+ verify(appUpdateManager).convertUriToAppName(Uri.parse(app));
+ verify(appUpdateManager, never()).handleTopTopicsWithoutContributors(epochId, app);
+ verify(appUpdateManager).deleteAppDataFromTableByApps(List.of(app));
+ }
+
+ // For test coverage only. The actual e2e logic is tested in TopicsWorkerTest. Methods invoked
+ // are tested respectively in this test class.
+ @Test
+ public void testHandleAppInstallationInRealTime() {
+ final String app = "app";
+ final long epochId = 1L;
+
+ AppUpdateManager appUpdateManager =
+ spy(new AppUpdateManager(mDbHelper, mTopicsDao, new Random(), mMockFlags));
+
+ appUpdateManager.handleAppInstallationInRealTime(Uri.parse(app), epochId);
+
+ verify(appUpdateManager).assignTopicsToNewlyInstalledApps(app, epochId);
+ }
}