1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.adservices.service.topics;
18 
19 import static com.android.adservices.service.topics.EpochManager.PADDED_TOP_TOPICS_STRING;
20 
21 import static com.google.common.truth.Truth.assertThat;
22 
23 import static org.junit.Assert.assertFalse;
24 import static org.junit.Assert.assertThrows;
25 import static org.junit.Assert.assertTrue;
26 import static org.mockito.ArgumentMatchers.any;
27 import static org.mockito.ArgumentMatchers.anyInt;
28 import static org.mockito.Mockito.spy;
29 import static org.mockito.Mockito.verify;
30 import static org.mockito.Mockito.when;
31 
32 import android.content.Context;
33 import android.content.pm.ApplicationInfo;
34 import android.content.pm.PackageManager;
35 import android.net.Uri;
36 import android.util.Pair;
37 
38 import androidx.test.core.app.ApplicationProvider;
39 
40 import com.android.adservices.MockRandom;
41 import com.android.adservices.data.DbHelper;
42 import com.android.adservices.data.DbTestUtil;
43 import com.android.adservices.data.topics.EncryptedTopic;
44 import com.android.adservices.data.topics.Topic;
45 import com.android.adservices.data.topics.TopicsDao;
46 import com.android.adservices.data.topics.TopicsTables;
47 import com.android.adservices.service.Flags;
48 import com.android.adservices.shared.testing.SdkLevelSupportRule;
49 import com.android.modules.utils.build.SdkLevel;
50 
51 import org.junit.Before;
52 import org.junit.Rule;
53 import org.junit.Test;
54 import org.mockito.Mock;
55 import org.mockito.Mockito;
56 import org.mockito.MockitoAnnotations;
57 
58 import java.nio.charset.StandardCharsets;
59 import java.util.Arrays;
60 import java.util.Collections;
61 import java.util.HashMap;
62 import java.util.HashSet;
63 import java.util.List;
64 import java.util.Map;
65 import java.util.Random;
66 import java.util.Set;
67 
68 /** Unit tests for {@link com.android.adservices.service.topics.AppUpdateManager} */
69 public class AppUpdateManagerTest {
70     @SuppressWarnings({"unused"})
71     private static final String TAG = "AppInstallationInfoManagerTest";
72 
73     private static final String EMPTY_SDK = "";
74     private static final long TAXONOMY_VERSION = 1L;
75     private static final long MODEL_VERSION = 1L;
76 
77     private final Context mContext = spy(ApplicationProvider.getApplicationContext());
78     private final DbHelper mDbHelper = spy(DbTestUtil.getDbHelperForTest());
79 
80     private AppUpdateManager mAppUpdateManager;
81     private TopicsDao mTopicsDao;
82 
83     @Mock PackageManager mMockPackageManager;
84     @Mock Flags mMockFlags;
85 
86     @Rule(order = 0)
87     public final SdkLevelSupportRule sdkLevel = SdkLevelSupportRule.forAtLeastS();
88 
89     @Before
setup()90     public void setup() {
91         // In order to mock Package Manager, context also needs to be mocked to return
92         // mocked Package Manager
93         MockitoAnnotations.initMocks(this);
94         when(mContext.getPackageManager()).thenReturn(mMockPackageManager);
95 
96         mTopicsDao = new TopicsDao(mDbHelper);
97         // Erase all existing data.
98         DbTestUtil.deleteTable(TopicsTables.TaxonomyContract.TABLE);
99         DbTestUtil.deleteTable(TopicsTables.AppClassificationTopicsContract.TABLE);
100         DbTestUtil.deleteTable(TopicsTables.CallerCanLearnTopicsContract.TABLE);
101         DbTestUtil.deleteTable(TopicsTables.TopTopicsContract.TABLE);
102         DbTestUtil.deleteTable(TopicsTables.ReturnedTopicContract.TABLE);
103         DbTestUtil.deleteTable(TopicsTables.UsageHistoryContract.TABLE);
104         DbTestUtil.deleteTable(TopicsTables.AppUsageHistoryContract.TABLE);
105         DbTestUtil.deleteTable(TopicsTables.TopicContributorsContract.TABLE);
106 
107         mAppUpdateManager = new AppUpdateManager(mDbHelper, mTopicsDao, new Random(), mMockFlags);
108     }
109 
110     @Test
testReconcileUninstalledApps()111     public void testReconcileUninstalledApps() {
112         // Both app1 and app2 have usages in database. App 2 won't be current installed app list
113         // that is returned by mocked Package Manager, so it'll be regarded as an unhanded installed
114         // app.
115         final String app1 = "app1";
116         final String app2 = "app2";
117 
118         // Mock Package Manager for installed applications
119         ApplicationInfo appInfo1 = new ApplicationInfo();
120         appInfo1.packageName = app1;
121 
122         mockInstalledApplications(Collections.singletonList(appInfo1));
123 
124         // Begin to persist data into database
125         // Handle AppClassificationTopicsContract
126         final long epochId1 = 1L;
127         final int topicId1 = 1;
128         final int numberOfLookBackEpochs = 1;
129 
130         Topic topic1 = Topic.create(topicId1, TAXONOMY_VERSION, MODEL_VERSION);
131 
132         Map<String, List<Topic>> appClassificationTopicsMap1 = new HashMap<>();
133         appClassificationTopicsMap1.put(app1, Collections.singletonList(topic1));
134         appClassificationTopicsMap1.put(app2, Collections.singletonList(topic1));
135 
136         mTopicsDao.persistAppClassificationTopics(epochId1, appClassificationTopicsMap1);
137         // Verify AppClassificationContract has both apps
138         assertThat(mTopicsDao.retrieveAppClassificationTopics(epochId1).keySet())
139                 .containsExactly(app1, app2);
140 
141         // Handle UsageHistoryContract
142         final String sdk1 = "sdk1";
143 
144         mTopicsDao.recordUsageHistory(epochId1, app1, EMPTY_SDK);
145         mTopicsDao.recordUsageHistory(epochId1, app1, sdk1);
146         mTopicsDao.recordUsageHistory(epochId1, app2, EMPTY_SDK);
147         mTopicsDao.recordUsageHistory(epochId1, app2, sdk1);
148 
149         // Verify UsageHistoryContract has both apps
150         assertThat(mTopicsDao.retrieveAppSdksUsageMap(epochId1).keySet())
151                 .containsExactly(app1, app2);
152 
153         // Handle AppUsageHistoryContract
154         mTopicsDao.recordAppUsageHistory(epochId1, app1);
155         mTopicsDao.recordAppUsageHistory(epochId1, app2);
156 
157         // Verify AppUsageHistoryContract has both apps
158         assertThat(mTopicsDao.retrieveAppUsageMap(epochId1).keySet()).containsExactly(app1, app2);
159 
160         // Handle CallerCanLearnTopicsContract
161         Map<Topic, Set<String>> callerCanLearnMap = new HashMap<>();
162         callerCanLearnMap.put(topic1, new HashSet<>(Arrays.asList(app1, app2, sdk1)));
163         mTopicsDao.persistCallerCanLearnTopics(epochId1, callerCanLearnMap);
164 
165         // Verify CallerCanLearnTopicsContract has both apps
166         assertThat(
167                         mTopicsDao
168                                 .retrieveCallerCanLearnTopicsMap(epochId1, numberOfLookBackEpochs)
169                                 .get(topic1))
170                 .containsAtLeast(app1, app2);
171 
172         // Handle ReturnedTopicContract
173         Map<Pair<String, String>, Topic> returnedAppSdkTopics = new HashMap<>();
174         returnedAppSdkTopics.put(Pair.create(app1, EMPTY_SDK), topic1);
175         returnedAppSdkTopics.put(Pair.create(app1, sdk1), topic1);
176         returnedAppSdkTopics.put(Pair.create(app2, EMPTY_SDK), topic1);
177         returnedAppSdkTopics.put(Pair.create(app2, sdk1), topic1);
178 
179         mTopicsDao.persistReturnedAppTopicsMap(epochId1, returnedAppSdkTopics);
180         Map<Pair<String, String>, Topic> expectedReturnedTopics = new HashMap<>();
181         expectedReturnedTopics.put(Pair.create(app1, EMPTY_SDK), topic1);
182         expectedReturnedTopics.put(Pair.create(app1, sdk1), topic1);
183         expectedReturnedTopics.put(Pair.create(app2, EMPTY_SDK), topic1);
184         expectedReturnedTopics.put(Pair.create(app2, sdk1), topic1);
185 
186         // Verify ReturnedTopicContract has both apps
187         assertThat(
188                         mTopicsDao
189                                 .retrieveReturnedTopics(epochId1, numberOfLookBackEpochs)
190                                 .get(epochId1))
191                 .isEqualTo(expectedReturnedTopics);
192 
193         // Reconcile uninstalled applications
194         mAppUpdateManager.reconcileUninstalledApps(mContext, epochId1);
195 
196         verify(mContext).getPackageManager();
197 
198         if (SdkLevel.isAtLeastT()) {
199             verify(mMockPackageManager).getInstalledApplications(Mockito.any());
200         } else {
201             verify(mMockPackageManager).getInstalledApplications(anyInt());
202         }
203 
204         // Each Table should have wiped off all data belonging to app2
205         Set<String> setContainsOnlyApp1 = new HashSet<>(Collections.singletonList(app1));
206         assertThat(mTopicsDao.retrieveAppClassificationTopics(epochId1).keySet())
207                 .isEqualTo(setContainsOnlyApp1);
208         assertThat(mTopicsDao.retrieveAppSdksUsageMap(epochId1).keySet())
209                 .isEqualTo(setContainsOnlyApp1);
210         assertThat(mTopicsDao.retrieveAppUsageMap(epochId1).keySet())
211                 .isEqualTo(setContainsOnlyApp1);
212         assertThat(
213                         mTopicsDao
214                                 .retrieveCallerCanLearnTopicsMap(epochId1, numberOfLookBackEpochs)
215                                 .get(topic1))
216                 .doesNotContain(app2);
217         // Returned Topics Map contains only App1 paris
218         Map<Pair<String, String>, Topic> expectedReturnedTopicsAfterWiping = new HashMap<>();
219         expectedReturnedTopicsAfterWiping.put(Pair.create(app1, EMPTY_SDK), topic1);
220         expectedReturnedTopicsAfterWiping.put(Pair.create(app1, sdk1), topic1);
221         assertThat(
222                         mTopicsDao
223                                 .retrieveReturnedTopics(epochId1, numberOfLookBackEpochs)
224                                 .get(epochId1))
225                 .isEqualTo(expectedReturnedTopicsAfterWiping);
226     }
227 
228     @Test
testReconcileUninstalledApps_handleTopicsWithoutContributor()229     public void testReconcileUninstalledApps_handleTopicsWithoutContributor() {
230         // Test Setup:
231         // * Both app1 and app2 have usages in database. app2 won't be current installed app list
232         //   that is returned by mocked Package Manager, so it'll be regarded as an unhandled
233         //   uninstalled app.
234         // * In Epoch1, app1 is classified to topic1, topic2. app2 is classified to topic1, topic3.
235         //   Both app1 and app2 have topic3 as returned topic as they both call Topics API via sdk.
236         // * In Epoch2, both app1 and app2 are classified to topic1, topic3. (verify epoch basis)
237         // * In Epoch3, both app2 and app3 are classified to topic1. app4 learns topic1 from sdk and
238         //   also returns topic1. After app2 and app4 are uninstalled, topic1 should be removed for
239         //   epoch3 and app3 should have no returned topic. (verify consecutive deletion on a topic)
240         // * In Epoch4, app2 is uninstalled. topic3 will be removed in Epoch1 as it has app2 as the
241         //   only contributor, while topic3 will stay in Epoch2 as app2 contributes to it.
242         final String app1 = "app1";
243         final String app2 = "app2";
244         final String sdk = "sdk";
245         final long epoch1 = 1L;
246         final long epoch2 = 2L;
247         final long epoch4 = 4L;
248         final int numberOfLookBackEpochs = 3;
249 
250         Topic topic1 = Topic.create(1, TAXONOMY_VERSION, MODEL_VERSION);
251         Topic topic2 = Topic.create(2, TAXONOMY_VERSION, MODEL_VERSION);
252         Topic topic3 = Topic.create(3, TAXONOMY_VERSION, MODEL_VERSION);
253         Topic topic4 = Topic.create(4, TAXONOMY_VERSION, MODEL_VERSION);
254         Topic topic5 = Topic.create(5, TAXONOMY_VERSION, MODEL_VERSION);
255         Topic topic6 = Topic.create(6, TAXONOMY_VERSION, MODEL_VERSION);
256 
257         // Mock Package Manager for installed applications
258         ApplicationInfo appInfo1 = new ApplicationInfo();
259         appInfo1.packageName = app1;
260 
261         mockInstalledApplications(List.of(appInfo1));
262 
263         // Persist to AppClassificationTopics table
264         mTopicsDao.persistAppClassificationTopics(
265                 epoch1, Map.of(app1, List.of(topic1, topic2), app2, List.of(topic1, topic3)));
266         mTopicsDao.persistAppClassificationTopics(
267                 epoch2, Map.of(app1, List.of(topic1, topic3), app2, List.of(topic1, topic3)));
268 
269         // Persist to TopTopics table
270         mTopicsDao.persistTopTopics(
271                 epoch1, List.of(topic1, topic2, topic3, topic4, topic5, topic6));
272         mTopicsDao.persistTopTopics(
273                 epoch2, List.of(topic1, topic2, topic3, topic4, topic5, topic6));
274 
275         // Persist to TopicContributors table
276         mTopicsDao.persistTopicContributors(epoch1, Map.of(topic1.getTopic(), Set.of(app1, app2)));
277         mTopicsDao.persistTopicContributors(epoch1, Map.of(topic2.getTopic(), Set.of(app1)));
278         mTopicsDao.persistTopicContributors(epoch1, Map.of(topic3.getTopic(), Set.of(app2)));
279         mTopicsDao.persistTopicContributors(epoch2, Map.of(topic1.getTopic(), Set.of(app1, app2)));
280         mTopicsDao.persistTopicContributors(epoch2, Map.of(topic3.getTopic(), Set.of(app1, app2)));
281 
282         // Persist to ReturnedTopics table
283         mTopicsDao.persistReturnedAppTopicsMap(epoch1, Map.of(Pair.create(app1, sdk), topic3));
284         mTopicsDao.persistReturnedAppTopicsMap(epoch1, Map.of(Pair.create(app2, sdk), topic3));
285         mTopicsDao.persistReturnedAppTopicsMap(epoch2, Map.of(Pair.create(app1, sdk), topic3));
286         mTopicsDao.persistReturnedAppTopicsMap(epoch2, Map.of(Pair.create(app2, sdk), topic3));
287 
288         // Mock flag value to remove dependency of actual flag value
289         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
290 
291         // Execute reconciliation to handle app2
292         mAppUpdateManager.reconcileUninstalledApps(mContext, epoch4);
293 
294         // Verify Returned Topics in [1, 3]. app2 should have no returnedTopics as it's uninstalled.
295         // app1 only has returned topic at Epoch2 as topic3 is removed from Epoch1.
296         Map<Long, Map<Pair<String, String>, Topic>> expectedReturnedTopicsMap =
297                 Map.of(epoch2, Map.of(Pair.create(app1, sdk), topic3));
298         assertThat(mTopicsDao.retrieveReturnedTopics(epoch4 - 1, numberOfLookBackEpochs))
299                 .isEqualTo(expectedReturnedTopicsMap);
300 
301         // Verify TopicContributors Map is updated: app1 should be removed after the uninstallation.
302         assertThat(mTopicsDao.retrieveTopicToContributorsMap(epoch1))
303                 .isEqualTo(
304                         Map.of(topic1.getTopic(), Set.of(app1), topic2.getTopic(), Set.of(app1)));
305         assertThat(mTopicsDao.retrieveTopicToContributorsMap(epoch2))
306                 .isEqualTo(
307                         Map.of(topic1.getTopic(), Set.of(app1), topic3.getTopic(), Set.of(app1)));
308     }
309 
310     @Test
testReconcileUninstalledApps_contributorDeletionsToSameTopic()311     public void testReconcileUninstalledApps_contributorDeletionsToSameTopic() {
312         // Test Setup:
313         // * app1 has usages in database. Both app2 and app3 won't be current installed app list
314         //   that is returned by mocked Package Manager, so they'll be regarded as an unhandled
315         //   uninstalled apps.
316         // * Both app2 and app3 are contributors to topic1 and return topic1. app1 is not the
317         //   contributor but also returns topic1, learnt via same SDK.
318         final String app1 = "app1";
319         final String app2 = "app2";
320         final String app3 = "app3";
321         final String sdk = "sdk";
322         final long epoch1 = 1L;
323         final long epoch2 = 2L;
324         final int numberOfLookBackEpochs = 3;
325 
326         Topic topic1 = Topic.create(1, TAXONOMY_VERSION, MODEL_VERSION);
327         Topic topic2 = Topic.create(2, TAXONOMY_VERSION, MODEL_VERSION);
328         Topic topic3 = Topic.create(3, TAXONOMY_VERSION, MODEL_VERSION);
329         Topic topic4 = Topic.create(4, TAXONOMY_VERSION, MODEL_VERSION);
330         Topic topic5 = Topic.create(5, TAXONOMY_VERSION, MODEL_VERSION);
331         Topic topic6 = Topic.create(6, TAXONOMY_VERSION, MODEL_VERSION);
332 
333         // Mock Package Manager for installed applications
334         ApplicationInfo appInfo1 = new ApplicationInfo();
335         appInfo1.packageName = app1;
336 
337         mockInstalledApplications(List.of(appInfo1));
338 
339         // Persist to AppClassificationTopics table
340         mTopicsDao.persistAppClassificationTopics(
341                 epoch1, Map.of(app2, List.of(topic1), app3, List.of(topic1)));
342 
343         // Persist to TopTopics table
344         mTopicsDao.persistTopTopics(
345                 epoch1, List.of(topic1, topic2, topic3, topic4, topic5, topic6));
346 
347         // Persist to TopicContributors table
348         mTopicsDao.persistTopicContributors(epoch1, Map.of(topic1.getTopic(), Set.of(app2, app3)));
349 
350         // Persist to ReturnedTopics table
351         mTopicsDao.persistReturnedAppTopicsMap(epoch1, Map.of(Pair.create(app1, sdk), topic1));
352         mTopicsDao.persistReturnedAppTopicsMap(epoch1, Map.of(Pair.create(app2, sdk), topic1));
353         mTopicsDao.persistReturnedAppTopicsMap(epoch1, Map.of(Pair.create(app3, sdk), topic1));
354 
355         // Mock flag value to remove dependency of actual flag value
356         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
357 
358         // Execute reconciliation to handle app2 and app3
359         mAppUpdateManager.reconcileUninstalledApps(mContext, epoch2);
360 
361         // Verify Returned Topics in epoch 1. app2 and app3 are uninstalled, so they definitely
362         // don't have a returned topic. As topic1 has no contributors after uninstallations of app2
363         // and app3, it's removed from database. Therefore, app1 should have no returned topics as
364         // well.
365         assertThat(mTopicsDao.retrieveTopicToContributorsMap(epoch1)).isEmpty();
366         assertThat(mTopicsDao.retrieveReturnedTopics(epoch1, numberOfLookBackEpochs)).isEmpty();
367     }
368 
369     @Test
testGetUnhandledUninstalledApps()370     public void testGetUnhandledUninstalledApps() {
371         final long epochId = 1L;
372         Set<String> currentInstalledApps = Set.of("app1", "app2", "app5");
373 
374         // Add app1 and app3 into usage table
375         mTopicsDao.recordAppUsageHistory(epochId, "app1");
376         mTopicsDao.recordAppUsageHistory(epochId, "app3");
377 
378         // Add app2 and app4 into returned topic table
379         mTopicsDao.persistReturnedAppTopicsMap(
380                 epochId,
381                 Map.of(
382                         Pair.create("app2", EMPTY_SDK),
383                         Topic.create(
384                                 /* topic ID */ 1, /* taxonomyVersion */ 1L, /* model version */ 1L),
385                         Pair.create("app4", EMPTY_SDK),
386                         Topic.create(
387                                 /* topic ID */ 1, /* taxonomyVersion */
388                                 1L, /* model version */
389                                 1L)));
390 
391         // Unhandled apps = usageTable U returnedTopicTable - currentInstalled
392         //                = ((app1, app3) U (app2, app4)) - (app1, app2, app5) = (app3, app4)
393         // Note that app5 is installed but doesn't have usage of returned topic, so it won't be
394         // handled.
395         assertThat(mAppUpdateManager.getUnhandledUninstalledApps(currentInstalledApps))
396                 .isEqualTo(Set.of("app3", "app4"));
397     }
398 
399     @Test
testGetUnhandledInstalledApps()400     public void testGetUnhandledInstalledApps() {
401         final long epochId = 10L;
402         Set<String> currentInstalledApps = Set.of("app1", "app2", "app3", "app4");
403 
404         // Add app1 and app5 into usage table
405         mTopicsDao.recordAppUsageHistory(epochId, "app1");
406         mTopicsDao.recordAppUsageHistory(epochId, "app5");
407 
408         // Add app2 and app6 into returned topic table
409         mTopicsDao.persistReturnedAppTopicsMap(
410                 epochId,
411                 Map.of(
412                         Pair.create("app2", EMPTY_SDK),
413                         Topic.create(
414                                 /* topic ID */ 1, /* taxonomyVersion */ 1L, /* model version */ 1L),
415                         Pair.create("app6", EMPTY_SDK),
416                         Topic.create(
417                                 /* topic ID */ 1, /* taxonomyVersion */
418                                 1L, /* model version */
419                                 1L)));
420 
421         // Unhandled apps = currentInstalled - usageTable U returnedTopicTable
422         //          = (app1, app2, app3, app4) - ((app1, app5) U (app2, app6)) -  = (app3, app4)
423         // Note that app5 and app6 have usages or returned topics, but not currently installed, so
424         // they won't be handled.
425         assertThat(mAppUpdateManager.getUnhandledInstalledApps(currentInstalledApps))
426                 .isEqualTo(Set.of("app3", "app4"));
427     }
428 
429     @Test
testDeleteAppDataFromTableByApps()430     public void testDeleteAppDataFromTableByApps() {
431         final String app1 = "app1";
432         final String app2 = "app2";
433         final String app3 = "app3";
434 
435         // Begin to persist data into database.
436         // app1, app2 and app3 have usages in database. Derived data of app2 and app3 will be wiped.
437         // Therefore, database will only contain app1's data.
438 
439         // Handle AppClassificationTopicsContract
440         final long epochId1 = 1L;
441         final int topicId1 = 1;
442         final int numberOfLookBackEpochs = 1;
443 
444         Topic topic1 = Topic.create(topicId1, TAXONOMY_VERSION, MODEL_VERSION);
445 
446         mTopicsDao.persistAppClassificationTopics(epochId1, Map.of(app1, List.of(topic1)));
447         mTopicsDao.persistAppClassificationTopics(epochId1, Map.of(app2, List.of(topic1)));
448         mTopicsDao.persistAppClassificationTopics(epochId1, Map.of(app3, List.of(topic1)));
449         // Verify AppClassificationContract has both apps
450         assertThat(mTopicsDao.retrieveAppClassificationTopics(epochId1).keySet())
451                 .isEqualTo(Set.of(app1, app2, app3));
452 
453         // Handle UsageHistoryContract
454         final String sdk1 = "sdk1";
455 
456         mTopicsDao.recordUsageHistory(epochId1, app1, EMPTY_SDK);
457         mTopicsDao.recordUsageHistory(epochId1, app1, sdk1);
458         mTopicsDao.recordUsageHistory(epochId1, app2, EMPTY_SDK);
459         mTopicsDao.recordUsageHistory(epochId1, app2, sdk1);
460         mTopicsDao.recordUsageHistory(epochId1, app3, EMPTY_SDK);
461         mTopicsDao.recordUsageHistory(epochId1, app3, sdk1);
462 
463         // Verify UsageHistoryContract has both apps
464         assertThat(mTopicsDao.retrieveAppSdksUsageMap(epochId1).keySet())
465                 .isEqualTo(Set.of(app1, app2, app3));
466 
467         // Handle AppUsageHistoryContract
468         mTopicsDao.recordAppUsageHistory(epochId1, app1);
469         mTopicsDao.recordAppUsageHistory(epochId1, app2);
470         mTopicsDao.recordAppUsageHistory(epochId1, app3);
471 
472         // Verify AppUsageHistoryContract has both apps
473         assertThat(mTopicsDao.retrieveAppUsageMap(epochId1).keySet())
474                 .isEqualTo(Set.of(app1, app2, app3));
475 
476         // Handle CallerCanLearnTopicsContract
477         Map<Topic, Set<String>> callerCanLearnMap = new HashMap<>();
478         callerCanLearnMap.put(topic1, new HashSet<>(List.of(app1, app2, app3, sdk1)));
479         mTopicsDao.persistCallerCanLearnTopics(epochId1, callerCanLearnMap);
480 
481         // Verify CallerCanLearnTopicsContract has both apps
482         assertThat(
483                         mTopicsDao
484                                 .retrieveCallerCanLearnTopicsMap(epochId1, numberOfLookBackEpochs)
485                                 .get(topic1))
486                 .isEqualTo(Set.of(app1, app2, app3, sdk1));
487 
488         // Handle ReturnedTopicContract
489         Map<Pair<String, String>, Topic> returnedAppSdkTopics = new HashMap<>();
490         returnedAppSdkTopics.put(Pair.create(app1, EMPTY_SDK), topic1);
491         returnedAppSdkTopics.put(Pair.create(app1, sdk1), topic1);
492         returnedAppSdkTopics.put(Pair.create(app2, EMPTY_SDK), topic1);
493         returnedAppSdkTopics.put(Pair.create(app2, sdk1), topic1);
494         returnedAppSdkTopics.put(Pair.create(app3, EMPTY_SDK), topic1);
495         returnedAppSdkTopics.put(Pair.create(app3, sdk1), topic1);
496 
497         mTopicsDao.persistReturnedAppTopicsMap(epochId1, returnedAppSdkTopics);
498 
499         // Verify ReturnedTopicContract has both apps
500         assertThat(
501                         mTopicsDao
502                                 .retrieveReturnedTopics(epochId1, numberOfLookBackEpochs)
503                                 .get(epochId1))
504                 .isEqualTo(returnedAppSdkTopics);
505 
506         // Handle Topics Contributors Table
507         Map<Integer, Set<String>> topicContributorsMap = Map.of(topicId1, Set.of(app1, app2, app3));
508         mTopicsDao.persistTopicContributors(epochId1, topicContributorsMap);
509 
510         // Verify Topics Contributors Table has all apps
511         assertThat(mTopicsDao.retrieveTopicToContributorsMap(epochId1))
512                 .isEqualTo(topicContributorsMap);
513 
514         // Delete app2's derived data
515         mAppUpdateManager.deleteAppDataFromTableByApps(List.of(app2, app3));
516 
517         // Each Table should have wiped off all data belonging to app2
518         Set<String> setContainsOnlyApp1 = Set.of(app1);
519         assertThat(mTopicsDao.retrieveAppClassificationTopics(epochId1).keySet())
520                 .isEqualTo(setContainsOnlyApp1);
521         assertThat(mTopicsDao.retrieveAppSdksUsageMap(epochId1).keySet())
522                 .isEqualTo(setContainsOnlyApp1);
523         assertThat(mTopicsDao.retrieveAppUsageMap(epochId1).keySet())
524                 .isEqualTo(setContainsOnlyApp1);
525         assertThat(
526                         mTopicsDao
527                                 .retrieveCallerCanLearnTopicsMap(epochId1, numberOfLookBackEpochs)
528                                 .get(topic1))
529                 .isEqualTo(Set.of(app1, sdk1));
530         assertThat(mTopicsDao.retrieveTopicToContributorsMap(epochId1).get(topicId1))
531                 .isEqualTo(setContainsOnlyApp1);
532         // Returned Topics Map contains only App1 paris
533         Map<Pair<String, String>, Topic> expectedReturnedTopicsAfterWiping = new HashMap<>();
534         expectedReturnedTopicsAfterWiping.put(Pair.create(app1, EMPTY_SDK), topic1);
535         expectedReturnedTopicsAfterWiping.put(Pair.create(app1, sdk1), topic1);
536         assertThat(
537                         mTopicsDao
538                                 .retrieveReturnedTopics(epochId1, numberOfLookBackEpochs)
539                                 .get(epochId1))
540                 .isEqualTo(expectedReturnedTopicsAfterWiping);
541     }
542 
543     @Test
testDeleteAppDataFromTableByApps_encryptedTopicsTable()544     public void testDeleteAppDataFromTableByApps_encryptedTopicsTable() {
545         final String app = "app";
546         final String sdk = "sdk";
547         final long epochId = 1L;
548         final int topicId = 1;
549         final int numberOfLookBackEpochs = 1;
550 
551         Topic topic1 = Topic.create(topicId, TAXONOMY_VERSION, MODEL_VERSION);
552         EncryptedTopic encryptedTopic1 =
553                 EncryptedTopic.create(
554                         topic1.toString().getBytes(StandardCharsets.UTF_8),
555                         "publicKey",
556                         "encapsulatedKey".getBytes(StandardCharsets.UTF_8));
557 
558         // Handle ReturnedTopicContract
559         Map<Pair<String, String>, Topic> returnedAppSdkTopics = new HashMap<>();
560         returnedAppSdkTopics.put(Pair.create(app, EMPTY_SDK), topic1);
561         returnedAppSdkTopics.put(Pair.create(app, sdk), topic1);
562 
563         mTopicsDao.persistReturnedAppTopicsMap(epochId, returnedAppSdkTopics);
564         // Handle ReturnedEncryptedTopicContract
565         Map<Pair<String, String>, EncryptedTopic> encryptedTopics =
566                 Map.of(Pair.create(app, sdk), encryptedTopic1);
567 
568         mTopicsDao.persistReturnedAppEncryptedTopicsMap(epochId, encryptedTopics);
569 
570         // Verify ReturnedTopicContract has expected apps
571         assertThat(mTopicsDao.retrieveReturnedTopics(epochId, numberOfLookBackEpochs).get(epochId))
572                 .isEqualTo(returnedAppSdkTopics);
573         // Verify ReturnedEncryptedTopicContract has expected apps
574         assertThat(
575                         mTopicsDao
576                                 .retrieveReturnedEncryptedTopics(epochId, numberOfLookBackEpochs)
577                                 .get(epochId))
578                 .isEqualTo(encryptedTopics);
579 
580         // When db flag is off for version 9.
581         when(mMockFlags.getEnableDatabaseSchemaVersion9()).thenReturn(false);
582         mAppUpdateManager.deleteAppDataFromTableByApps(List.of(app));
583         // Unencrypted table is cleared.
584         assertThat(mTopicsDao.retrieveReturnedTopics(epochId, numberOfLookBackEpochs)).isEmpty();
585         // Encrypted table is not cleared.
586         assertThat(
587                         mTopicsDao
588                                 .retrieveReturnedEncryptedTopics(epochId, numberOfLookBackEpochs)
589                                 .get(epochId))
590                 .isEqualTo(encryptedTopics);
591 
592         // When db flag is on for version 9.
593         when(mMockFlags.getEnableDatabaseSchemaVersion9()).thenReturn(true);
594         mAppUpdateManager.deleteAppDataFromTableByApps(List.of(app));
595         // Unencrypted table is cleared.
596         assertThat(mTopicsDao.retrieveReturnedTopics(epochId, numberOfLookBackEpochs)).isEmpty();
597         // Encrypted table is cleared.
598         assertThat(mTopicsDao.retrieveReturnedEncryptedTopics(epochId, numberOfLookBackEpochs))
599                 .isEmpty();
600     }
601 
602     @Test
testDeleteAppDataFromTableByApps_nullUninstalledAppName()603     public void testDeleteAppDataFromTableByApps_nullUninstalledAppName() {
604         assertThrows(
605                 NullPointerException.class,
606                 () -> mAppUpdateManager.deleteAppDataFromTableByApps(null));
607     }
608 
609     @Test
testDeleteAppDataFromTableByApps_nonExistingUninstalledAppName()610     public void testDeleteAppDataFromTableByApps_nonExistingUninstalledAppName() {
611         // To test it won't throw by calling the method with non-existing application name
612         mAppUpdateManager.deleteAppDataFromTableByApps(List.of("app"));
613     }
614 
615     @Test
testReconcileInstalledApps()616     public void testReconcileInstalledApps() {
617         final String app1 = "app1";
618         final String app2 = "app2";
619         final long currentEpochId = 4L;
620         final int numOfLookBackEpochs = 3;
621         final int topicsNumberOfTopTopics = 5;
622         final int topicsPercentageForRandomTopic = 5;
623 
624         // As selectAssignedTopicFromTopTopics() randomly assigns a top topic, pass in a Mocked
625         // Random object to make the result deterministic.
626         //
627         // In this test, topic 1, 2, and 6 are supposed to be returned. For each topic, it needs 2
628         // random draws: the first is to determine whether to select a random topic, the second is
629         // draw the actual topic index.
630         MockRandom mockRandom =
631                 new MockRandom(
632                         new long[] {
633                             topicsPercentageForRandomTopic, // Will select a regular topic
634                             0, // Index of first topic
635                             topicsPercentageForRandomTopic, // Will select a regular topic
636                             1, // Index of second topic
637                             0, // Will select a random topic
638                             0, // Select the first random topic
639                         });
640         AppUpdateManager appUpdateManager =
641                 new AppUpdateManager(mDbHelper, mTopicsDao, mockRandom, mMockFlags);
642         // Mock Flags to get an independent result
643         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numOfLookBackEpochs);
644         when(mMockFlags.getTopicsNumberOfTopTopics()).thenReturn(topicsNumberOfTopTopics);
645         when(mMockFlags.getTopicsPercentageForRandomTopic())
646                 .thenReturn(topicsPercentageForRandomTopic);
647 
648         // Mock Package Manager for installed applications
649         ApplicationInfo appInfo1 = new ApplicationInfo();
650         appInfo1.packageName = app1;
651         ApplicationInfo appInfo2 = new ApplicationInfo();
652         appInfo2.packageName = app2;
653 
654         mockInstalledApplications(List.of(appInfo1, appInfo2));
655 
656         Topic topic1 = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
657         Topic topic2 = Topic.create(/* topic */ 2, TAXONOMY_VERSION, MODEL_VERSION);
658         Topic topic3 = Topic.create(/* topic */ 3, TAXONOMY_VERSION, MODEL_VERSION);
659         Topic topic4 = Topic.create(/* topic */ 4, TAXONOMY_VERSION, MODEL_VERSION);
660         Topic topic5 = Topic.create(/* topic */ 5, TAXONOMY_VERSION, MODEL_VERSION);
661         Topic topic6 = Topic.create(/* topic */ 6, TAXONOMY_VERSION, MODEL_VERSION);
662         List<Topic> topTopics = List.of(topic1, topic2, topic3, topic4, topic5, topic6);
663 
664         // Begin to persist data into database
665         // Both app1 and app2 are currently installed apps according to Package Manager, but
666         // Only app1 will have usage in database. Therefore, app2 will be regarded as newly
667         // installed app.
668         mTopicsDao.recordAppUsageHistory(currentEpochId - 1, app1);
669         // Unused but to mimic what happens in reality
670         mTopicsDao.recordUsageHistory(currentEpochId - 1, app1, "sdk");
671 
672         // Persist top topics into database for last 3 epochs
673         for (long epochId = currentEpochId - 1;
674                 epochId >= currentEpochId - numOfLookBackEpochs;
675                 epochId--) {
676             mTopicsDao.persistTopTopics(epochId, topTopics);
677             // Persist topics to TopicContributors Table avoid being filtered out
678             for (Topic topic : topTopics) {
679                 mTopicsDao.persistTopicContributors(
680                         epochId, Map.of(topic.getTopic(), Set.of(app1, app2)));
681             }
682         }
683 
684         // Assign topics to past epochs
685         appUpdateManager.reconcileInstalledApps(mContext, currentEpochId);
686 
687         Map<Long, Map<Pair<String, String>, Topic>> expectedReturnedTopics = new HashMap<>();
688         expectedReturnedTopics.put(
689                 currentEpochId - 1, Map.of(Pair.create(app2, EMPTY_SDK), topic1));
690         expectedReturnedTopics.put(
691                 currentEpochId - 2, Map.of(Pair.create(app2, EMPTY_SDK), topic2));
692         expectedReturnedTopics.put(
693                 currentEpochId - 3, Map.of(Pair.create(app2, EMPTY_SDK), topic6));
694 
695         assertThat(mTopicsDao.retrieveReturnedTopics(currentEpochId - 1, numOfLookBackEpochs))
696                 .isEqualTo(expectedReturnedTopics);
697 
698         verify(mMockFlags).getTopicsNumberOfLookBackEpochs();
699         verify(mMockFlags).getTopicsNumberOfTopTopics();
700         verify(mMockFlags).getTopicsPercentageForRandomTopic();
701     }
702 
703     @Test
testSelectAssignedTopicFromTopTopics()704     public void testSelectAssignedTopicFromTopTopics() {
705         final int topicsPercentageForRandomTopic = 5;
706 
707         // Test the randomness with pre-defined values
708         MockRandom mockRandom =
709                 new MockRandom(
710                         new long[] {
711                             0, // Will select a random topic
712                             0, // Select the first random topic
713                             topicsPercentageForRandomTopic, // Will select a regular topic
714                             0 // Select the first regular topic
715                         });
716         AppUpdateManager appUpdateManager =
717                 new AppUpdateManager(mDbHelper, mTopicsDao, mockRandom, mMockFlags);
718 
719         Topic topic1 = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
720         Topic topic2 = Topic.create(/* topic */ 2, TAXONOMY_VERSION, MODEL_VERSION);
721         Topic topic3 = Topic.create(/* topic */ 3, TAXONOMY_VERSION, MODEL_VERSION);
722         Topic topic6 = Topic.create(/* topic */ 6, TAXONOMY_VERSION, MODEL_VERSION);
723 
724         List<Topic> regularTopics = List.of(topic1, topic2, topic3);
725         List<Topic> randomTopics = List.of(topic6);
726 
727         // In the first invocation, mockRandom returns a 0 that indicates a random top topic will
728         // be returned, and followed by another 0 to select the first(only) random top topic.
729         Topic randomTopTopic =
730                 appUpdateManager.selectAssignedTopicFromTopTopics(
731                         regularTopics, randomTopics, topicsPercentageForRandomTopic);
732         assertThat(randomTopTopic).isEqualTo(topic6);
733 
734         // In the second invocation, mockRandom returns a 5 that indicates a regular top topic will
735         // be returned, and following by a 0 to select the first regular top topic.
736         Topic regularTopTopic =
737                 appUpdateManager.selectAssignedTopicFromTopTopics(
738                         regularTopics, randomTopics, topicsPercentageForRandomTopic);
739         assertThat(regularTopTopic).isEqualTo(topic1);
740     }
741 
742     @Test
testSelectAssignedTopicFromTopTopics_bothListsAreEmpty()743     public void testSelectAssignedTopicFromTopTopics_bothListsAreEmpty() {
744         final int topicsPercentageForRandomTopic = 5;
745 
746         AppUpdateManager appUpdateManager =
747                 new AppUpdateManager(mDbHelper, mTopicsDao, new Random(), mMockFlags);
748 
749         List<Topic> regularTopics = List.of();
750         List<Topic> randomTopics = List.of();
751 
752         Topic selectedTopic =
753                 appUpdateManager.selectAssignedTopicFromTopTopics(
754                         regularTopics, randomTopics, topicsPercentageForRandomTopic);
755         assertThat(selectedTopic).isNull();
756     }
757 
758     @Test
testSelectAssignedTopicFromTopTopics_oneListIsEmpty()759     public void testSelectAssignedTopicFromTopTopics_oneListIsEmpty() {
760         final int topicsPercentageForRandomTopic = 5;
761 
762         // Test the randomness with pre-defined values. Ask it to select the second element for next
763         // two random draws.
764         MockRandom mockRandom = new MockRandom(new long[] {1, 1});
765         AppUpdateManager appUpdateManager =
766                 new AppUpdateManager(mDbHelper, mTopicsDao, mockRandom, mMockFlags);
767 
768         Topic topic1 = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
769         Topic topic2 = Topic.create(/* topic */ 2, TAXONOMY_VERSION, MODEL_VERSION);
770         Topic topic3 = Topic.create(/* topic */ 3, TAXONOMY_VERSION, MODEL_VERSION);
771 
772         // Return a regular topic if the list of random topics is empty.
773         List<Topic> regularTopics = List.of(topic1, topic2, topic3);
774         List<Topic> randomTopics = List.of();
775 
776         Topic regularTopTopic =
777                 appUpdateManager.selectAssignedTopicFromTopTopics(
778                         regularTopics, randomTopics, topicsPercentageForRandomTopic);
779         assertThat(regularTopTopic).isEqualTo(topic2);
780 
781         // Return a random topic if the list of regular topics is empty.
782         regularTopics = List.of();
783         randomTopics = List.of(topic1, topic2, topic3);
784 
785         Topic randomTopTopic =
786                 appUpdateManager.selectAssignedTopicFromTopTopics(
787                         regularTopics, randomTopics, topicsPercentageForRandomTopic);
788         assertThat(randomTopTopic).isEqualTo(topic2);
789     }
790 
791     @Test
testAssignTopicsToNewlyInstalledApps()792     public void testAssignTopicsToNewlyInstalledApps() {
793         final String appName = "app";
794         final long currentEpochId = 4L;
795         final int numOfLookBackEpochs = 3;
796         final int topicsNumberOfTopTopics = 5;
797         final int topicsPercentageForRandomTopic = 5;
798 
799         // As selectAssignedTopicFromTopTopics() randomly assigns a top topic, pass in a Mocked
800         // Random object to make the result deterministic.
801         //
802         // In this test, topic 1, 2, and 6 are supposed to be returned. For each topic, it needs 2
803         // random draws: the first is to determine whether to select a random topic, the second is
804         // draw the actual topic index.
805         MockRandom mockRandom =
806                 new MockRandom(
807                         new long[] {
808                             topicsPercentageForRandomTopic, // Will select a regular topic
809                             0, // Index of first topic
810                             topicsPercentageForRandomTopic, // Will select a regular topic
811                             1, // Index of second topic
812                             0, // Will select a random topic
813                             0, // Select the first random topic
814                         });
815 
816         // Spy an instance of AppUpdateManager in order to mock selectAssignedTopicFromTopTopics()
817         // to avoid randomness.
818         AppUpdateManager appUpdateManager =
819                 new AppUpdateManager(mDbHelper, mTopicsDao, mockRandom, mMockFlags);
820         // Mock Flags to get an independent result
821         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numOfLookBackEpochs);
822         when(mMockFlags.getTopicsNumberOfTopTopics()).thenReturn(topicsNumberOfTopTopics);
823         when(mMockFlags.getTopicsPercentageForRandomTopic())
824                 .thenReturn(topicsPercentageForRandomTopic);
825 
826         Topic topic1 = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
827         Topic topic2 = Topic.create(/* topic */ 2, TAXONOMY_VERSION, MODEL_VERSION);
828         Topic topic3 = Topic.create(/* topic */ 3, TAXONOMY_VERSION, MODEL_VERSION);
829         Topic topic4 = Topic.create(/* topic */ 4, TAXONOMY_VERSION, MODEL_VERSION);
830         Topic topic5 = Topic.create(/* topic */ 5, TAXONOMY_VERSION, MODEL_VERSION);
831         Topic topic6 = Topic.create(/* topic */ 6, TAXONOMY_VERSION, MODEL_VERSION);
832         List<Topic> topTopics = List.of(topic1, topic2, topic3, topic4, topic5, topic6);
833 
834         // Persist top topics into database for last 3 epochs
835         for (long epochId = currentEpochId - 1;
836                 epochId >= currentEpochId - numOfLookBackEpochs;
837                 epochId--) {
838             mTopicsDao.persistTopTopics(epochId, topTopics);
839 
840             // Persist topics to TopicContributors Table avoid being filtered out
841             for (Topic topic : topTopics) {
842                 mTopicsDao.persistTopicContributors(
843                         epochId, Map.of(topic.getTopic(), Set.of(appName)));
844             }
845         }
846 
847         // Assign topics to past epochs
848         appUpdateManager.assignTopicsToNewlyInstalledApps(appName, currentEpochId);
849 
850         Map<Long, Map<Pair<String, String>, Topic>> expectedReturnedTopics = new HashMap<>();
851         expectedReturnedTopics.put(
852                 currentEpochId - 1, Map.of(Pair.create(appName, EMPTY_SDK), topic1));
853         expectedReturnedTopics.put(
854                 currentEpochId - 2, Map.of(Pair.create(appName, EMPTY_SDK), topic2));
855         expectedReturnedTopics.put(
856                 currentEpochId - 3, Map.of(Pair.create(appName, EMPTY_SDK), topic6));
857 
858         assertThat(mTopicsDao.retrieveReturnedTopics(currentEpochId - 1, numOfLookBackEpochs))
859                 .isEqualTo(expectedReturnedTopics);
860 
861         verify(mMockFlags).getTopicsNumberOfLookBackEpochs();
862         verify(mMockFlags).getTopicsNumberOfTopTopics();
863         verify(mMockFlags).getTopicsPercentageForRandomTopic();
864     }
865 
866     @Test
testAssignTopicsToSdkForAppInstallation()867     public void testAssignTopicsToSdkForAppInstallation() {
868         final String app = "app";
869         final String sdk = "sdk";
870         final int numberOfLookBackEpochs = 3;
871         final long currentEpochId = 5L;
872         final long taxonomyVersion = 1L;
873         final long modelVersion = 1L;
874 
875         Pair<String, String> appOnlyCaller = Pair.create(app, EMPTY_SDK);
876         Pair<String, String> appSdkCaller = Pair.create(app, sdk);
877 
878         Topic topic1 = Topic.create(/* topic */ 1, taxonomyVersion, modelVersion);
879         Topic topic2 = Topic.create(/* topic */ 2, taxonomyVersion, modelVersion);
880         Topic topic3 = Topic.create(/* topic */ 3, taxonomyVersion, modelVersion);
881         Topic[] topics = {topic1, topic2, topic3};
882 
883         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
884 
885         // Assign returned topics to app-only caller for epochs in [current - 3, current - 1]
886         for (long epochId = currentEpochId - 1;
887                 epochId >= currentEpochId - numberOfLookBackEpochs;
888                 epochId--) {
889             Topic topic = topics[(int) (currentEpochId - 1 - epochId)];
890 
891             // Assign the returned topic for the app in this epoch.
892             mTopicsDao.persistReturnedAppTopicsMap(epochId, Map.of(appOnlyCaller, topic));
893 
894             // Make the topic learnable to app-sdk caller for epochs in [current - 3, current - 1].
895             // In order to achieve this, persist learnability in [current - 5, current - 3]. This
896             // ensures to test the earliest epoch to be learnt from.
897             long earliestEpochIdToLearnFrom = epochId - numberOfLookBackEpochs + 1;
898             mTopicsDao.persistCallerCanLearnTopics(
899                     earliestEpochIdToLearnFrom, Map.of(topic, Set.of(sdk)));
900         }
901 
902         // Check app-sdk doesn't have returned topic before calling the method
903         Map<Long, Map<Pair<String, String>, Topic>> returnedTopicsWithoutAssignment =
904                 mTopicsDao.retrieveReturnedTopics(currentEpochId - 1, numberOfLookBackEpochs);
905         for (Map.Entry<Long, Map<Pair<String, String>, Topic>> entry :
906                 returnedTopicsWithoutAssignment.entrySet()) {
907             assertThat(entry.getValue()).doesNotContainKey(appSdkCaller);
908         }
909 
910         assertTrue(mAppUpdateManager.assignTopicsToSdkForAppInstallation(app, sdk, currentEpochId));
911 
912         // Check app-sdk has been assigned with topic after calling the method
913         Map<Long, Map<Pair<String, String>, Topic>> expectedReturnedTopics = new HashMap<>();
914         for (long epochId = currentEpochId - 1;
915                 epochId >= currentEpochId - numberOfLookBackEpochs;
916                 epochId--) {
917             Topic topic = topics[(int) (currentEpochId - 1 - epochId)];
918 
919             expectedReturnedTopics.put(epochId, Map.of(appSdkCaller, topic, appOnlyCaller, topic));
920         }
921         assertThat(mTopicsDao.retrieveReturnedTopics(currentEpochId - 1, numberOfLookBackEpochs))
922                 .isEqualTo(expectedReturnedTopics);
923 
924         verify(mMockFlags).getTopicsNumberOfLookBackEpochs();
925     }
926 
927     @Test
testAssignTopicsToSdkForAppInstallation_NonSdk()928     public void testAssignTopicsToSdkForAppInstallation_NonSdk() {
929         final String app = "app";
930         final String sdk = EMPTY_SDK; // App calls Topics API directly
931         final int numberOfLookBackEpochs = 3;
932         final long currentEpochId = 5L;
933 
934         Pair<String, String> appOnlyCaller = Pair.create(app, sdk);
935 
936         Topic topic1 = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
937         Topic topic2 = Topic.create(/* topic */ 2, TAXONOMY_VERSION, MODEL_VERSION);
938         Topic topic3 = Topic.create(/* topic */ 3, TAXONOMY_VERSION, MODEL_VERSION);
939         Topic[] topics = {topic1, topic2, topic3};
940 
941         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
942 
943         // Assign returned topics to app-only caller for epochs in [current - 3, current - 1]
944         for (long epochId = currentEpochId - 1;
945                 epochId >= currentEpochId - numberOfLookBackEpochs;
946                 epochId--) {
947             Topic topic = topics[(int) (currentEpochId - 1 - epochId)];
948 
949             mTopicsDao.persistReturnedAppTopicsMap(epochId, Map.of(appOnlyCaller, topic));
950             mTopicsDao.persistCallerCanLearnTopics(epochId - 1, Map.of(topic, Set.of(sdk)));
951         }
952 
953         // No topic will be assigned even though app itself has returned topics
954         assertFalse(
955                 mAppUpdateManager.assignTopicsToSdkForAppInstallation(app, sdk, currentEpochId));
956     }
957 
958     @Test
testAssignTopicsToSdkForAppInstallation_unsatisfiedApp()959     public void testAssignTopicsToSdkForAppInstallation_unsatisfiedApp() {
960         final String app = "app";
961         final String sdk = "sdk";
962         final int numberOfLookBackEpochs = 1;
963 
964         Pair<String, String> appOnlyCaller = Pair.create(app, EMPTY_SDK);
965         Pair<String, String> otherAppOnlyCaller = Pair.create("otherApp", EMPTY_SDK);
966         Pair<String, String> appSdkCaller = Pair.create(app, sdk);
967 
968         Topic topic = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
969 
970         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
971 
972         // For Epoch 3, no topic will be assigned to app because epoch in [0,2] doesn't have any
973         // returned topics.
974         assertFalse(
975                 mAppUpdateManager.assignTopicsToSdkForAppInstallation(
976                         app, sdk, /* currentEpochId */ 3L));
977 
978         // Persist returned topics to otherAppOnlyCaller instead of appOnlyCaller
979         // Also persist sdk to CallerCanLearnTopics Map to allow sdk to learn the topic.
980         mTopicsDao.persistReturnedAppTopicsMap(/* epochId */ 2L, Map.of(otherAppOnlyCaller, topic));
981         mTopicsDao.persistCallerCanLearnTopics(/* epochId */ 2L, Map.of(topic, Set.of(sdk)));
982 
983         // Epoch 3 won't be assigned topics as appOnlyCaller doesn't have a returned Topic for epoch
984         // in [0,2].
985         assertFalse(
986                 mAppUpdateManager.assignTopicsToSdkForAppInstallation(
987                         app, sdk, /* currentEpochId */ 3L));
988 
989         // Persist returned topics to appOnlyCaller
990         mTopicsDao.persistReturnedAppTopicsMap(/* epochId */ 2L, Map.of(appOnlyCaller, topic));
991 
992         assertTrue(
993                 mAppUpdateManager.assignTopicsToSdkForAppInstallation(
994                         app, sdk, /* currentEpochId */ 3L));
995         assertThat(mTopicsDao.retrieveReturnedTopics(/* epochId */ 2L, numberOfLookBackEpochs))
996                 .isEqualTo(
997                         Map.of(
998                                 /* epochId */ 2L,
999                                 Map.of(
1000                                         appOnlyCaller,
1001                                         topic,
1002                                         appSdkCaller,
1003                                         topic,
1004                                         otherAppOnlyCaller,
1005                                         topic)));
1006     }
1007 
1008     @Test
testAssignTopicsToSdkForAppInstallation_unsatisfiedSdk()1009     public void testAssignTopicsToSdkForAppInstallation_unsatisfiedSdk() {
1010         final String app = "app";
1011         final String sdk = "sdk";
1012         final String otherSDK = "otherSdk";
1013         final int numberOfLookBackEpochs = 1;
1014 
1015         Pair<String, String> appOnlyCaller = Pair.create(app, EMPTY_SDK);
1016         Pair<String, String> appSdkCaller = Pair.create(app, sdk);
1017 
1018         Topic topic = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
1019 
1020         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
1021 
1022         mTopicsDao.persistReturnedAppTopicsMap(/* epochId */ 2L, Map.of(appOnlyCaller, topic));
1023 
1024         // No topic will be assigned as topic is not learned in past epochs
1025         assertFalse(
1026                 mAppUpdateManager.assignTopicsToSdkForAppInstallation(
1027                         app, sdk, /* currentEpochId */ 3L));
1028 
1029         // Enable learnability for otherSDK instead of sdk
1030         mTopicsDao.persistCallerCanLearnTopics(/* epochId */ 1L, Map.of(topic, Set.of(otherSDK)));
1031 
1032         // No topic will be assigned as topic is not learned by "sdk" in past epochs
1033         assertFalse(
1034                 mAppUpdateManager.assignTopicsToSdkForAppInstallation(
1035                         app, sdk, /* currentEpochId */ 3L));
1036 
1037         // Enable learnability for sdk
1038         mTopicsDao.persistCallerCanLearnTopics(/* epochId */ 2L, Map.of(topic, Set.of(sdk)));
1039 
1040         // Topic will be assigned as both app and sdk are satisfied
1041         assertTrue(
1042                 mAppUpdateManager.assignTopicsToSdkForAppInstallation(
1043                         app, sdk, /* currentEpochId */ 3L));
1044         assertThat(mTopicsDao.retrieveReturnedTopics(/* epochId */ 2L, numberOfLookBackEpochs))
1045                 .isEqualTo(
1046                         Map.of(
1047                                 /* epochId */ 2L,
1048                                 Map.of(appOnlyCaller, topic, appSdkCaller, topic)));
1049     }
1050 
1051     @Test
testConvertUriToAppName()1052     public void testConvertUriToAppName() {
1053         final String samplePackageName = "com.example.measurement.sampleapp";
1054         final String packageScheme = "package:";
1055 
1056         Uri uri = Uri.parse(packageScheme + samplePackageName);
1057         assertThat(mAppUpdateManager.convertUriToAppName(uri)).isEqualTo(samplePackageName);
1058     }
1059 
1060     @Test
testHandleTopTopicsWithoutContributors()1061     public void testHandleTopTopicsWithoutContributors() {
1062         final long epochId1 = 1;
1063         final long epochId2 = 2;
1064         final String app1 = "app1";
1065         final String app2 = "app2";
1066         final String sdk = "sdk";
1067         Topic topic1 = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
1068         Topic topic2 = Topic.create(/* topic */ 2, TAXONOMY_VERSION, MODEL_VERSION);
1069         Topic topic3 = Topic.create(/* topic */ 3, TAXONOMY_VERSION, MODEL_VERSION);
1070         Topic topic4 = Topic.create(/* topic */ 4, TAXONOMY_VERSION, MODEL_VERSION);
1071         Topic topic5 = Topic.create(/* topic */ 5, TAXONOMY_VERSION, MODEL_VERSION);
1072         Topic topic6 = Topic.create(/* topic */ 6, TAXONOMY_VERSION, MODEL_VERSION);
1073 
1074         // Both app1 and app2 have usage in the epoch and all 6 topics are top topics
1075         // Both Topic1 and Topic2 have 2 contributors, app1, and app2. Topic3 has the only
1076         // contributor app1.
1077         // Therefore, Topic3 will be removed from ReturnedTopics if app1 is uninstalled.
1078         mTopicsDao.persistTopTopics(
1079                 epochId1, List.of(topic1, topic2, topic3, topic4, topic5, topic6));
1080         mTopicsDao.persistAppClassificationTopics(
1081                 epochId1,
1082                 Map.of(
1083                         app1, List.of(topic1, topic2, topic3),
1084                         app2, List.of(topic1, topic2)));
1085         mTopicsDao.persistTopicContributors(
1086                 epochId1,
1087                 Map.of(
1088                         topic1.getTopic(), Set.of(app1, app2),
1089                         topic2.getTopic(), Set.of(app1, app2),
1090                         topic3.getTopic(), Set.of(app1)));
1091         mTopicsDao.persistReturnedAppTopicsMap(
1092                 epochId1,
1093                 Map.of(
1094                         Pair.create(app1, EMPTY_SDK), topic3,
1095                         Pair.create(app1, sdk), topic3,
1096                         Pair.create(app2, EMPTY_SDK), topic2,
1097                         Pair.create(app2, sdk), topic1));
1098 
1099         // Copy data of Epoch1 to Epoch2 to verify the removal is on epoch basis
1100         mTopicsDao.persistTopTopics(epochId2, mTopicsDao.retrieveTopTopics(epochId1));
1101         mTopicsDao.persistAppClassificationTopics(
1102                 epochId2, mTopicsDao.retrieveAppClassificationTopics(epochId1));
1103         mTopicsDao.persistTopicContributors(
1104                 epochId2, mTopicsDao.retrieveTopicToContributorsMap(epochId1));
1105         mTopicsDao.persistTopicContributors(
1106                 epochId2, mTopicsDao.retrieveTopicToContributorsMap(epochId1));
1107         mTopicsDao.persistReturnedAppTopicsMap(
1108                 epochId2,
1109                 mTopicsDao
1110                         .retrieveReturnedTopics(epochId1, /* numberOfLookBackEpochs */ 1)
1111                         .get(epochId1));
1112 
1113         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(1);
1114         mAppUpdateManager.handleTopTopicsWithoutContributors(
1115                 /* only handle past epochs */ epochId2, app1);
1116 
1117         // Only observe current epoch per the setup of this test
1118         // Topic3 should be removed from returnedTopics
1119         assertThat(
1120                         mTopicsDao
1121                                 .retrieveReturnedTopics(epochId1, /* numberOfLookBackEpochs */ 1)
1122                                 .get(epochId1))
1123                 .isEqualTo(
1124                         Map.of(
1125                                 Pair.create(app2, EMPTY_SDK), topic2,
1126                                 Pair.create(app2, sdk), topic1));
1127         // Epoch2 has no changes.
1128         assertThat(
1129                         mTopicsDao
1130                                 .retrieveReturnedTopics(epochId2, /* numberOfLookBackEpochs */ 1)
1131                                 .get(epochId2))
1132                 .isEqualTo(
1133                         Map.of(
1134                                 Pair.create(app1, EMPTY_SDK), topic3,
1135                                 Pair.create(app1, sdk), topic3,
1136                                 Pair.create(app2, EMPTY_SDK), topic2,
1137                                 Pair.create(app2, sdk), topic1));
1138     }
1139 
1140     @Test
testFilterRegularTopicsWithoutContributors()1141     public void testFilterRegularTopicsWithoutContributors() {
1142         final long epochId = 1;
1143         final String app = "app";
1144 
1145         Topic topic1 = Topic.create(/* topic */ 1, TAXONOMY_VERSION, MODEL_VERSION);
1146         Topic topic2 = Topic.create(/* topic */ 2, TAXONOMY_VERSION, MODEL_VERSION);
1147         Topic topic3 = Topic.create(/* topic */ 3, TAXONOMY_VERSION, MODEL_VERSION);
1148 
1149         List<Topic> regularTopics = List.of(topic1, topic2, topic3);
1150         // topic1 has a contributor. topic2 has empty contributor set and topic3 is annotated with
1151         // PADDED_TOP_TOPICS_STRING. (See EpochManager#PADDED_TOP_TOPICS_STRING for details)
1152         mTopicsDao.persistTopicContributors(
1153                 epochId,
1154                 Map.of(
1155                         topic1.getTopic(),
1156                         Set.of(app),
1157                         topic2.getTopic(),
1158                         Set.of(),
1159                         topic3.getTopic(),
1160                         Set.of(PADDED_TOP_TOPICS_STRING)));
1161 
1162         // topic2 is filtered out.
1163         assertThat(mAppUpdateManager.filterRegularTopicsWithoutContributors(regularTopics, epochId))
1164                 .isEqualTo(List.of(topic1, topic3));
1165     }
1166 
1167     // For test coverage only. The actual e2e logic is tested in TopicsWorkerTest. Methods invoked
1168     // are tested respectively in this test class.
1169     @Test
testHandleAppInstallationInRealTime()1170     public void testHandleAppInstallationInRealTime() {
1171         final String app = "app";
1172         final long epochId = 1L;
1173 
1174         AppUpdateManager appUpdateManager =
1175                 spy(new AppUpdateManager(mDbHelper, mTopicsDao, new Random(), mMockFlags));
1176 
1177         appUpdateManager.handleAppInstallationInRealTime(Uri.parse(app), epochId);
1178 
1179         verify(appUpdateManager).assignTopicsToNewlyInstalledApps(app, epochId);
1180     }
1181 
mockInstalledApplications(List<ApplicationInfo> applicationInfos)1182     private void mockInstalledApplications(List<ApplicationInfo> applicationInfos) {
1183         if (SdkLevel.isAtLeastT()) {
1184             when(mMockPackageManager.getInstalledApplications(
1185                             any(PackageManager.ApplicationInfoFlags.class)))
1186                     .thenReturn(applicationInfos);
1187         } else {
1188             when(mMockPackageManager.getInstalledApplications(anyInt()))
1189                     .thenReturn(applicationInfos);
1190         }
1191     }
1192 }
1193