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