• 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 package com.android.adservices.service.topics;
17 
18 import static android.adservices.common.AdServicesStatusUtils.STATUS_SUCCESS;
19 
20 import static com.google.common.truth.Truth.assertThat;
21 
22 import static org.junit.Assert.assertThrows;
23 import static org.mockito.ArgumentMatchers.any;
24 import static org.mockito.ArgumentMatchers.anyInt;
25 import static org.mockito.Mockito.doNothing;
26 import static org.mockito.Mockito.doReturn;
27 import static org.mockito.Mockito.eq;
28 import static org.mockito.Mockito.never;
29 import static org.mockito.Mockito.only;
30 import static org.mockito.Mockito.spy;
31 import static org.mockito.Mockito.times;
32 import static org.mockito.Mockito.verify;
33 import static org.mockito.Mockito.when;
34 
35 import android.adservices.topics.GetTopicsResult;
36 import android.app.adservices.AdServicesManager;
37 import android.app.adservices.topics.TopicParcel;
38 import android.content.Context;
39 import android.content.pm.ApplicationInfo;
40 import android.content.pm.PackageManager;
41 import android.net.Uri;
42 import android.util.Pair;
43 
44 import androidx.test.core.app.ApplicationProvider;
45 
46 import com.android.adservices.MockRandom;
47 import com.android.adservices.cobalt.TopicsCobaltLogger;
48 import com.android.adservices.data.DbHelper;
49 import com.android.adservices.data.DbTestUtil;
50 import com.android.adservices.data.topics.EncryptedTopic;
51 import com.android.adservices.data.topics.Topic;
52 import com.android.adservices.data.topics.TopicsDao;
53 import com.android.adservices.data.topics.TopicsTables;
54 import com.android.adservices.service.Flags;
55 import com.android.adservices.service.appsearch.AppSearchConsentManager;
56 import com.android.adservices.service.stats.AdServicesLogger;
57 import com.android.adservices.shared.testing.SdkLevelSupportRule;
58 import com.android.adservices.shared.util.Clock;
59 import com.android.modules.utils.build.SdkLevel;
60 
61 import com.google.common.collect.ImmutableList;
62 import com.google.common.truth.Correspondence;
63 
64 import org.junit.Before;
65 import org.junit.Rule;
66 import org.junit.Test;
67 import org.mockito.Mock;
68 import org.mockito.Mockito;
69 import org.mockito.MockitoAnnotations;
70 
71 import java.nio.charset.StandardCharsets;
72 import java.util.ArrayList;
73 import java.util.Arrays;
74 import java.util.Collections;
75 import java.util.HashMap;
76 import java.util.HashSet;
77 import java.util.List;
78 import java.util.Map;
79 import java.util.Random;
80 import java.util.Set;
81 import java.util.stream.Collectors;
82 
83 /** Unit test for {@link com.android.adservices.service.topics.TopicsWorker}. */
84 public class TopicsWorkerTest {
85     // Spy the Context to test app reconciliation
86     private final Context mContext = spy(ApplicationProvider.getApplicationContext());
87     private final DbHelper mDbHelper = spy(DbTestUtil.getDbHelperForTest());
88 
89     private TopicsWorker mTopicsWorker;
90     private TopicsDao mTopicsDao;
91     private CacheManager mCacheManager;
92     private BlockedTopicsManager mBlockedTopicsManager;
93     // Spy DbHelper to mock supportsTopContributorsTable feature.
94 
95     @Mock private EpochManager mMockEpochManager;
96     @Mock private Flags mMockFlags;
97     @Mock AdServicesLogger mLogger;
98     @Mock AdServicesManager mMockAdServicesManager;
99     @Mock AppSearchConsentManager mAppSearchConsentManager;
100     @Mock TopicsCobaltLogger mTopicsCobaltLogger;
101     @Mock Clock mClock;
102 
103     @Rule(order = 0)
104     public final SdkLevelSupportRule sdkLevel = SdkLevelSupportRule.forAtLeastS();
105 
106     @Before
setup()107     public void setup() {
108         MockitoAnnotations.initMocks(this);
109 
110         // Clean DB before each test
111         DbTestUtil.deleteTable(TopicsTables.TaxonomyContract.TABLE);
112         DbTestUtil.deleteTable(TopicsTables.AppClassificationTopicsContract.TABLE);
113         DbTestUtil.deleteTable(TopicsTables.CallerCanLearnTopicsContract.TABLE);
114         DbTestUtil.deleteTable(TopicsTables.TopTopicsContract.TABLE);
115         DbTestUtil.deleteTable(TopicsTables.ReturnedTopicContract.TABLE);
116         DbTestUtil.deleteTable(TopicsTables.UsageHistoryContract.TABLE);
117         DbTestUtil.deleteTable(TopicsTables.AppUsageHistoryContract.TABLE);
118         DbTestUtil.deleteTable(TopicsTables.BlockedTopicsContract.TABLE);
119         DbTestUtil.deleteTable(TopicsTables.TopicContributorsContract.TABLE);
120 
121         mTopicsDao = new TopicsDao(mDbHelper);
122         mBlockedTopicsManager =
123                 new BlockedTopicsManager(
124                         mTopicsDao,
125                         mMockAdServicesManager,
126                         mAppSearchConsentManager,
127                         Flags.PPAPI_AND_SYSTEM_SERVER,
128                         /* enableAppSearchConsent= */ false);
129         mCacheManager =
130                 new CacheManager(
131                         mTopicsDao,
132                         mMockFlags,
133                         mLogger,
134                         mBlockedTopicsManager,
135                         new GlobalBlockedTopicsManager(
136                                 /* globalBlockedTopicIds= */ new HashSet<>()),
137                         mTopicsCobaltLogger,
138                         mClock);
139         AppUpdateManager appUpdateManager =
140                 new AppUpdateManager(mDbHelper, mTopicsDao, new Random(), mMockFlags);
141 
142         mTopicsWorker =
143                 new TopicsWorker(
144                         mMockEpochManager,
145                         mCacheManager,
146                         mBlockedTopicsManager,
147                         appUpdateManager,
148                         mMockFlags);
149     }
150 
151     @Test
testGetTopics()152     public void testGetTopics() {
153         final long epochId = 4L;
154         final int numberOfLookBackEpochs = 3;
155         final Pair<String, String> appSdkKey = Pair.create("app", "sdk");
156         Topic topic1 = Topic.create(/* topic */ 1, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
157         Topic topic2 = Topic.create(/* topic */ 2, /* taxonomyVersion */ 2L, /* modelVersion */ 5L);
158         Topic topic3 = Topic.create(/* topic */ 3, /* taxonomyVersion */ 3L, /* modelVersion */ 6L);
159         Topic[] topics = {topic1, topic2, topic3};
160         // persist returned topics into DB
161         for (int numEpoch = 1; numEpoch <= numberOfLookBackEpochs; numEpoch++) {
162             Topic currentTopic = topics[numberOfLookBackEpochs - numEpoch];
163             Map<Pair<String, String>, Topic> returnedAppSdkTopicsMap = new HashMap<>();
164             returnedAppSdkTopicsMap.put(appSdkKey, currentTopic);
165             mTopicsDao.persistReturnedAppTopicsMap(numEpoch, returnedAppSdkTopicsMap);
166         }
167 
168         when(mMockEpochManager.getCurrentEpochId()).thenReturn(epochId);
169         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
170 
171         // Real Cache Manager requires loading cache before getTopics() being called.
172         mTopicsWorker.loadCache();
173 
174         GetTopicsResult getTopicsResult = mTopicsWorker.getTopics("app", "sdk");
175 
176         GetTopicsResult expectedGetTopicsResult =
177                 new GetTopicsResult.Builder()
178                         .setResultCode(STATUS_SUCCESS)
179                         .setTaxonomyVersions(Arrays.asList(1L, 2L, 3L))
180                         .setModelVersions(Arrays.asList(4L, 5L, 6L))
181                         .setTopics(Arrays.asList(1, 2, 3))
182                         .build();
183 
184         // Since the returned topic list is shuffled, elements have to be verified separately
185         assertThat(getTopicsResult.getResultCode())
186                 .isEqualTo(expectedGetTopicsResult.getResultCode());
187         assertThat(getTopicsResult.getTaxonomyVersions())
188                 .containsExactlyElementsIn(expectedGetTopicsResult.getTaxonomyVersions());
189         assertThat(getTopicsResult.getModelVersions())
190                 .containsExactlyElementsIn(expectedGetTopicsResult.getModelVersions());
191         assertThat(getTopicsResult.getTopics())
192                 .containsExactlyElementsIn(expectedGetTopicsResult.getTopics());
193 
194         // getTopic() + loadCache() + handleSdkTopicsAssignmentForAppInstallation()
195         verify(mMockEpochManager, times(3)).getCurrentEpochId();
196         verify(mMockFlags, times(3)).getTopicsNumberOfLookBackEpochs();
197     }
198 
199     @Test
testGetTopics_enableEncryption_disablePlaintextTopics()200     public void testGetTopics_enableEncryption_disablePlaintextTopics() {
201         final long epochId = 4L;
202         final int numberOfLookBackEpochs = 3;
203         final Pair<String, String> appSdkKey = Pair.create("app", "sdk");
204         Topic topic1 = Topic.create(/* topic */ 1, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
205         EncryptedTopic encryptedTopic1 =
206                 EncryptedTopic.create(
207                         /* encryptedTopic */ topic1.toString().getBytes(StandardCharsets.UTF_8),
208                         /* keyIdentifier */ "keyIdentifier1",
209                         /* encapsulatedKey */ "encapsulatedKey1".getBytes(StandardCharsets.UTF_8));
210         Topic topic2 = Topic.create(/* topic */ 2, /* taxonomyVersion */ 2L, /* modelVersion */ 5L);
211         EncryptedTopic encryptedTopic2 =
212                 EncryptedTopic.create(
213                         /* encryptedTopic */ topic2.toString().getBytes(StandardCharsets.UTF_8),
214                         /* keyIdentifier */ "keyIdentifier2",
215                         /* encapsulatedKey */ "encapsulatedKey2".getBytes(StandardCharsets.UTF_8));
216         Topic topic3 = Topic.create(/* topic */ 3, /* taxonomyVersion */ 3L, /* modelVersion */ 6L);
217         EncryptedTopic encryptedTopic3 =
218                 EncryptedTopic.create(
219                         /* encryptedTopic */ topic3.toString().getBytes(StandardCharsets.UTF_8),
220                         /* keyIdentifier */ "keyIdentifier3",
221                         /* encapsulatedKey */ "encapsulatedKey3".getBytes(StandardCharsets.UTF_8));
222         Topic[] topics = {topic1, topic2, topic3};
223         EncryptedTopic[] encryptedTopics = {encryptedTopic1, encryptedTopic2, encryptedTopic3};
224         // persist returned topics into DB
225         for (int numEpoch = 1; numEpoch <= numberOfLookBackEpochs; numEpoch++) {
226             Topic currentTopic = topics[numberOfLookBackEpochs - numEpoch];
227             Map<Pair<String, String>, Topic> returnedAppSdkTopicsMap = new HashMap<>();
228             returnedAppSdkTopicsMap.put(appSdkKey, currentTopic);
229             mTopicsDao.persistReturnedAppTopicsMap(numEpoch, returnedAppSdkTopicsMap);
230 
231             EncryptedTopic currentEncryptedTopic =
232                     encryptedTopics[numberOfLookBackEpochs - numEpoch];
233             Map<Pair<String, String>, EncryptedTopic> returnedEncryptedTopicsMap = new HashMap<>();
234             returnedEncryptedTopicsMap.put(appSdkKey, currentEncryptedTopic);
235             mTopicsDao.persistReturnedAppEncryptedTopicsMap(numEpoch, returnedEncryptedTopicsMap);
236         }
237 
238         when(mMockEpochManager.getCurrentEpochId()).thenReturn(epochId);
239         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
240         when(mMockFlags.getEnableDatabaseSchemaVersion9()).thenReturn(true);
241         when(mMockFlags.getTopicsEncryptionEnabled()).thenReturn(true);
242         when(mMockFlags.getTopicsDisablePlaintextResponse()).thenReturn(true);
243 
244         // Real Cache Manager requires loading cache before getTopics() being called.
245         mTopicsWorker.loadCache();
246 
247         GetTopicsResult getTopicsResult = mTopicsWorker.getTopics("app", "sdk");
248 
249         GetTopicsResult expectedGetTopicsResult =
250                 new GetTopicsResult.Builder()
251                         .setResultCode(STATUS_SUCCESS)
252                         .setTaxonomyVersions(List.of())
253                         .setModelVersions(List.of())
254                         .setTopics(List.of())
255                         .setEncryptedTopics(
256                                 Arrays.asList(
257                                         encryptedTopic1.getEncryptedTopic(),
258                                         encryptedTopic2.getEncryptedTopic(),
259                                         encryptedTopic3.getEncryptedTopic()))
260                         .setEncryptionKeys(
261                                 Arrays.asList(
262                                         encryptedTopic1.getKeyIdentifier(),
263                                         encryptedTopic2.getKeyIdentifier(),
264                                         encryptedTopic3.getKeyIdentifier()))
265                         .setEncapsulatedKeys(
266                                 Arrays.asList(
267                                         encryptedTopic1.getEncapsulatedKey(),
268                                         encryptedTopic2.getEncapsulatedKey(),
269                                         encryptedTopic3.getEncapsulatedKey()))
270                         .build();
271 
272         // Since the returned topic list is shuffled, elements have to be verified separately
273         assertThat(getTopicsResult.getResultCode())
274                 .isEqualTo(expectedGetTopicsResult.getResultCode());
275         assertThat(getTopicsResult.getTaxonomyVersions())
276                 .containsExactlyElementsIn(expectedGetTopicsResult.getTaxonomyVersions());
277         assertThat(getTopicsResult.getModelVersions())
278                 .containsExactlyElementsIn(expectedGetTopicsResult.getModelVersions());
279         assertThat(getTopicsResult.getTopics())
280                 .containsExactlyElementsIn(expectedGetTopicsResult.getTopics());
281         Correspondence<byte[], byte[]> equalByteArray =
282                 Correspondence.from(Arrays::equals, "Equal byte array check");
283         assertThat(getTopicsResult.getEncryptedTopics())
284                 .comparingElementsUsing(equalByteArray)
285                 .containsExactlyElementsIn(expectedGetTopicsResult.getEncryptedTopics());
286         assertThat(getTopicsResult.getEncryptionKeys())
287                 .containsExactlyElementsIn(expectedGetTopicsResult.getEncryptionKeys());
288         assertThat(getTopicsResult.getEncapsulatedKeys())
289                 .comparingElementsUsing(equalByteArray)
290                 .containsExactlyElementsIn(expectedGetTopicsResult.getEncapsulatedKeys());
291 
292         // getTopic() + loadCache() + handleSdkTopicsAssignmentForAppInstallation()
293         verify(mMockEpochManager, times(3)).getCurrentEpochId();
294         verify(mMockFlags, times(3)).getTopicsNumberOfLookBackEpochs();
295     }
296 
297     @Test
testGetTopics_enableEncryption_featureOn()298     public void testGetTopics_enableEncryption_featureOn() {
299         final long epochId = 4L;
300         final int numberOfLookBackEpochs = 3;
301         final Pair<String, String> appSdkKey = Pair.create("app", "sdk");
302         Topic topic1 = Topic.create(/* topic */ 1, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
303         EncryptedTopic encryptedTopic1 =
304                 EncryptedTopic.create(
305                         /* encryptedTopic */ topic1.toString().getBytes(StandardCharsets.UTF_8),
306                         /* keyIdentifier */ "keyIdentifier1",
307                         /* encapsulatedKey */ "encapsulatedKey1".getBytes(StandardCharsets.UTF_8));
308         Topic topic2 = Topic.create(/* topic */ 2, /* taxonomyVersion */ 2L, /* modelVersion */ 5L);
309         EncryptedTopic encryptedTopic2 =
310                 EncryptedTopic.create(
311                         /* encryptedTopic */ topic2.toString().getBytes(StandardCharsets.UTF_8),
312                         /* keyIdentifier */ "keyIdentifier2",
313                         /* encapsulatedKey */ "encapsulatedKey2".getBytes(StandardCharsets.UTF_8));
314         Topic topic3 = Topic.create(/* topic */ 3, /* taxonomyVersion */ 3L, /* modelVersion */ 6L);
315         EncryptedTopic encryptedTopic3 =
316                 EncryptedTopic.create(
317                         /* encryptedTopic */ topic3.toString().getBytes(StandardCharsets.UTF_8),
318                         /* keyIdentifier */ "keyIdentifier3",
319                         /* encapsulatedKey */ "encapsulatedKey3".getBytes(StandardCharsets.UTF_8));
320         Topic[] topics = {topic1, topic2, topic3};
321         EncryptedTopic[] encryptedTopics = {encryptedTopic1, encryptedTopic2, encryptedTopic3};
322         // persist returned topics into DB
323         for (int numEpoch = 1; numEpoch <= numberOfLookBackEpochs; numEpoch++) {
324             Topic currentTopic = topics[numberOfLookBackEpochs - numEpoch];
325             Map<Pair<String, String>, Topic> returnedAppSdkTopicsMap = new HashMap<>();
326             returnedAppSdkTopicsMap.put(appSdkKey, currentTopic);
327             mTopicsDao.persistReturnedAppTopicsMap(numEpoch, returnedAppSdkTopicsMap);
328 
329             EncryptedTopic currentEncryptedTopic =
330                     encryptedTopics[numberOfLookBackEpochs - numEpoch];
331             Map<Pair<String, String>, EncryptedTopic> returnedEncryptedTopicsMap = new HashMap<>();
332             returnedEncryptedTopicsMap.put(appSdkKey, currentEncryptedTopic);
333             mTopicsDao.persistReturnedAppEncryptedTopicsMap(numEpoch, returnedEncryptedTopicsMap);
334         }
335 
336         when(mMockEpochManager.getCurrentEpochId()).thenReturn(epochId);
337         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
338         when(mMockFlags.getEnableDatabaseSchemaVersion9()).thenReturn(true);
339         when(mMockFlags.getTopicsEncryptionEnabled()).thenReturn(true);
340 
341         // Real Cache Manager requires loading cache before getTopics() being called.
342         mTopicsWorker.loadCache();
343 
344         GetTopicsResult getTopicsResult = mTopicsWorker.getTopics("app", "sdk");
345 
346         GetTopicsResult expectedGetTopicsResult =
347                 new GetTopicsResult.Builder()
348                         .setResultCode(STATUS_SUCCESS)
349                         .setTaxonomyVersions(Arrays.asList(1L, 2L, 3L))
350                         .setModelVersions(Arrays.asList(4L, 5L, 6L))
351                         .setTopics(Arrays.asList(1, 2, 3))
352                         .setEncryptedTopics(
353                                 Arrays.asList(
354                                         encryptedTopic1.getEncryptedTopic(),
355                                         encryptedTopic2.getEncryptedTopic(),
356                                         encryptedTopic3.getEncryptedTopic()))
357                         .setEncryptionKeys(
358                                 Arrays.asList(
359                                         encryptedTopic1.getKeyIdentifier(),
360                                         encryptedTopic2.getKeyIdentifier(),
361                                         encryptedTopic3.getKeyIdentifier()))
362                         .setEncapsulatedKeys(
363                                 Arrays.asList(
364                                         encryptedTopic1.getEncapsulatedKey(),
365                                         encryptedTopic2.getEncapsulatedKey(),
366                                         encryptedTopic3.getEncapsulatedKey()))
367                         .build();
368 
369         // Since the returned topic list is shuffled, elements have to be verified separately
370         assertThat(getTopicsResult.getResultCode())
371                 .isEqualTo(expectedGetTopicsResult.getResultCode());
372         assertThat(getTopicsResult.getTaxonomyVersions())
373                 .containsExactlyElementsIn(expectedGetTopicsResult.getTaxonomyVersions());
374         assertThat(getTopicsResult.getModelVersions())
375                 .containsExactlyElementsIn(expectedGetTopicsResult.getModelVersions());
376         assertThat(getTopicsResult.getTopics())
377                 .containsExactlyElementsIn(expectedGetTopicsResult.getTopics());
378         Correspondence<byte[], byte[]> equalByteArray =
379                 Correspondence.from(Arrays::equals, "Equal byte array check");
380         assertThat(getTopicsResult.getEncryptedTopics())
381                 .comparingElementsUsing(equalByteArray)
382                 .containsExactlyElementsIn(expectedGetTopicsResult.getEncryptedTopics());
383         assertThat(getTopicsResult.getEncryptionKeys())
384                 .containsExactlyElementsIn(expectedGetTopicsResult.getEncryptionKeys());
385         assertThat(getTopicsResult.getEncapsulatedKeys())
386                 .comparingElementsUsing(equalByteArray)
387                 .containsExactlyElementsIn(expectedGetTopicsResult.getEncapsulatedKeys());
388 
389         // getTopic() + loadCache() + handleSdkTopicsAssignmentForAppInstallation()
390         verify(mMockEpochManager, times(3)).getCurrentEpochId();
391         verify(mMockFlags, times(3)).getTopicsNumberOfLookBackEpochs();
392     }
393 
394     @Test
testGetTopics_enableEncryption_featureOff()395     public void testGetTopics_enableEncryption_featureOff() {
396         final long epochId = 4L;
397         final int numberOfLookBackEpochs = 3;
398         final Pair<String, String> appSdkKey = Pair.create("app", "sdk");
399         Topic topic1 = Topic.create(/* topic */ 1, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
400         EncryptedTopic encryptedTopic1 =
401                 EncryptedTopic.create(
402                         /* encryptedTopic */ topic1.toString().getBytes(StandardCharsets.UTF_8),
403                         /* keyIdentifier */ "keyIdentifier1",
404                         /* encapsulatedKey */ "encapsulatedKey1".getBytes(StandardCharsets.UTF_8));
405         Topic topic2 = Topic.create(/* topic */ 2, /* taxonomyVersion */ 2L, /* modelVersion */ 5L);
406         EncryptedTopic encryptedTopic2 =
407                 EncryptedTopic.create(
408                         /* encryptedTopic */ topic2.toString().getBytes(StandardCharsets.UTF_8),
409                         /* keyIdentifier */ "keyIdentifier2",
410                         /* encapsulatedKey */ "encapsulatedKey2".getBytes(StandardCharsets.UTF_8));
411         Topic topic3 = Topic.create(/* topic */ 3, /* taxonomyVersion */ 3L, /* modelVersion */ 6L);
412         EncryptedTopic encryptedTopic3 =
413                 EncryptedTopic.create(
414                         /* encryptedTopic */ topic3.toString().getBytes(StandardCharsets.UTF_8),
415                         /* keyIdentifier */ "keyIdentifier3",
416                         /* encapsulatedKey */ "encapsulatedKey3".getBytes(StandardCharsets.UTF_8));
417         Topic[] topics = {topic1, topic2, topic3};
418         EncryptedTopic[] encryptedTopics = {encryptedTopic1, encryptedTopic2, encryptedTopic3};
419         // persist returned topics into DB
420         for (int numEpoch = 1; numEpoch <= numberOfLookBackEpochs; numEpoch++) {
421             Topic currentTopic = topics[numberOfLookBackEpochs - numEpoch];
422             Map<Pair<String, String>, Topic> returnedAppSdkTopicsMap = new HashMap<>();
423             returnedAppSdkTopicsMap.put(appSdkKey, currentTopic);
424             mTopicsDao.persistReturnedAppTopicsMap(numEpoch, returnedAppSdkTopicsMap);
425 
426             EncryptedTopic currentEncryptedTopic =
427                     encryptedTopics[numberOfLookBackEpochs - numEpoch];
428             Map<Pair<String, String>, EncryptedTopic> returnedEncryptedTopicsMap = new HashMap<>();
429             returnedEncryptedTopicsMap.put(appSdkKey, currentEncryptedTopic);
430             mTopicsDao.persistReturnedAppEncryptedTopicsMap(numEpoch, returnedEncryptedTopicsMap);
431         }
432 
433         when(mMockEpochManager.getCurrentEpochId()).thenReturn(epochId);
434         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
435 
436         // Feature off; Db flag on.
437         when(mMockFlags.getEnableDatabaseSchemaVersion9()).thenReturn(true);
438         when(mMockFlags.getTopicsEncryptionEnabled()).thenReturn(false);
439 
440         // Real Cache Manager requires loading cache before getTopics() being called.
441         mTopicsWorker.loadCache();
442 
443         GetTopicsResult getTopicsResult = mTopicsWorker.getTopics("app", "sdk");
444 
445         // Expect empty encrypted fields.
446         GetTopicsResult expectedGetTopicsResult =
447                 new GetTopicsResult.Builder()
448                         .setResultCode(STATUS_SUCCESS)
449                         .setTaxonomyVersions(Arrays.asList(1L, 2L, 3L))
450                         .setModelVersions(Arrays.asList(4L, 5L, 6L))
451                         .setTopics(Arrays.asList(1, 2, 3))
452                         .setEncryptedTopics(Arrays.asList())
453                         .setEncryptionKeys(Arrays.asList())
454                         .setEncapsulatedKeys(Arrays.asList())
455                         .build();
456 
457         // Since the returned topic list is shuffled, elements have to be verified separately
458         assertThat(getTopicsResult.getResultCode())
459                 .isEqualTo(expectedGetTopicsResult.getResultCode());
460         assertThat(getTopicsResult.getTaxonomyVersions())
461                 .containsExactlyElementsIn(expectedGetTopicsResult.getTaxonomyVersions());
462         assertThat(getTopicsResult.getModelVersions())
463                 .containsExactlyElementsIn(expectedGetTopicsResult.getModelVersions());
464         assertThat(getTopicsResult.getTopics())
465                 .containsExactlyElementsIn(expectedGetTopicsResult.getTopics());
466         Correspondence<byte[], byte[]> equalByteArray =
467                 Correspondence.from(Arrays::equals, "Equal byte array check");
468         assertThat(getTopicsResult.getEncryptedTopics())
469                 .comparingElementsUsing(equalByteArray)
470                 .containsExactlyElementsIn(expectedGetTopicsResult.getEncryptedTopics());
471         assertThat(getTopicsResult.getEncryptionKeys())
472                 .containsExactlyElementsIn(expectedGetTopicsResult.getEncryptionKeys());
473         assertThat(getTopicsResult.getEncapsulatedKeys())
474                 .comparingElementsUsing(equalByteArray)
475                 .containsExactlyElementsIn(expectedGetTopicsResult.getEncapsulatedKeys());
476 
477         // getTopic() + loadCache() + handleSdkTopicsAssignmentForAppInstallation()
478         verify(mMockEpochManager, times(3)).getCurrentEpochId();
479         verify(mMockFlags, times(3)).getTopicsNumberOfLookBackEpochs();
480     }
481 
482     @Test
testGetTopics_emptyCache()483     public void testGetTopics_emptyCache() {
484         final long epochId = 4L;
485 
486         when(mMockEpochManager.getCurrentEpochId()).thenReturn(epochId);
487 
488         // // There is no returned Topics persisted in the DB so cache is empty
489         mTopicsWorker.loadCache();
490 
491         GetTopicsResult getTopicsResult = mTopicsWorker.getTopics("app", "sdk");
492 
493         GetTopicsResult expectedGetTopicsResult =
494                 new GetTopicsResult.Builder()
495                         .setResultCode(STATUS_SUCCESS)
496                         .setTaxonomyVersions(Collections.emptyList())
497                         .setModelVersions(Collections.emptyList())
498                         .setTopics(Collections.emptyList())
499                         .build();
500 
501         // Since the returned topic list is shuffled, elements have to be verified separately
502         assertThat(getTopicsResult.getResultCode())
503                 .isEqualTo(expectedGetTopicsResult.getResultCode());
504         assertThat(getTopicsResult.getTaxonomyVersions())
505                 .containsExactlyElementsIn(expectedGetTopicsResult.getTaxonomyVersions());
506         assertThat(getTopicsResult.getModelVersions())
507                 .containsExactlyElementsIn(expectedGetTopicsResult.getModelVersions());
508         assertThat(getTopicsResult.getTopics())
509                 .containsExactlyElementsIn(expectedGetTopicsResult.getTopics());
510     }
511 
512     @Test
testGetTopics_appNotInCache()513     public void testGetTopics_appNotInCache() {
514         final long epochId = 4L;
515         final int numberOfLookBackEpochs = 1;
516         final Pair<String, String> appSdkKey = Pair.create("app", "sdk");
517         Topic topic1 = Topic.create(/* topic */ 1, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
518         Topic[] topics = {topic1};
519         // persist returned topics into DB
520         for (int numEpoch = 1; numEpoch <= numberOfLookBackEpochs; numEpoch++) {
521             Topic currentTopic = topics[numberOfLookBackEpochs - numEpoch];
522             Map<Pair<String, String>, Topic> returnedAppSdkTopicsMap = new HashMap<>();
523             returnedAppSdkTopicsMap.put(appSdkKey, currentTopic);
524             mTopicsDao.persistReturnedAppTopicsMap(numEpoch, returnedAppSdkTopicsMap);
525         }
526 
527         when(mMockEpochManager.getCurrentEpochId()).thenReturn(epochId);
528         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
529 
530         // Real Cache Manager requires loading cache before getTopics() being called.
531         mTopicsWorker.loadCache();
532 
533         GetTopicsResult getTopicsResult = mTopicsWorker.getTopics("app_not_in_cache", "sdk");
534 
535         GetTopicsResult expectedGetTopicsResult =
536                 new GetTopicsResult.Builder()
537                         .setResultCode(STATUS_SUCCESS)
538                         .setTaxonomyVersions(Collections.emptyList())
539                         .setModelVersions(Collections.emptyList())
540                         .setTopics(Collections.emptyList())
541                         .build();
542 
543         assertThat(getTopicsResult).isEqualTo(expectedGetTopicsResult);
544     }
545 
546     @Test
testGetTopics_sdkNotInCache()547     public void testGetTopics_sdkNotInCache() {
548         final long epochId = 4L;
549         final int numberOfLookBackEpochs = 1;
550         final Pair<String, String> appSdkKey = Pair.create("app", "sdk");
551         Topic topic1 = Topic.create(/* topic */ 1, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
552         Topic[] topics = {topic1};
553         // persist returned topics into DB
554         for (int numEpoch = 1; numEpoch <= numberOfLookBackEpochs; numEpoch++) {
555             Topic currentTopic = topics[numberOfLookBackEpochs - numEpoch];
556             Map<Pair<String, String>, Topic> returnedAppSdkTopicsMap = new HashMap<>();
557             returnedAppSdkTopicsMap.put(appSdkKey, currentTopic);
558             mTopicsDao.persistReturnedAppTopicsMap(numEpoch, returnedAppSdkTopicsMap);
559         }
560 
561         when(mMockEpochManager.getCurrentEpochId()).thenReturn(epochId);
562         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
563 
564         // Real Cache Manager requires loading cache before getTopics() being called.
565         mTopicsWorker.loadCache();
566 
567         GetTopicsResult getTopicsResult = mTopicsWorker.getTopics("app", "sdk_not_in_cache");
568 
569         GetTopicsResult expectedGetTopicsResult =
570                 new GetTopicsResult.Builder()
571                         .setResultCode(STATUS_SUCCESS)
572                         .setTaxonomyVersions(Collections.emptyList())
573                         .setModelVersions(Collections.emptyList())
574                         .setTopics(Collections.emptyList())
575                         .build();
576 
577         assertThat(getTopicsResult).isEqualTo(expectedGetTopicsResult);
578     }
579 
580     @Test
testGetTopics_handleSdkTopicAssignment()581     public void testGetTopics_handleSdkTopicAssignment() {
582         final int numberOfLookBackEpochs = 3;
583         final long currentEpochId = 5L;
584 
585         final String app = "app";
586         final String sdk = "sdk";
587 
588         Pair<String, String> appOnlyCaller = Pair.create(app, /* sdk */ "");
589 
590         Topic topic1 = Topic.create(/* topic */ 1, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
591         Topic topic2 = Topic.create(/* topic */ 2, /* taxonomyVersion */ 2L, /* modelVersion */ 5L);
592         Topic topic3 = Topic.create(/* topic */ 3, /* taxonomyVersion */ 3L, /* modelVersion */ 6L);
593         Topic[] topics = {topic1, topic2, topic3};
594 
595         for (long epoch = 0; epoch < numberOfLookBackEpochs; epoch++) {
596             long epochId = currentEpochId - 1 - epoch;
597             Topic topic = topics[(int) epoch];
598 
599             // Assign returned topics to app-only caller for epochs in [current - 3, current - 1]
600             mTopicsDao.persistReturnedAppTopicsMap(epochId, Map.of(appOnlyCaller, topic));
601 
602             // Make the topic learnable to app-sdk caller for epochs in [current - 3, current - 1].
603             // In order to achieve this, persist learnability in [current - 5, current - 3]. This
604             // ensures to test the earliest epoch to be learnt from.
605             long earliestEpochIdToLearnFrom = epochId - numberOfLookBackEpochs + 1;
606             mTopicsDao.persistCallerCanLearnTopics(
607                     earliestEpochIdToLearnFrom, Map.of(topic, Set.of(sdk)));
608         }
609 
610         when(mMockEpochManager.getCurrentEpochId()).thenReturn(currentEpochId);
611         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
612 
613         GetTopicsResult getTopicsResult = mTopicsWorker.getTopics(app, sdk);
614 
615         GetTopicsResult expectedGetTopicsResult =
616                 new GetTopicsResult.Builder()
617                         .setResultCode(STATUS_SUCCESS)
618                         .setTaxonomyVersions(Arrays.asList(1L, 2L, 3L))
619                         .setModelVersions(Arrays.asList(4L, 5L, 6L))
620                         .setTopics(Arrays.asList(1, 2, 3))
621                         .build();
622 
623         // Since the returned topic list is shuffled, elements have to be verified separately
624         assertThat(getTopicsResult.getResultCode())
625                 .isEqualTo(expectedGetTopicsResult.getResultCode());
626         assertThat(getTopicsResult.getTaxonomyVersions())
627                 .containsExactlyElementsIn(expectedGetTopicsResult.getTaxonomyVersions());
628         assertThat(getTopicsResult.getModelVersions())
629                 .containsExactlyElementsIn(expectedGetTopicsResult.getModelVersions());
630         assertThat(getTopicsResult.getTopics())
631                 .containsExactlyElementsIn(expectedGetTopicsResult.getTopics());
632     }
633 
634     @Test
testGetTopics_handleSdkTopicAssignment_existingTopicsForSdk()635     public void testGetTopics_handleSdkTopicAssignment_existingTopicsForSdk() {
636         final int numberOfLookBackEpochs = 3;
637         final long currentEpochId = 5L;
638 
639         final String app = "app";
640         final String sdk = "sdk";
641 
642         Pair<String, String> appOnlyCaller = Pair.create(app, /* sdk */ "");
643         Pair<String, String> appSdkCaller = Pair.create(app, sdk);
644 
645         Topic topic1 = Topic.create(/* topic */ 1, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
646         Topic topic2 = Topic.create(/* topic */ 2, /* taxonomyVersion */ 2L, /* modelVersion */ 5L);
647         Topic topic3 = Topic.create(/* topic */ 3, /* taxonomyVersion */ 3L, /* modelVersion */ 6L);
648         Topic[] topics = {topic1, topic2, topic3};
649 
650         for (long epoch = 0; epoch < numberOfLookBackEpochs; epoch++) {
651             long epochId = currentEpochId - 1 - epoch;
652             Topic topic = topics[(int) epoch];
653 
654             // Assign returned topics to app-only caller for epochs in [current - 3, current - 1]
655             mTopicsDao.persistReturnedAppTopicsMap(epochId, Map.of(appOnlyCaller, topic));
656 
657             // Make the topic learnable to app-sdk caller for epochs in [current - 3, current - 1].
658             // In order to achieve this, persist learnability in [current - 5, current - 3]. This
659             // ensures to test the earliest epoch to be learnt from.
660             long earliestEpochIdToLearnFrom = epochId - numberOfLookBackEpochs + 1;
661             mTopicsDao.persistCallerCanLearnTopics(
662                     earliestEpochIdToLearnFrom, Map.of(topic, Set.of(sdk)));
663         }
664 
665         // Current epoch is 5. Sdk has an existing topic in Epoch 2, which is an epoch in
666         // [current epoch - 3, current epoch - 1]
667         mTopicsDao.persistReturnedAppTopicsMap(
668                 currentEpochId - numberOfLookBackEpochs, Map.of(appSdkCaller, topic1));
669 
670         when(mMockEpochManager.getCurrentEpochId()).thenReturn(currentEpochId);
671         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
672 
673         mTopicsWorker.loadCache();
674         GetTopicsResult getTopicsResult = mTopicsWorker.getTopics(app, sdk);
675 
676         // Only the existing topic will be returned, i.e. No topic assignment has happened.
677         GetTopicsResult expectedGetTopicsResult =
678                 new GetTopicsResult.Builder()
679                         .setResultCode(STATUS_SUCCESS)
680                         .setTaxonomyVersions(List.of(1L))
681                         .setModelVersions(List.of(4L))
682                         .setTopics(List.of(1))
683                         .build();
684 
685         // Since the returned topic list is shuffled, elements have to be verified separately
686         assertThat(getTopicsResult.getResultCode())
687                 .isEqualTo(expectedGetTopicsResult.getResultCode());
688         assertThat(getTopicsResult.getTaxonomyVersions())
689                 .containsExactlyElementsIn(expectedGetTopicsResult.getTaxonomyVersions());
690         assertThat(getTopicsResult.getModelVersions())
691                 .containsExactlyElementsIn(expectedGetTopicsResult.getModelVersions());
692         assertThat(getTopicsResult.getTopics())
693                 .containsExactlyElementsIn(expectedGetTopicsResult.getTopics());
694     }
695 
696     @Test
testRecordUsage()697     public void testRecordUsage() {
698         mTopicsWorker.recordUsage("app", "sdk");
699         verify(mMockEpochManager, only()).recordUsageHistory(eq("app"), eq("sdk"));
700     }
701 
702     @Test
testComputeEpoch()703     public void testComputeEpoch() {
704         mTopicsWorker.computeEpoch();
705         verify(mMockEpochManager, times(1)).processEpoch();
706     }
707 
708     @Test
testGetKnownTopicsWithConsent_oneTopicBlocked()709     public void testGetKnownTopicsWithConsent_oneTopicBlocked() {
710         final long lastEpoch = 3;
711         final int numberOfLookBackEpochs = 3;
712         final Pair<String, String> appSdkKey = Pair.create("app", "sdk");
713         Topic topic1 = Topic.create(/* topic */ 1, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
714         Topic topic2 = Topic.create(/* topic */ 2, /* taxonomyVersion */ 2L, /* modelVersion */ 5L);
715         Topic topic3 = Topic.create(/* topic */ 3, /* taxonomyVersion */ 3L, /* modelVersion */ 6L);
716         Topic[] topics = {topic1, topic2, topic3};
717         // persist returned topics into Db
718         // populate topics for different epochs to get realistic state of the Db for testing
719         // blocked topics.
720         for (int numEpoch = 1; numEpoch <= numberOfLookBackEpochs; numEpoch++) {
721             Topic currentTopic = topics[numberOfLookBackEpochs - numEpoch];
722             Map<Pair<String, String>, Topic> returnedAppSdkTopicsMap = new HashMap<>();
723             returnedAppSdkTopicsMap.put(appSdkKey, currentTopic);
724             mTopicsDao.persistReturnedAppTopicsMap(numEpoch, returnedAppSdkTopicsMap);
725         }
726         when(mMockEpochManager.getCurrentEpochId()).thenReturn(lastEpoch);
727         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
728         Topic blockedTopic1 =
729                 Topic.create(/* topic */ 1, /* taxonomyVersion */ 3L, /* modelVersion */ 6L);
730 
731         // Mock IPC calls
732         TopicParcel topicParcel1 = blockedTopic1.convertTopicToTopicParcel();
733         doReturn(List.of(topicParcel1)).when(mMockAdServicesManager).retrieveAllBlockedTopics();
734         mTopicsDao.recordBlockedTopic(blockedTopic1);
735 
736         mTopicsWorker.loadCache();
737         ImmutableList<Topic> knownTopicsWithConsent = mTopicsWorker.getKnownTopicsWithConsent();
738         ImmutableList<Topic> topicsWithRevokedConsent = mTopicsWorker.getTopicsWithRevokedConsent();
739 
740         // there is only one blocked topic.
741         assertThat(topicsWithRevokedConsent).hasSize(1);
742         assertThat(topicsWithRevokedConsent).containsExactly(blockedTopic1);
743         // out of 3 existing topics, 2 of them are not blocked.
744         assertThat(knownTopicsWithConsent).hasSize(2);
745         assertThat(knownTopicsWithConsent).containsExactly(topic2, topic3);
746 
747         // Verify IPC calls
748         // loadCache() + retrieveAllBlockedTopics()
749         verify(mMockAdServicesManager, times(2)).retrieveAllBlockedTopics();
750     }
751 
752     @Test
testGetKnownTopicsWithConsent_allTopicsBlocked()753     public void testGetKnownTopicsWithConsent_allTopicsBlocked() {
754         final long lastEpoch = 3;
755         final int numberOfLookBackEpochs = 3;
756         final Pair<String, String> appSdkKey = Pair.create("app", "sdk");
757         Topic topic1 = Topic.create(/* topic */ 1, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
758         Topic topic2 = Topic.create(/* topic */ 2, /* taxonomyVersion */ 2L, /* modelVersion */ 5L);
759         Topic topic3 = Topic.create(/* topic */ 3, /* taxonomyVersion */ 3L, /* modelVersion */ 6L);
760         Topic[] topics = {topic1, topic2, topic3};
761         // persist returned topics into DB
762         for (int numEpoch = 1; numEpoch <= numberOfLookBackEpochs; numEpoch++) {
763             Topic currentTopic = topics[numberOfLookBackEpochs - numEpoch];
764             Map<Pair<String, String>, Topic> returnedAppSdkTopicsMap = new HashMap<>();
765             returnedAppSdkTopicsMap.put(appSdkKey, currentTopic);
766             mTopicsDao.persistReturnedAppTopicsMap(numEpoch, returnedAppSdkTopicsMap);
767         }
768         when(mMockEpochManager.getCurrentEpochId()).thenReturn(lastEpoch);
769         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
770 
771         List<TopicParcel> topicParcels =
772                 Arrays.stream(topics)
773                         .map(Topic::convertTopicToTopicParcel)
774                         .collect(Collectors.toList());
775         // Mock IPC calls
776         doReturn(topicParcels).when(mMockAdServicesManager).retrieveAllBlockedTopics();
777         // block all topics
778         mTopicsDao.recordBlockedTopic(topic1);
779         mTopicsDao.recordBlockedTopic(topic2);
780         mTopicsDao.recordBlockedTopic(topic3);
781 
782         mTopicsWorker.loadCache();
783         ImmutableList<Topic> knownTopicsWithConsent = mTopicsWorker.getKnownTopicsWithConsent();
784 
785         assertThat(knownTopicsWithConsent).isEmpty();
786 
787         // Verify IPC calls
788         verify(mMockAdServicesManager).retrieveAllBlockedTopics();
789     }
790 
791     @Test
testTopicsWithRevokedConsent()792     public void testTopicsWithRevokedConsent() {
793         Topic blockedTopic1 =
794                 Topic.create(/* topic */ 1, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
795         Topic blockedTopic2 =
796                 Topic.create(/* topic */ 2, /* taxonomyVersion */ 2L, /* modelVersion */ 5L);
797         Topic blockedTopic3 =
798                 Topic.create(/* topic */ 3, /* taxonomyVersion */ 3L, /* modelVersion */ 6L);
799 
800         // Mock IPC calls
801         TopicParcel topicParcel1 = blockedTopic1.convertTopicToTopicParcel();
802         TopicParcel topicParcel2 = blockedTopic2.convertTopicToTopicParcel();
803         TopicParcel topicParcel3 = blockedTopic3.convertTopicToTopicParcel();
804         doReturn(List.of(topicParcel1, topicParcel2, topicParcel3))
805                 .when(mMockAdServicesManager)
806                 .retrieveAllBlockedTopics();
807         // block all blockedTopics
808         mTopicsDao.recordBlockedTopic(blockedTopic1);
809         mTopicsDao.recordBlockedTopic(blockedTopic2);
810         mTopicsDao.recordBlockedTopic(blockedTopic3);
811 
812         // persist one not blocked topic.
813         final Pair<String, String> appSdkKey = Pair.create("app", "sdk");
814         Topic topic1 = Topic.create(/* topic */ 4, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
815         Map<Pair<String, String>, Topic> returnedAppSdkTopicsMap = new HashMap<>();
816         returnedAppSdkTopicsMap.put(appSdkKey, topic1);
817         mTopicsDao.persistReturnedAppTopicsMap(/* epochId */ 1, returnedAppSdkTopicsMap);
818 
819         mTopicsWorker.loadCache();
820         ImmutableList<Topic> topicsWithRevokedConsent = mTopicsWorker.getTopicsWithRevokedConsent();
821 
822         // Three topics are persisted into blocked topic table
823         assertThat(topicsWithRevokedConsent).hasSize(3);
824         assertThat(topicsWithRevokedConsent)
825                 .containsExactly(blockedTopic1, blockedTopic2, blockedTopic3);
826 
827         // Verify IPC calls
828         // loadCache() + retrieveAllBlockedTopics()
829         verify(mMockAdServicesManager, times(2)).retrieveAllBlockedTopics();
830     }
831 
832     @Test
testTopicsWithRevokedConsent_noTopicsBlocked()833     public void testTopicsWithRevokedConsent_noTopicsBlocked() {
834         final int numberOfLookBackEpochs = 3;
835         final Pair<String, String> appSdkKey = Pair.create("app", "sdk");
836         Topic topic1 = Topic.create(/* topic */ 1, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
837         Topic topic2 = Topic.create(/* topic */ 2, /* taxonomyVersion */ 2L, /* modelVersion */ 5L);
838         Topic topic3 = Topic.create(/* topic */ 3, /* taxonomyVersion */ 3L, /* modelVersion */ 6L);
839         Topic[] topics = {topic1, topic2, topic3};
840         // persist returned topics into DB
841         // populate topics for different epochs to get realistic state of the Db for testing
842         // blocked topics.
843         for (int numEpoch = 1; numEpoch <= numberOfLookBackEpochs; numEpoch++) {
844             Topic currentTopic = topics[numberOfLookBackEpochs - numEpoch];
845             Map<Pair<String, String>, Topic> returnedAppSdkTopicsMap = new HashMap<>();
846             returnedAppSdkTopicsMap.put(appSdkKey, currentTopic);
847             mTopicsDao.persistReturnedAppTopicsMap(numEpoch, returnedAppSdkTopicsMap);
848         }
849 
850         mTopicsWorker.loadCache();
851 
852         // Mock IPC calls
853         doReturn(List.of()).when(mMockAdServicesManager).retrieveAllBlockedTopics();
854         ImmutableList<Topic> topicsWithRevokedConsent = mTopicsWorker.getTopicsWithRevokedConsent();
855 
856         assertThat(topicsWithRevokedConsent).isEmpty();
857         // Verify IPC calls. loadCache() + retrieveAllBlockedTopics().
858         verify(mMockAdServicesManager, times(2)).retrieveAllBlockedTopics();
859     }
860 
861     @Test
testRevokeConsent()862     public void testRevokeConsent() {
863         Topic topic1 = Topic.create(/* topic */ 1, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
864         mTopicsWorker.loadCache();
865 
866         // Mock IPC calls
867         TopicParcel topicParcel1 = topic1.convertTopicToTopicParcel();
868         doNothing().when(mMockAdServicesManager).recordBlockedTopic(List.of(topicParcel1));
869         doReturn(List.of(topicParcel1)).when(mMockAdServicesManager).retrieveAllBlockedTopics();
870         mTopicsWorker.revokeConsentForTopic(topic1);
871 
872         ImmutableList<Topic> topicsWithRevokedConsent = mTopicsWorker.getTopicsWithRevokedConsent();
873 
874         assertThat(topicsWithRevokedConsent).hasSize(1);
875         assertThat(topicsWithRevokedConsent).containsExactly(topic1);
876 
877         // Verify IPC calls
878         verify(mMockAdServicesManager).recordBlockedTopic(List.of(topicParcel1));
879         // revokeConsentForTopic() + loadCache() + retrieveAllBlockedTopics()
880         verify(mMockAdServicesManager, times(3)).retrieveAllBlockedTopics();
881     }
882 
883     @Test
testRevokeAndRestoreConsent()884     public void testRevokeAndRestoreConsent() {
885         Topic topic1 = Topic.create(/* topic */ 1, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
886         mTopicsWorker.loadCache();
887 
888         // Mock IPC calls
889         TopicParcel topicParcel1 = topic1.convertTopicToTopicParcel();
890         doNothing().when(mMockAdServicesManager).recordBlockedTopic(List.of(topicParcel1));
891         doReturn(List.of(topicParcel1)).when(mMockAdServicesManager).retrieveAllBlockedTopics();
892         // Revoke consent for topic1
893         mTopicsWorker.revokeConsentForTopic(topic1);
894         ImmutableList<Topic> topicsWithRevokedConsent = mTopicsWorker.getTopicsWithRevokedConsent();
895 
896         assertThat(topicsWithRevokedConsent).hasSize(1);
897         assertThat(topicsWithRevokedConsent).containsExactly(topic1);
898 
899         // Verify IPC calls
900         verify(mMockAdServicesManager).recordBlockedTopic(List.of(topicParcel1));
901         // revokeConsentForTopic() + loadCache() + retrieveAllBlockedTopics()
902         verify(mMockAdServicesManager, times(3)).retrieveAllBlockedTopics();
903 
904         // Mock IPC calls
905         doNothing().when(mMockAdServicesManager).removeBlockedTopic(topicParcel1);
906         doReturn(List.of()).when(mMockAdServicesManager).retrieveAllBlockedTopics();
907         // Restore consent for topic1
908         mTopicsWorker.restoreConsentForTopic(topic1);
909         topicsWithRevokedConsent = mTopicsWorker.getTopicsWithRevokedConsent();
910 
911         assertThat(topicsWithRevokedConsent).isEmpty();
912 
913         // Verify IPC calls
914         verify(mMockAdServicesManager).removeBlockedTopic(topicParcel1);
915         // revokeConsentForTopic() * 2 + loadCache() + retrieveAllBlockedTopics() * 2
916         verify(mMockAdServicesManager, times(5)).retrieveAllBlockedTopics();
917     }
918 
919     @Test
testClearAllTopicsData()920     public void testClearAllTopicsData() {
921         final long epochId = 4L;
922         final int numberOfLookBackEpochs = 3;
923         final String app = "app";
924         final String sdk = "sdk";
925 
926         ArrayList<String> tableExclusionList = new ArrayList<>();
927         tableExclusionList.add(TopicsTables.BlockedTopicsContract.TABLE);
928 
929         Topic topic1 = Topic.create(/* topic */ 1, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
930         Topic topic2 = Topic.create(/* topic */ 2, /* taxonomyVersion */ 2L, /* modelVersion */ 5L);
931         Topic topic3 = Topic.create(/* topic */ 3, /* taxonomyVersion */ 3L, /* modelVersion */ 6L);
932         Topic[] topics = {topic1, topic2, topic3};
933         // persist returned topics into DB
934         for (int numEpoch = 1; numEpoch <= numberOfLookBackEpochs; numEpoch++) {
935             Topic currentTopic = topics[numberOfLookBackEpochs - numEpoch];
936             Map<Pair<String, String>, Topic> returnedAppSdkTopicsMap = new HashMap<>();
937 
938             // Test both cases of app and app-sdk calling getTopics()
939             returnedAppSdkTopicsMap.put(Pair.create(app, sdk), currentTopic);
940             returnedAppSdkTopicsMap.put(Pair.create(app, /* sdk */ ""), currentTopic);
941             mTopicsDao.persistReturnedAppTopicsMap(numEpoch, returnedAppSdkTopicsMap);
942         }
943 
944         // Mock IPC calls
945         TopicParcel topicParcel1 = topic1.convertTopicToTopicParcel();
946         doReturn(List.of(topicParcel1)).when(mMockAdServicesManager).retrieveAllBlockedTopics();
947         mTopicsDao.recordBlockedTopic(topic1);
948 
949         when(mMockEpochManager.getCurrentEpochId()).thenReturn(epochId);
950         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
951 
952         // Real Cache Manager requires loading cache before getTopics() being called.
953         mTopicsWorker.loadCache();
954 
955         // Verify topics are persisted in the database
956         GetTopicsResult expectedGetTopicsResult =
957                 new GetTopicsResult.Builder()
958                         .setResultCode(STATUS_SUCCESS)
959                         .setTaxonomyVersions(Arrays.asList(2L, 3L))
960                         .setModelVersions(Arrays.asList(5L, 6L))
961                         .setTopics(Arrays.asList(2, 3))
962                         .build();
963         GetTopicsResult getTopicsResultAppOnly1 = mTopicsWorker.getTopics(app, /* sdk */ "");
964         GetTopicsResult getTopicsResultAppSdk1 = mTopicsWorker.getTopics(app, sdk);
965 
966         // Since the returned topic list is shuffled, elements have to be verified separately
967         assertThat(getTopicsResultAppOnly1.getResultCode())
968                 .isEqualTo(expectedGetTopicsResult.getResultCode());
969         assertThat(getTopicsResultAppOnly1.getTaxonomyVersions())
970                 .containsExactlyElementsIn(expectedGetTopicsResult.getTaxonomyVersions());
971         assertThat(getTopicsResultAppOnly1.getModelVersions())
972                 .containsExactlyElementsIn(expectedGetTopicsResult.getModelVersions());
973         assertThat(getTopicsResultAppOnly1.getTopics())
974                 .containsExactlyElementsIn(expectedGetTopicsResult.getTopics());
975         assertThat(getTopicsResultAppSdk1.getResultCode())
976                 .isEqualTo(expectedGetTopicsResult.getResultCode());
977         assertThat(getTopicsResultAppSdk1.getTaxonomyVersions())
978                 .containsExactlyElementsIn(expectedGetTopicsResult.getTaxonomyVersions());
979         assertThat(getTopicsResultAppSdk1.getModelVersions())
980                 .containsExactlyElementsIn(expectedGetTopicsResult.getModelVersions());
981         assertThat(getTopicsResultAppSdk1.getTopics())
982                 .containsExactlyElementsIn(expectedGetTopicsResult.getTopics());
983 
984         // Mock AdServicesManager.clearAllBlockedTopics
985         doNothing().when(mMockAdServicesManager).clearAllBlockedTopics();
986         // Clear all data in database belonging to app except blocked topics table
987         mTopicsWorker.clearAllTopicsData(tableExclusionList);
988         assertThat(mTopicsDao.retrieveAllBlockedTopics()).isNotEmpty();
989         // Verify AdServicesManager.clearAllBlockedTopics is not invoked because tableExclusionList
990         // contains blocked topics table
991         verify(mMockAdServicesManager, never()).clearAllBlockedTopics();
992 
993         mTopicsWorker.clearAllTopicsData(new ArrayList<>());
994         assertThat(mTopicsDao.retrieveAllBlockedTopics()).isEmpty();
995         // Verify AdServicesManager.clearAllBlockedTopics is invoked
996         verify(mMockAdServicesManager, times(1)).clearAllBlockedTopics();
997 
998         GetTopicsResult emptyGetTopicsResult =
999                 new GetTopicsResult.Builder()
1000                         .setResultCode(STATUS_SUCCESS)
1001                         .setTaxonomyVersions(Collections.emptyList())
1002                         .setModelVersions(Collections.emptyList())
1003                         .setTopics(Collections.emptyList())
1004                         .build();
1005 
1006         assertThat(mTopicsWorker.getTopics(app, sdk)).isEqualTo(emptyGetTopicsResult);
1007         assertThat(mTopicsWorker.getTopics(app, /* sdk */ "")).isEqualTo(emptyGetTopicsResult);
1008 
1009         // Verify IPC calls
1010         // 1 loadCache() + 2 clearAllTopicsData()
1011         verify(mMockAdServicesManager, times(3)).retrieveAllBlockedTopics();
1012     }
1013 
1014     @Test
testClearAllTopicsData_topicContributorsTable()1015     public void testClearAllTopicsData_topicContributorsTable() {
1016         final long epochId = 1;
1017         final int topicId = 1;
1018         final String app = "app";
1019         Map<Integer, Set<String>> topicContributorsMap = Map.of(topicId, Set.of(app));
1020         mTopicsDao.persistTopicContributors(epochId, topicContributorsMap);
1021 
1022         // Mock AdServicesManager.clearAllBlockedTopics
1023         doNothing().when(mMockAdServicesManager).clearAllBlockedTopics();
1024 
1025         // To test feature flag is on
1026         mTopicsWorker.clearAllTopicsData(/* tables to exclude */ new ArrayList<>());
1027         // TopicContributors table be cleared.
1028         assertThat(mTopicsDao.retrieveTopicToContributorsMap(epochId)).isEmpty();
1029         // Verify AdServicesManager.clearAllBlockedTopics is invoked
1030         verify(mMockAdServicesManager).clearAllBlockedTopics();
1031     }
1032 
1033     @Test
testClearAllTopicsData_ImmutableList()1034     public void testClearAllTopicsData_ImmutableList() {
1035         assertThrows(
1036                 ClassCastException.class,
1037                 () -> mTopicsWorker.clearAllTopicsData((ArrayList<String>) List.of("anyString")));
1038     }
1039 
1040     @Test
testReconcileApplicationUpdate()1041     public void testReconcileApplicationUpdate() {
1042         final String app1 = "app1"; // regular app
1043         final String app2 = "app2"; // unhandled uninstalled app
1044         final String app3 = "app3"; // unhandled installed app
1045         final String app4 = "app4"; // uninstalled app but with only usage
1046         final String app5 = "app5"; // installed app but with only returned topic
1047         final String sdk = "sdk";
1048 
1049         final long currentEpochId = 4L;
1050         final long taxonomyVersion = 1L;
1051         final long modelVersion = 1L;
1052         final int numOfLookBackEpochs = 3;
1053         final int topicsNumberOfTopTopics = 5;
1054         final int topicsPercentageForRandomTopic = 5;
1055 
1056         Topic topic1 = Topic.create(/* topic */ 1, taxonomyVersion, modelVersion);
1057         Topic topic2 = Topic.create(/* topic */ 2, taxonomyVersion, modelVersion);
1058         Topic topic3 = Topic.create(/* topic */ 3, taxonomyVersion, modelVersion);
1059         Topic topic4 = Topic.create(/* topic */ 4, taxonomyVersion, modelVersion);
1060         Topic topic5 = Topic.create(/* topic */ 5, taxonomyVersion, modelVersion);
1061         Topic topic6 = Topic.create(/* topic */ 6, taxonomyVersion, modelVersion);
1062         List<Topic> topTopics = List.of(topic1, topic2, topic3, topic4, topic5, topic6);
1063 
1064         // In order to mock Package Manager, context also needs to be mocked to return
1065         // mocked Package Manager
1066         PackageManager mockPackageManager = Mockito.mock(PackageManager.class);
1067         when(mContext.getPackageManager()).thenReturn(mockPackageManager);
1068 
1069         // Mock Package Manager for installed applications
1070         // Note app2 is not here to mock uninstallation, app3 is here to mock installation
1071         ApplicationInfo appInfo1 = new ApplicationInfo();
1072         appInfo1.packageName = app1;
1073         ApplicationInfo appInfo3 = new ApplicationInfo();
1074         appInfo3.packageName = app3;
1075 
1076         if (SdkLevel.isAtLeastT()) {
1077             when(mockPackageManager.getInstalledApplications(
1078                             any(PackageManager.ApplicationInfoFlags.class)))
1079                     .thenReturn(List.of(appInfo1, appInfo3));
1080         } else {
1081             when(mockPackageManager.getInstalledApplications(anyInt()))
1082                     .thenReturn(List.of(appInfo1, appInfo3));
1083         }
1084 
1085         // As selectAssignedTopicFromTopTopics() randomly assigns a top topic, pass in a Mocked
1086         // Random object to make the result deterministic.
1087         //
1088         // In this test, topic 1, 2, and 6 are supposed to be returned. For each topic, it needs 2
1089         // random draws: the first is to determine whether to select a random topic, the second is
1090         // draw the actual topic index.
1091         MockRandom mockRandom =
1092                 new MockRandom(
1093                         new long[] {
1094                             topicsPercentageForRandomTopic, // Will select a regular topic
1095                             0, // Index of first topic
1096                             topicsPercentageForRandomTopic, // Will select a regular topic
1097                             1, // Index of second topic
1098                             0, // Will select a random topic
1099                             0 // Select the first random topic
1100                         });
1101         AppUpdateManager appUpdateManager =
1102                 new AppUpdateManager(mDbHelper, mTopicsDao, mockRandom, mMockFlags);
1103 
1104         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numOfLookBackEpochs);
1105         when(mMockFlags.getTopicsNumberOfTopTopics()).thenReturn(topicsNumberOfTopTopics);
1106         when(mMockFlags.getTopicsPercentageForRandomTopic())
1107                 .thenReturn(topicsPercentageForRandomTopic);
1108 
1109         Topic[] topics1 = {topic1, topic2, topic3};
1110         Topic[] topics2 = {topic4, topic5, topic6};
1111         for (int numEpoch = 0; numEpoch < numOfLookBackEpochs; numEpoch++) {
1112             long epochId = currentEpochId - 1 - numEpoch;
1113             // Persist returned topics into DB
1114             Topic currentTopic1 = topics1[numEpoch];
1115             Map<Pair<String, String>, Topic> returnedAppSdkTopicsMap1 = new HashMap<>();
1116             returnedAppSdkTopicsMap1.put(Pair.create(app1, sdk), currentTopic1);
1117             mTopicsDao.persistReturnedAppTopicsMap(epochId, returnedAppSdkTopicsMap1);
1118 
1119             Topic currentTopic2 = topics2[numEpoch];
1120             Map<Pair<String, String>, Topic> returnedAppSdkTopicsMap2 = new HashMap<>();
1121             returnedAppSdkTopicsMap2.put(Pair.create(app2, sdk), currentTopic2);
1122             mTopicsDao.persistReturnedAppTopicsMap(epochId, returnedAppSdkTopicsMap2);
1123 
1124             // Persist top topics
1125             mTopicsDao.persistTopTopics(epochId, topTopics);
1126 
1127             // Since AppUpdateManager evaluates previously installed apps through App Usage, usages
1128             // should be persisted into database.
1129             //
1130             // Note app3 doesn't have usage as newly installation. And app4 only has usage but
1131             // doesn't have returned topics
1132             mTopicsDao.recordAppUsageHistory(epochId, app1);
1133             mTopicsDao.recordAppUsageHistory(epochId, app2);
1134             mTopicsDao.recordAppUsageHistory(epochId, app4);
1135             // Persist into AppSdkUsage table to mimic reality but this is unnecessary.
1136             mTopicsDao.recordUsageHistory(epochId, app1, sdk);
1137             mTopicsDao.recordUsageHistory(epochId, app2, sdk);
1138             mTopicsDao.recordUsageHistory(epochId, app4, sdk);
1139 
1140             // Persist topics to TopicContributors Table avoid being filtered out
1141             for (Topic topic : topTopics) {
1142                 mTopicsDao.persistTopicContributors(
1143                         epochId, Map.of(topic.getTopic(), Set.of(app1, app2, app3, app4)));
1144             }
1145         }
1146         // Persist returned topic to app 5. Note that the epoch id to persist is older than
1147         // (currentEpochId - numOfLookBackEpochs). Therefore, app5 won't be handled as a newly
1148         // installed app.
1149         mTopicsDao.persistReturnedAppTopicsMap(
1150                 currentEpochId - numOfLookBackEpochs - 1, Map.of(Pair.create(app5, ""), topic1));
1151 
1152         when(mMockEpochManager.getCurrentEpochId()).thenReturn(currentEpochId);
1153         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numOfLookBackEpochs);
1154 
1155         // Initialize a local TopicsWorker to use mocked AppUpdateManager
1156         TopicsWorker topicsWorker =
1157                 new TopicsWorker(
1158                         mMockEpochManager,
1159                         mCacheManager,
1160                         mBlockedTopicsManager,
1161                         appUpdateManager,
1162                         mMockFlags);
1163         // Reconcile the unhandled uninstalled apps.
1164         // As PackageManager is mocked, app2 will be identified as unhandled uninstalled app.
1165         // All data belonging to app2 will be deleted.
1166         topicsWorker.reconcileApplicationUpdate(mContext);
1167 
1168         // Both reconciling uninstalled apps and installed apps call these mocked functions
1169         verify(mContext, times(2)).getPackageManager();
1170 
1171         PackageManager verifier = verify(mockPackageManager, times(2));
1172         if (SdkLevel.isAtLeastT()) {
1173             verifier.getInstalledApplications(any(PackageManager.ApplicationInfoFlags.class));
1174         } else {
1175             verifier.getInstalledApplications(anyInt());
1176         }
1177 
1178         // App1 should get topics 1, 2, 3
1179         GetTopicsResult expectedGetTopicsResult1 =
1180                 new GetTopicsResult.Builder()
1181                         .setResultCode(STATUS_SUCCESS)
1182                         .setTaxonomyVersions(
1183                                 Arrays.asList(taxonomyVersion, taxonomyVersion, taxonomyVersion))
1184                         .setModelVersions(Arrays.asList(modelVersion, modelVersion, modelVersion))
1185                         .setTopics(Arrays.asList(1, 2, 3))
1186                         .build();
1187         GetTopicsResult getTopicsResult1 = topicsWorker.getTopics(app1, sdk);
1188         // Since the returned topic list is shuffled, elements have to be verified separately
1189         assertThat(getTopicsResult1.getResultCode())
1190                 .isEqualTo(expectedGetTopicsResult1.getResultCode());
1191         assertThat(getTopicsResult1.getTaxonomyVersions())
1192                 .containsExactlyElementsIn(expectedGetTopicsResult1.getTaxonomyVersions());
1193         assertThat(getTopicsResult1.getModelVersions())
1194                 .containsExactlyElementsIn(expectedGetTopicsResult1.getModelVersions());
1195         assertThat(getTopicsResult1.getTopics())
1196                 .containsExactlyElementsIn(expectedGetTopicsResult1.getTopics());
1197 
1198         // App2 is uninstalled so should return empty topics.
1199         GetTopicsResult emptyGetTopicsResult =
1200                 new GetTopicsResult.Builder()
1201                         .setResultCode(STATUS_SUCCESS)
1202                         .setTaxonomyVersions(Collections.emptyList())
1203                         .setModelVersions(Collections.emptyList())
1204                         .setTopics(Collections.emptyList())
1205                         .build();
1206         assertThat((topicsWorker.getTopics(app2, sdk))).isEqualTo(emptyGetTopicsResult);
1207 
1208         // App4 is uninstalled so usage table should be clear. As it originally doesn't have
1209         // returned topic, getTopic won't be checked
1210         assertThat(
1211                         mTopicsDao.retrieveDistinctAppsFromTables(
1212                                 List.of(TopicsTables.AppUsageHistoryContract.TABLE),
1213                                 List.of(TopicsTables.AppUsageHistoryContract.APP)))
1214                 .doesNotContain(app4);
1215 
1216         // App3 is newly installed and should topics 1, 2, 6
1217         GetTopicsResult expectedGetTopicsResult3 =
1218                 new GetTopicsResult.Builder()
1219                         .setResultCode(STATUS_SUCCESS)
1220                         .setTaxonomyVersions(
1221                                 Arrays.asList(taxonomyVersion, taxonomyVersion, taxonomyVersion))
1222                         .setModelVersions(Arrays.asList(modelVersion, modelVersion, modelVersion))
1223                         .setTopics(Arrays.asList(1, 2, 6))
1224                         .build();
1225         GetTopicsResult getTopicsResult3 = topicsWorker.getTopics(app3, /* sdk */ "");
1226         // Since the returned topic list is shuffled, elements have to be verified separately
1227         assertThat(getTopicsResult3.getResultCode())
1228                 .isEqualTo(expectedGetTopicsResult3.getResultCode());
1229         assertThat(getTopicsResult3.getTaxonomyVersions())
1230                 .containsExactlyElementsIn(expectedGetTopicsResult3.getTaxonomyVersions());
1231         assertThat(getTopicsResult3.getModelVersions())
1232                 .containsExactlyElementsIn(expectedGetTopicsResult3.getModelVersions());
1233         assertThat(getTopicsResult3.getTopics())
1234                 .containsExactlyElementsIn(expectedGetTopicsResult3.getTopics());
1235 
1236         // App5 has a returned topic in old epoch, so it won't be regarded as newly installed app
1237         // Therefore, it won't get any topic in recent epochs.
1238         assertThat((topicsWorker.getTopics(app5, sdk))).isEqualTo(emptyGetTopicsResult);
1239 
1240         verify(mMockFlags).getTopicsNumberOfTopTopics();
1241         verify(mMockFlags).getTopicsPercentageForRandomTopic();
1242     }
1243 
1244     @Test
testHandleAppUninstallation()1245     public void testHandleAppUninstallation() {
1246         final long epochId = 4L;
1247         final int numberOfLookBackEpochs = 3;
1248         final String app = "app";
1249         final String sdk = "sdk";
1250 
1251         final Pair<String, String> appSdkKey = Pair.create(app, sdk);
1252         Topic topic1 = Topic.create(/* topic */ 1, /* taxonomyVersion */ 1L, /* modelVersion */ 4L);
1253         Topic topic2 = Topic.create(/* topic */ 2, /* taxonomyVersion */ 2L, /* modelVersion */ 5L);
1254         Topic topic3 = Topic.create(/* topic */ 3, /* taxonomyVersion */ 3L, /* modelVersion */ 6L);
1255         Topic[] topics = {topic1, topic2, topic3};
1256         // persist returned topics into DB
1257         for (int numEpoch = 1; numEpoch <= numberOfLookBackEpochs; numEpoch++) {
1258             Topic currentTopic = topics[numberOfLookBackEpochs - numEpoch];
1259             Map<Pair<String, String>, Topic> returnedAppSdkTopicsMap = new HashMap<>();
1260             returnedAppSdkTopicsMap.put(appSdkKey, currentTopic);
1261             mTopicsDao.persistReturnedAppTopicsMap(numEpoch, returnedAppSdkTopicsMap);
1262         }
1263 
1264         when(mMockEpochManager.getCurrentEpochId()).thenReturn(epochId);
1265         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numberOfLookBackEpochs);
1266 
1267         // Real Cache Manager requires loading cache before getTopics() being called.
1268         mTopicsWorker.loadCache();
1269 
1270         GetTopicsResult getTopicsResult = mTopicsWorker.getTopics(app, sdk);
1271 
1272         verify(mMockEpochManager, times(3)).getCurrentEpochId();
1273         verify(mMockFlags, times(3)).getTopicsNumberOfLookBackEpochs();
1274 
1275         GetTopicsResult expectedGetTopicsResult =
1276                 new GetTopicsResult.Builder()
1277                         .setResultCode(STATUS_SUCCESS)
1278                         .setTaxonomyVersions(Arrays.asList(1L, 2L, 3L))
1279                         .setModelVersions(Arrays.asList(4L, 5L, 6L))
1280                         .setTopics(Arrays.asList(1, 2, 3))
1281                         .build();
1282 
1283         // Since the returned topic list is shuffled, elements have to be verified separately
1284         assertThat(getTopicsResult.getResultCode())
1285                 .isEqualTo(expectedGetTopicsResult.getResultCode());
1286         assertThat(getTopicsResult.getTaxonomyVersions())
1287                 .containsExactlyElementsIn(expectedGetTopicsResult.getTaxonomyVersions());
1288         assertThat(getTopicsResult.getModelVersions())
1289                 .containsExactlyElementsIn(expectedGetTopicsResult.getModelVersions());
1290         assertThat(getTopicsResult.getTopics())
1291                 .containsExactlyElementsIn(expectedGetTopicsResult.getTopics());
1292 
1293         // Delete data belonging to the app
1294         Uri packageUri = Uri.parse("package:" + app);
1295         mTopicsWorker.handleAppUninstallation(packageUri);
1296 
1297         GetTopicsResult emptyGetTopicsResult =
1298                 new GetTopicsResult.Builder()
1299                         .setResultCode(STATUS_SUCCESS)
1300                         .setTaxonomyVersions(Collections.emptyList())
1301                         .setModelVersions(Collections.emptyList())
1302                         .setTopics(Collections.emptyList())
1303                         .build();
1304         assertThat((mTopicsWorker.getTopics(app, sdk))).isEqualTo(emptyGetTopicsResult);
1305     }
1306 
1307     @Test
testHandleAppUninstallation_handleTopTopicsWithoutContributors()1308     public void testHandleAppUninstallation_handleTopTopicsWithoutContributors() {
1309         // The test sets up to handle below scenarios:
1310         // * Both app1 and app2 have usage in the epoch and all 6 topics are top topics.
1311         // * app1 is classified to topic1, topic2. app2 is classified to topic1 and topic3.
1312         // * Both app1 and app2 calls Topics API via .
1313         // * In Epoch1, as app2 is able to learn topic2 via sdk, though topic2 is not a classified
1314         //   topic of app2, both app1 and app2 can have topic2 as the returned topic.
1315         // * In Epoch4, app1 gets uninstalled. Since app1 is the only contributor of topic2, topic2
1316         //   will be deleted from epoch1. Therefore, app2 will also have empty returned topic in
1317         //   Epoch1, along with app1.
1318         // * Comparison case in Epoch2 (multiple contributors): Both app1 and app3 has usages and
1319         //   are classified topic1 and topic2 with topic1 as the returned topic . When app1 gets
1320         //   uninstalled in Epoch4, app3 will still be able to return topic2 which comes from
1321         //   Epoch2.
1322         // * Comparison case in Epoch3 (the feature topic is only removed on epoch basis): app4 has
1323         //   same setup as app2 in Epoch1: it has topic2 as returned topic in Epoch1, but is NOT
1324         //   classified to topic2. app4 also has topic2 as returned topic in Epoch3. Therefore, when
1325         //   app1 is uninstalled in Epoch4, topic1 will be removed for app4 as returned topic in
1326         //   Epoch1 but not in Epoch3. So if app4 calls Topics API in Epoch4, it's still able to
1327         //   return topic1 as a result.
1328         final long epochId1 = 1;
1329         final long epochId2 = 2;
1330         final long epochId3 = 3;
1331         final long epochId4 = 4;
1332         final long taxonomyVersion = 1L;
1333         final long modelVersion = 1L;
1334         final String app1 = "app1"; // app to uninstall at Epoch4
1335         final String app2 = "app2"; // positive case to verify the removal of the returned topic
1336         final String app3 = "app3"; // negative case to verify scenario of multiple contributors
1337         final String app4 = "app4"; // negative ase to verify the removal is on epoch basis
1338         final String sdk = "sdk";
1339         Topic topic1 = Topic.create(/* topic */ 1, taxonomyVersion, modelVersion);
1340         Topic topic2 = Topic.create(/* topic */ 2, taxonomyVersion, modelVersion);
1341         Topic topic3 = Topic.create(/* topic */ 3, taxonomyVersion, modelVersion);
1342         Topic topic4 = Topic.create(/* topic */ 4, taxonomyVersion, modelVersion);
1343         Topic topic5 = Topic.create(/* topic */ 5, taxonomyVersion, modelVersion);
1344         Topic topic6 = Topic.create(/* topic */ 6, taxonomyVersion, modelVersion);
1345 
1346         // Set the number in flag so that it doesn't depend on the actual value in PhFlags.
1347         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(3);
1348 
1349         // Persist Top topics for epoch1 ~ epoch4
1350         mTopicsDao.persistTopTopics(
1351                 epochId1, List.of(topic1, topic2, topic3, topic4, topic5, topic6));
1352         mTopicsDao.persistTopTopics(
1353                 epochId2, List.of(topic1, topic2, topic3, topic4, topic5, topic6));
1354         mTopicsDao.persistTopTopics(
1355                 epochId3, List.of(topic1, topic2, topic3, topic4, topic5, topic6));
1356 
1357         // Persist AppClassificationTopics Table
1358         mTopicsDao.persistAppClassificationTopics(
1359                 epochId1,
1360                 Map.of(
1361                         app1, List.of(topic1, topic2),
1362                         app2, List.of(topic1, topic3),
1363                         app4, List.of(topic1, topic3)));
1364         mTopicsDao.persistAppClassificationTopics(
1365                 epochId2, // app1 and app3 have same setup in epoch2
1366                 Map.of(
1367                         app1, List.of(topic1, topic2),
1368                         app3, List.of(topic1, topic2)));
1369         mTopicsDao.persistAppClassificationTopics(
1370                 epochId3, // app4 has topic2 as returned topic in epoch3, which won't be removed.
1371                 Map.of(app4, List.of(topic2)));
1372 
1373         // Compute and persist TopicContributors table
1374         mTopicsDao.persistTopicContributors(
1375                 epochId1,
1376                 Map.of(
1377                         topic1.getTopic(), Set.of(app1, app2, app4),
1378                         topic2.getTopic(), Set.of(app1),
1379                         topic3.getTopic(), Set.of(app2, app4)));
1380         mTopicsDao.persistTopicContributors(
1381                 epochId2,
1382                 Map.of(
1383                         topic1.getTopic(), Set.of(app1, app3),
1384                         topic2.getTopic(), Set.of(app1, app3)));
1385         mTopicsDao.persistTopicContributors(epochId3, Map.of(topic2.getTopic(), Set.of(app4)));
1386 
1387         // Persist Usage table to ensure each app has called Topics API in favored epoch
1388         mTopicsDao.recordUsageHistory(epochId1, app1, sdk);
1389         mTopicsDao.recordUsageHistory(epochId1, app2, sdk);
1390         mTopicsDao.recordUsageHistory(epochId2, app1, sdk);
1391         mTopicsDao.recordUsageHistory(epochId2, app3, sdk);
1392         mTopicsDao.recordUsageHistory(epochId3, app4, sdk);
1393 
1394         // Persist ReturnedTopics table, all returned topics should be topic2 based on the setup
1395         mTopicsDao.persistReturnedAppTopicsMap(
1396                 epochId1,
1397                 Map.of(
1398                         Pair.create(app1, sdk), topic2,
1399                         Pair.create(app2, sdk), topic2));
1400         mTopicsDao.persistReturnedAppTopicsMap(
1401                 epochId2,
1402                 Map.of(
1403                         Pair.create(app1, sdk), topic2,
1404                         Pair.create(app3, sdk), topic2));
1405         mTopicsDao.persistReturnedAppTopicsMap(epochId3, Map.of(Pair.create(app4, sdk), topic2));
1406 
1407         // Real Cache Manager requires loading cache before getTopics() being called.
1408         // The results are observed at Epoch4
1409         when(mMockEpochManager.getCurrentEpochId()).thenReturn(epochId4);
1410         mTopicsWorker.loadCache();
1411 
1412         // Verify apps are able to get topic before uninstallation happens
1413         GetTopicsResult topic2GetTopicsResult =
1414                 new GetTopicsResult.Builder()
1415                         .setResultCode(STATUS_SUCCESS)
1416                         .setTaxonomyVersions(List.of(taxonomyVersion))
1417                         .setModelVersions(List.of(modelVersion))
1418                         .setTopics(List.of(topic2.getTopic()))
1419                         .build();
1420         assertThat(mTopicsWorker.getTopics(app1, sdk)).isEqualTo(topic2GetTopicsResult);
1421         assertThat(mTopicsWorker.getTopics(app2, sdk)).isEqualTo(topic2GetTopicsResult);
1422         assertThat(mTopicsWorker.getTopics(app3, sdk)).isEqualTo(topic2GetTopicsResult);
1423         assertThat(mTopicsWorker.getTopics(app4, sdk)).isEqualTo(topic2GetTopicsResult);
1424 
1425         // Uninstall app1
1426         Uri packageUri = Uri.parse("package:" + app1);
1427         mTopicsWorker.handleAppUninstallation(packageUri);
1428 
1429         // Verify Topics API results at Epoch 4
1430         GetTopicsResult emptyGetTopicsResult =
1431                 new GetTopicsResult.Builder()
1432                         .setResultCode(STATUS_SUCCESS)
1433                         .setTaxonomyVersions(Collections.emptyList())
1434                         .setModelVersions(Collections.emptyList())
1435                         .setTopics(Collections.emptyList())
1436                         .build();
1437 
1438         // app1 doesn't have any returned topics due to uninstallation
1439         assertThat(mTopicsWorker.getTopics(app1, sdk)).isEqualTo(emptyGetTopicsResult);
1440         // app2 doesn't have returned topics as it only calls Topics API at Epoch1 and its returned
1441         // topic topic2 is cleaned due to app1's uninstallation.
1442         assertThat(mTopicsWorker.getTopics(app2, sdk)).isEqualTo(emptyGetTopicsResult);
1443         // app3 has topic2 as returned topic. topic2 won't be cleaned at Epoch2 as both app1 and
1444         // app3 are contributors to topic2 in Epoch3.
1445         assertThat(mTopicsWorker.getTopics(app3, sdk)).isEqualTo(topic2GetTopicsResult);
1446         // app4 has topic2 as returned topic. topic2 is cleaned as returned topic for app4 in
1447         // Epoch1. However, app4 is still able to return topic2 as topic2 is a returned topic for
1448         // app4 at Epoch3.
1449         assertThat(mTopicsWorker.getTopics(app4, sdk)).isEqualTo(topic2GetTopicsResult);
1450 
1451         // Verify TopicContributors Map is updated: app1 should be removed after the uninstallation.
1452         // To make the result more readable, original TopicContributors Map before uninstallation is
1453         // Epoch1:  topic1 -> app1, app2, app4
1454         //          topic2 -> app1
1455         //          topic3 -> app2, app4
1456         // Epoch2:  topic1 -> app1, app3
1457         //          topic2 -> app1, app3
1458         // Epoch3:  topic2 -> app4
1459         assertThat(mTopicsDao.retrieveTopicToContributorsMap(epochId1))
1460                 .isEqualTo(
1461                         Map.of(
1462                                 topic1.getTopic(),
1463                                 Set.of(app2, app4),
1464                                 topic3.getTopic(),
1465                                 Set.of(app2, app4)));
1466         assertThat(mTopicsDao.retrieveTopicToContributorsMap(epochId2))
1467                 .isEqualTo(
1468                         Map.of(topic1.getTopic(), Set.of(app3), topic2.getTopic(), Set.of(app3)));
1469         assertThat(mTopicsDao.retrieveTopicToContributorsMap(epochId3))
1470                 .isEqualTo(Map.of(topic2.getTopic(), Set.of(app4)));
1471     }
1472 
1473     @Test
testHandleAppUninstallation_contributorDeletionsToSameTopic()1474     public void testHandleAppUninstallation_contributorDeletionsToSameTopic() {
1475         // To test the scenario a topic has two contributors, and both are deleted consecutively.
1476         // Both app1 and app2 are contributors to topic1 and return topic1. app3 is not the
1477         // contributor but also returns topic1, learnt via same SDK.
1478         final long epochId1 = 1;
1479         final long epochId2 = 2;
1480         final long taxonomyVersion = 1L;
1481         final long modelVersion = 1L;
1482         final String app1 = "app1";
1483         final String app2 = "app2";
1484         final String app3 = "app3";
1485         final String sdk = "sdk";
1486         Topic topic1 = Topic.create(/* topic */ 1, taxonomyVersion, modelVersion);
1487         Topic topic2 = Topic.create(/* topic */ 2, taxonomyVersion, modelVersion);
1488         Topic topic3 = Topic.create(/* topic */ 3, taxonomyVersion, modelVersion);
1489         Topic topic4 = Topic.create(/* topic */ 4, taxonomyVersion, modelVersion);
1490         Topic topic5 = Topic.create(/* topic */ 5, taxonomyVersion, modelVersion);
1491         Topic topic6 = Topic.create(/* topic */ 6, taxonomyVersion, modelVersion);
1492 
1493         // Set the number in flag so that it doesn't depend on the actual value in PhFlags.
1494         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(1);
1495 
1496         // Persist Top topics for epoch1 ~ epoch4
1497         mTopicsDao.persistTopTopics(
1498                 epochId1, List.of(topic1, topic2, topic3, topic4, topic5, topic6));
1499 
1500         // Persist AppClassificationTopics Table
1501         mTopicsDao.persistAppClassificationTopics(
1502                 epochId1,
1503                 Map.of(
1504                         app1, List.of(topic1),
1505                         app2, List.of(topic1)));
1506 
1507         // Compute and persist TopicContributors table
1508         mTopicsDao.persistTopicContributors(
1509                 epochId1, Map.of(topic1.getTopic(), Set.of(app1, app2)));
1510 
1511         // Persist ReturnedTopics table. App3 is able to
1512         mTopicsDao.persistReturnedAppTopicsMap(
1513                 epochId1,
1514                 Map.of(
1515                         Pair.create(app1, sdk), topic1,
1516                         Pair.create(app2, sdk), topic1,
1517                         Pair.create(app3, sdk), topic1));
1518 
1519         // Real Cache Manager requires loading cache before getTopics() being called.
1520         // The results are observed at EpochId = 2
1521         when(mMockEpochManager.getCurrentEpochId()).thenReturn(epochId2);
1522         mTopicsWorker.loadCache();
1523 
1524         // An empty getTopics() result to verify
1525         GetTopicsResult emptyGetTopicsResult =
1526                 new GetTopicsResult.Builder()
1527                         .setResultCode(STATUS_SUCCESS)
1528                         .setTaxonomyVersions(Collections.emptyList())
1529                         .setModelVersions(Collections.emptyList())
1530                         .setTopics(Collections.emptyList())
1531                         .build();
1532 
1533         // Delete app1
1534         mTopicsWorker.handleAppUninstallation(Uri.parse(app1));
1535         // app1 should be deleted from TopicContributors Map
1536         assertThat(mTopicsDao.retrieveTopicToContributorsMap(epochId1))
1537                 .isEqualTo(Map.of(topic1.getTopic(), Set.of(app2)));
1538         // app1 should have empty result
1539         assertThat(mTopicsWorker.getTopics(app1, sdk)).isEqualTo(emptyGetTopicsResult);
1540 
1541         // Delete app2
1542         mTopicsWorker.handleAppUninstallation(Uri.parse(app2));
1543         // topic1 has app2 as the only contributor, and will be removed.
1544         assertThat(mTopicsDao.retrieveTopicToContributorsMap(epochId1)).isEmpty();
1545         // app2 should have empty result
1546         assertThat(mTopicsWorker.getTopics(app2, sdk)).isEqualTo(emptyGetTopicsResult);
1547 
1548         // As topic1 is removed, app3 also has empty result
1549         assertThat(mTopicsWorker.getTopics(app3, sdk)).isEqualTo(emptyGetTopicsResult);
1550     }
1551 
1552     @Test
testHandleAppInstallation()1553     public void testHandleAppInstallation() {
1554         final String appName = "app";
1555         Uri packageUri = Uri.parse("package:" + appName);
1556         final long currentEpochId = 4L;
1557         final long taxonomyVersion = 1L;
1558         final long modelVersion = 1L;
1559         final int numOfLookBackEpochs = 3;
1560         final int topicsNumberOfTopTopics = 5;
1561         final int topicsPercentageForRandomTopic = 5;
1562 
1563         // As selectAssignedTopicFromTopTopics() randomly assigns a top topic, pass in a Mocked
1564         // Random object to make the result deterministic.
1565         //
1566         // In this test, topic 1, 2, and 6 are supposed to be returned. For each topic, it needs 2
1567         // random draws: the first is to determine whether to select a random topic, the second is
1568         // draw the actual topic index.
1569         MockRandom mockRandom =
1570                 new MockRandom(
1571                         new long[] {
1572                             topicsPercentageForRandomTopic, // Will select a regular topic
1573                             0, // Index of first topic
1574                             topicsPercentageForRandomTopic, // Will select a regular topic
1575                             1, // Index of second topic
1576                             0, // Will select a random topic
1577                             0 // Select the first random topic
1578                         });
1579         AppUpdateManager appUpdateManager =
1580                 new AppUpdateManager(mDbHelper, mTopicsDao, mockRandom, mMockFlags);
1581         // Create a local TopicsWorker in order to user above local AppUpdateManager
1582         TopicsWorker topicsWorker =
1583                 new TopicsWorker(
1584                         mMockEpochManager,
1585                         mCacheManager,
1586                         mBlockedTopicsManager,
1587                         appUpdateManager,
1588                         mMockFlags);
1589 
1590         when(mMockFlags.getTopicsNumberOfLookBackEpochs()).thenReturn(numOfLookBackEpochs);
1591         when(mMockFlags.getTopicsNumberOfTopTopics()).thenReturn(topicsNumberOfTopTopics);
1592         when(mMockFlags.getTopicsPercentageForRandomTopic())
1593                 .thenReturn(topicsPercentageForRandomTopic);
1594         when(mMockEpochManager.getCurrentEpochId()).thenReturn(currentEpochId);
1595 
1596         Topic topic1 = Topic.create(/* topic */ 1, taxonomyVersion, modelVersion);
1597         Topic topic2 = Topic.create(/* topic */ 2, taxonomyVersion, modelVersion);
1598         Topic topic3 = Topic.create(/* topic */ 3, taxonomyVersion, modelVersion);
1599         Topic topic4 = Topic.create(/* topic */ 4, taxonomyVersion, modelVersion);
1600         Topic topic5 = Topic.create(/* topic */ 5, taxonomyVersion, modelVersion);
1601         Topic topic6 = Topic.create(/* topic */ 6, taxonomyVersion, modelVersion);
1602         List<Topic> topTopics = List.of(topic1, topic2, topic3, topic4, topic5, topic6);
1603 
1604         // Persist top topics into database for last 3 epochs
1605         for (long epochId = currentEpochId - 1;
1606                 epochId >= currentEpochId - numOfLookBackEpochs;
1607                 epochId--) {
1608             mTopicsDao.persistTopTopics(epochId, topTopics);
1609             // Persist topics to TopicContributors Table avoid being filtered out
1610             for (Topic topic : topTopics) {
1611                 mTopicsDao.persistTopicContributors(
1612                         epochId, Map.of(topic.getTopic(), Set.of(appName)));
1613             }
1614         }
1615 
1616         // Verify getTopics() returns nothing before calling assignTopicsToNewlyInstalledApps()
1617         GetTopicsResult emptyGetTopicsResult =
1618                 new GetTopicsResult.Builder()
1619                         .setResultCode(STATUS_SUCCESS)
1620                         .setTaxonomyVersions(Collections.emptyList())
1621                         .setModelVersions(Collections.emptyList())
1622                         .setTopics(Collections.emptyList())
1623                         .build();
1624         assertThat(topicsWorker.getTopics(appName, /* sdk */ "")).isEqualTo(emptyGetTopicsResult);
1625 
1626         // Assign topics to past epochs
1627         topicsWorker.handleAppInstallation(packageUri);
1628 
1629         GetTopicsResult expectedGetTopicsResult =
1630                 new GetTopicsResult.Builder()
1631                         .setResultCode(STATUS_SUCCESS)
1632                         .setTaxonomyVersions(
1633                                 Arrays.asList(taxonomyVersion, taxonomyVersion, taxonomyVersion))
1634                         .setModelVersions(Arrays.asList(modelVersion, modelVersion, modelVersion))
1635                         .setTopics(Arrays.asList(1, 2, 6))
1636                         .build();
1637         GetTopicsResult getTopicsResult = topicsWorker.getTopics(appName, /* sdk */ "");
1638 
1639         // Since the returned topic list is shuffled, elements have to be verified separately
1640         assertThat(getTopicsResult.getResultCode())
1641                 .isEqualTo(expectedGetTopicsResult.getResultCode());
1642         assertThat(getTopicsResult.getTaxonomyVersions())
1643                 .containsExactlyElementsIn(expectedGetTopicsResult.getTaxonomyVersions());
1644         assertThat(getTopicsResult.getModelVersions())
1645                 .containsExactlyElementsIn(expectedGetTopicsResult.getModelVersions());
1646         assertThat(getTopicsResult.getTopics())
1647                 .containsExactlyElementsIn(expectedGetTopicsResult.getTopics());
1648 
1649         verify(mMockFlags).getTopicsNumberOfTopTopics();
1650         verify(mMockFlags).getTopicsPercentageForRandomTopic();
1651     }
1652 }
1653