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