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