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