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