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