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 android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.content.pm.ApplicationInfo; 23 import android.content.pm.PackageManager; 24 import android.database.sqlite.SQLiteDatabase; 25 import android.net.Uri; 26 import android.os.Build; 27 import android.util.Pair; 28 29 import androidx.annotation.RequiresApi; 30 31 import com.android.adservices.LoggerFactory; 32 import com.android.adservices.data.DbHelper; 33 import com.android.adservices.data.topics.Topic; 34 import com.android.adservices.data.topics.TopicsDao; 35 import com.android.adservices.data.topics.TopicsTables; 36 import com.android.adservices.service.Flags; 37 import com.android.adservices.service.FlagsFactory; 38 import com.android.adservices.service.common.compat.PackageManagerCompatUtils; 39 import com.android.internal.annotations.VisibleForTesting; 40 41 import java.util.ArrayList; 42 import java.util.Arrays; 43 import java.util.HashSet; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.Objects; 47 import java.util.Random; 48 import java.util.Set; 49 import java.util.stream.Collectors; 50 51 /** 52 * Class to manage application update flow in Topics API. 53 * 54 * <p>It contains methods to handle app installation and uninstallation. App update will either be 55 * regarded as the combination of app installation and uninstallation, or be handled in the next 56 * epoch. 57 * 58 * <p>See go/rb-topics-app-update for details. 59 */ 60 // TODO(b/269798827): Enable for R. 61 @RequiresApi(Build.VERSION_CODES.S) 62 public class AppUpdateManager { 63 private static final LoggerFactory.Logger sLogger = LoggerFactory.getTopicsLogger(); 64 private static final String EMPTY_SDK = ""; 65 private static AppUpdateManager sSingleton; 66 67 // Tables that needs to be wiped out for application data 68 // and its corresponding app column name. 69 // Pair<Table Name, app Column Name> 70 private static final Pair<String, String>[] TABLE_INFO_TO_ERASE_APP_DATA = 71 new Pair[] { 72 Pair.create( 73 TopicsTables.AppClassificationTopicsContract.TABLE, 74 TopicsTables.AppClassificationTopicsContract.APP), 75 Pair.create( 76 TopicsTables.CallerCanLearnTopicsContract.TABLE, 77 TopicsTables.CallerCanLearnTopicsContract.CALLER), 78 Pair.create( 79 TopicsTables.ReturnedTopicContract.TABLE, 80 TopicsTables.ReturnedTopicContract.APP), 81 Pair.create( 82 TopicsTables.UsageHistoryContract.TABLE, 83 TopicsTables.UsageHistoryContract.APP), 84 Pair.create( 85 TopicsTables.AppUsageHistoryContract.TABLE, 86 TopicsTables.AppUsageHistoryContract.APP), 87 Pair.create( 88 TopicsTables.TopicContributorsContract.TABLE, 89 TopicsTables.TopicContributorsContract.APP) 90 }; 91 92 private final DbHelper mDbHelper; 93 private final TopicsDao mTopicsDao; 94 private final Random mRandom; 95 private final Flags mFlags; 96 AppUpdateManager( @onNull DbHelper dbHelper, @NonNull TopicsDao topicsDao, @NonNull Random random, @NonNull Flags flags)97 AppUpdateManager( 98 @NonNull DbHelper dbHelper, 99 @NonNull TopicsDao topicsDao, 100 @NonNull Random random, 101 @NonNull Flags flags) { 102 mDbHelper = dbHelper; 103 mTopicsDao = topicsDao; 104 mRandom = random; 105 mFlags = flags; 106 } 107 108 /** 109 * Returns an instance of AppUpdateManager given a context 110 * 111 * @param context the context 112 * @return an instance of AppUpdateManager 113 */ 114 @NonNull getInstance(@onNull Context context)115 public static AppUpdateManager getInstance(@NonNull Context context) { 116 synchronized (AppUpdateManager.class) { 117 if (sSingleton == null) { 118 sSingleton = 119 new AppUpdateManager( 120 DbHelper.getInstance(context), 121 TopicsDao.getInstance(context), 122 new Random(), 123 FlagsFactory.getFlags()); 124 } 125 } 126 127 return sSingleton; 128 } 129 130 /** 131 * Handle application uninstallation for Topics API. 132 * 133 * <ul> 134 * <li>Delete all derived data for an uninstalled app. 135 * <li>When the feature is enabled, remove a topic if it has the uninstalled app as the only 136 * contributor in an epoch. 137 * </ul> 138 * 139 * @param packageUri The {@link Uri} got from Broadcast Intent 140 * @param currentEpochId the epoch id of current Epoch 141 */ handleAppUninstallationInRealTime(@onNull Uri packageUri, long currentEpochId)142 public void handleAppUninstallationInRealTime(@NonNull Uri packageUri, long currentEpochId) { 143 String packageName = convertUriToAppName(packageUri); 144 145 SQLiteDatabase db = mDbHelper.safeGetWritableDatabase(); 146 if (db == null) { 147 sLogger.e( 148 "Database is not available, Stop processing app uninstallation for %s!", 149 packageName); 150 return; 151 } 152 153 // This cross db and java boundaries multiple times, so we need to have a db transaction. 154 db.beginTransaction(); 155 156 try { 157 handleTopTopicsWithoutContributors(currentEpochId, packageName); 158 159 deleteAppDataFromTableByApps(List.of(packageName)); 160 161 // Mark the transaction successful. 162 db.setTransactionSuccessful(); 163 } finally { 164 db.endTransaction(); 165 sLogger.d("End of processing app uninstallation for %s", packageName); 166 } 167 } 168 169 /** 170 * Handle application installation for Topics API. 171 * 172 * <p>Assign topics to past epochs for the installed app. 173 * 174 * @param packageUri The {@link Uri} got from Broadcast Intent 175 * @param currentEpochId the epoch id of current Epoch 176 */ handleAppInstallationInRealTime(@onNull Uri packageUri, long currentEpochId)177 public void handleAppInstallationInRealTime(@NonNull Uri packageUri, long currentEpochId) { 178 String packageName = convertUriToAppName(packageUri); 179 180 SQLiteDatabase db = mDbHelper.safeGetWritableDatabase(); 181 if (db == null) { 182 sLogger.e( 183 "Database is not available, Stop processing app installation for %s", 184 packageName); 185 return; 186 } 187 188 // This cross db and java boundaries multiple times, so we need to have a db transaction. 189 db.beginTransaction(); 190 191 try { 192 assignTopicsToNewlyInstalledApps(packageName, currentEpochId); 193 194 // Mark the transaction successful. 195 db.setTransactionSuccessful(); 196 } finally { 197 db.endTransaction(); 198 sLogger.d("End of processing app installation for %s", packageName); 199 } 200 } 201 202 /** 203 * Reconcile any mismatched data for application uninstallation. 204 * 205 * <p>Uninstallation: Wipe out data in all tables for an uninstalled application with data still 206 * persisted in database. 207 * 208 * <ul> 209 * <li>Step 1: Get currently installed apps from Package Manager. 210 * <li>Step 2: Apps that have either usages or returned topics but are not installed are 211 * regarded as newly uninstalled apps. 212 * <li>Step 3: For each newly uninstalled app, wipe out its data from database. 213 * </ul> 214 * 215 * @param context the context 216 * @param currentEpochId epoch ID of current epoch 217 */ reconcileUninstalledApps(@onNull Context context, long currentEpochId)218 public void reconcileUninstalledApps(@NonNull Context context, long currentEpochId) { 219 Set<String> currentInstalledApps = getCurrentInstalledApps(context); 220 Set<String> unhandledUninstalledApps = getUnhandledUninstalledApps(currentInstalledApps); 221 if (unhandledUninstalledApps.isEmpty()) { 222 return; 223 } 224 225 sLogger.v( 226 "Detect below unhandled mismatched applications: %s", 227 unhandledUninstalledApps.toString()); 228 229 SQLiteDatabase db = mDbHelper.safeGetWritableDatabase(); 230 if (db == null) { 231 sLogger.e("Database is not available, Stop reconciling app uninstallation in Topics!"); 232 return; 233 } 234 235 // This cross db and java boundaries multiple times, so we need to have a db transaction. 236 db.beginTransaction(); 237 238 try { 239 handleUninstalledAppsInReconciliation(unhandledUninstalledApps, currentEpochId); 240 241 // Mark the transaction successful. 242 db.setTransactionSuccessful(); 243 } finally { 244 db.endTransaction(); 245 sLogger.v("App uninstallation reconciliation in Topics is finished!"); 246 } 247 } 248 249 /** 250 * Reconcile any mismatched data for application installation. 251 * 252 * <p>Installation: Assign a random top topic from last 3 epochs to app only. 253 * 254 * <ul> 255 * <li>Step 1: Get currently installed apps from Package Manager. 256 * <li>Step 2: Installed apps that don't have neither usages nor returned topics are regarded 257 * as newly installed apps. 258 * <li>Step 3: For each newly installed app, assign a random top topic from last epoch to it 259 * and persist in the database. 260 * </ul> 261 * 262 * @param context the context 263 * @param currentEpochId id of current epoch 264 */ reconcileInstalledApps(@onNull Context context, long currentEpochId)265 public void reconcileInstalledApps(@NonNull Context context, long currentEpochId) { 266 Set<String> currentInstalledApps = getCurrentInstalledApps(context); 267 Set<String> unhandledInstalledApps = getUnhandledInstalledApps(currentInstalledApps); 268 269 if (unhandledInstalledApps.isEmpty()) { 270 return; 271 } 272 273 sLogger.v( 274 "Detect below unhandled installed applications: %s", 275 unhandledInstalledApps.toString()); 276 277 SQLiteDatabase db = mDbHelper.safeGetWritableDatabase(); 278 if (db == null) { 279 sLogger.e("Database is not available, Stop reconciling app installation in Topics!"); 280 return; 281 } 282 283 // This cross db and java boundaries multiple times, so we need to have a db transaction. 284 db.beginTransaction(); 285 286 try { 287 handleInstalledAppsInReconciliation(unhandledInstalledApps, currentEpochId); 288 289 // Mark the transaction successful. 290 db.setTransactionSuccessful(); 291 } finally { 292 db.endTransaction(); 293 sLogger.v("App installation reconciliation in Topics is finished!"); 294 } 295 } 296 297 // TODO(b/256703300): Currently we handled app-sdk topic assignments in serving flow. Move the 298 // logic back to app installation after we can get all SDKs when an app is 299 // installed. 300 /** 301 * For a newly installed app, in case SDKs that this app uses are not known when the app is 302 * installed, the returned topic for an SDK can only be assigned when user calls getTopic(). 303 * 304 * <p>If an app calls Topics API via an SDK, and this app has a returned topic while SDK 305 * doesn't, assign this topic to the SDK if it can learn this topic from past observable epochs. 306 * 307 * @param app the app 308 * @param sdk the sdk. In case the app calls the Topics API directly, the sdk == empty string. 309 * @param currentEpochId the epoch id of current cycle 310 * @return A {@link Boolean} that notes whether a topic has been assigned to the sdk, so that 311 * {@link CacheManager} needs to reload the cachedTopics 312 */ assignTopicsToSdkForAppInstallation( @onNull String app, @NonNull String sdk, long currentEpochId)313 public boolean assignTopicsToSdkForAppInstallation( 314 @NonNull String app, @NonNull String sdk, long currentEpochId) { 315 // Don't do anything if app calls getTopics directly without an SDK. 316 if (sdk.isEmpty()) { 317 return false; 318 } 319 320 int numberOfLookBackEpochs = mFlags.getTopicsNumberOfLookBackEpochs(); 321 Pair<String, String> appOnlyCaller = Pair.create(app, EMPTY_SDK); 322 Pair<String, String> appSdkCaller = Pair.create(app, sdk); 323 324 // Get ReturnedTopics for past epochs in [currentEpochId - numberOfLookBackEpochs, 325 // currentEpochId - 1]. 326 // TODO(b/237436146): Create an object class for Returned Topics. 327 Map<Long, Map<Pair<String, String>, Topic>> pastReturnedTopics = 328 mTopicsDao.retrieveReturnedTopics(currentEpochId - 1, numberOfLookBackEpochs); 329 for (Map<Pair<String, String>, Topic> returnedTopics : pastReturnedTopics.values()) { 330 // If the SDK has a returned topic, this implies we have generated returned topics for 331 // SDKs already. Exit early. 332 if (returnedTopics.containsKey(appSdkCaller)) { 333 return false; 334 } 335 } 336 337 // Track whether a topic is assigned in order to know whether cache needs to be reloaded. 338 boolean isAssigned = false; 339 340 for (long epochId = currentEpochId - 1; 341 epochId >= currentEpochId - numberOfLookBackEpochs && epochId >= 0; 342 epochId--) { 343 // Validate for an app-sdk pair, whether it satisfies 344 // 1) In current epoch, app as the single caller has a returned topic 345 // 2) The sdk can learn this topic from last numberOfLookBackEpochs epochs 346 // If so, the same topic should be assigned to the sdk. 347 if (pastReturnedTopics.get(epochId) != null 348 && pastReturnedTopics.get(epochId).containsKey(appOnlyCaller)) { 349 // This is the top Topic assigned to this app-only caller. 350 Topic appReturnedTopic = pastReturnedTopics.get(epochId).get(appOnlyCaller); 351 352 // For this epochId, check whether sdk can learn this topic for past 353 // numberOfLookBackEpochs observed epochs, i.e. 354 // [epochId - numberOfLookBackEpochs + 1, epochId] 355 // pastCallerCanLearnTopicsMap = Map<Topic, Set<Caller>>. Caller = App or Sdk 356 Map<Topic, Set<String>> pastCallerCanLearnTopicsMap = 357 mTopicsDao.retrieveCallerCanLearnTopicsMap(epochId, numberOfLookBackEpochs); 358 List<Topic> pastTopTopic = mTopicsDao.retrieveTopTopics(epochId); 359 360 if (EpochManager.isTopicLearnableByCaller( 361 appReturnedTopic, 362 sdk, 363 pastCallerCanLearnTopicsMap, 364 pastTopTopic, 365 mFlags.getTopicsNumberOfTopTopics())) { 366 mTopicsDao.persistReturnedAppTopicsMap( 367 epochId, Map.of(appSdkCaller, appReturnedTopic)); 368 isAssigned = true; 369 } 370 } 371 } 372 373 return isAssigned; 374 } 375 376 /** 377 * Generating a random topic from given top topic list 378 * 379 * @param regularTopics a {@link List} of non-random topics in current epoch, excluding those 380 * which have no contributors 381 * @param randomTopics a {@link List} of random top topics 382 * @param percentageForRandomTopic the probability to select random object 383 * @return a selected {@link Topic} to be assigned to newly installed app. Return null if both 384 * lists are empty. 385 */ 386 @VisibleForTesting 387 @Nullable selectAssignedTopicFromTopTopics( @onNull List<Topic> regularTopics, @NonNull List<Topic> randomTopics, int percentageForRandomTopic)388 Topic selectAssignedTopicFromTopTopics( 389 @NonNull List<Topic> regularTopics, 390 @NonNull List<Topic> randomTopics, 391 int percentageForRandomTopic) { 392 // Return null if both lists are empty. 393 if (regularTopics.isEmpty() && randomTopics.isEmpty()) { 394 return null; 395 } 396 397 // If one of the list is empty, select from the other list. 398 if (regularTopics.isEmpty()) { 399 return randomTopics.get(mRandom.nextInt(randomTopics.size())); 400 } else if (randomTopics.isEmpty()) { 401 return regularTopics.get(mRandom.nextInt(regularTopics.size())); 402 } 403 404 // If both lists are not empty, make a draw to determine whether to pick a random topic. 405 // If random number is in [0, randomPercentage - 1], a random topic will be selected. 406 boolean shouldSelectRandomTopic = mRandom.nextInt(100) < percentageForRandomTopic; 407 408 return shouldSelectRandomTopic 409 ? randomTopics.get(mRandom.nextInt(randomTopics.size())) 410 : regularTopics.get(mRandom.nextInt(regularTopics.size())); 411 } 412 413 /** 414 * Delete application data for a specific application. 415 * 416 * <p>This method allows other usages besides daily maintenance job, such as real-time data 417 * wiping for an app uninstallation. 418 * 419 * @param apps a {@link List} of applications to wipe data for 420 */ 421 @VisibleForTesting 422 void deleteAppDataFromTableByApps(@NonNull List<String> apps) { 423 List<Pair<String, String>> tableToEraseData = 424 Arrays.stream(TABLE_INFO_TO_ERASE_APP_DATA).collect(Collectors.toList()); 425 426 mTopicsDao.deleteFromTableByColumn( 427 /* tableNamesAndColumnNamePairs */ tableToEraseData, /* valuesToDelete */ apps); 428 429 sLogger.v("Have deleted data for application " + apps); 430 } 431 432 /** 433 * Assign a top Topic for the newly installed app. This allows SDKs in the newly installed app 434 * to get the past 3 epochs' topics if they did observe the topic in the past. 435 * 436 * <p>See more details in go/rb-topics-app-update 437 * 438 * @param app the app package name of newly installed application 439 * @param currentEpochId current epoch id 440 */ 441 @VisibleForTesting 442 void assignTopicsToNewlyInstalledApps(@NonNull String app, long currentEpochId) { 443 Objects.requireNonNull(app); 444 445 final int numberOfEpochsToAssignTopics = mFlags.getTopicsNumberOfLookBackEpochs(); 446 final int numberOfTopTopics = mFlags.getTopicsNumberOfTopTopics(); 447 final int topicsPercentageForRandomTopic = mFlags.getTopicsPercentageForRandomTopic(); 448 449 Pair<String, String> appOnlyCaller = Pair.create(app, EMPTY_SDK); 450 451 // For each past epoch, assign a random topic to this newly installed app. 452 // The assigned topic should align the probability with rule to generate top topics. 453 for (long epochId = currentEpochId - 1; 454 epochId >= currentEpochId - numberOfEpochsToAssignTopics && epochId >= 0; 455 epochId--) { 456 List<Topic> topTopics = mTopicsDao.retrieveTopTopics(epochId); 457 458 if (topTopics.isEmpty()) { 459 sLogger.v( 460 "Empty top topic list in Epoch %d, do not assign topic to App %s.", 461 epochId, app); 462 continue; 463 } 464 465 // Regular Topics are placed at the beginning of top topic list. 466 List<Topic> regularTopics = topTopics.subList(0, numberOfTopTopics); 467 regularTopics = filterRegularTopicsWithoutContributors(regularTopics, epochId); 468 List<Topic> randomTopics = topTopics.subList(numberOfTopTopics, topTopics.size()); 469 470 Topic assignedTopic = 471 selectAssignedTopicFromTopTopics( 472 regularTopics, randomTopics, topicsPercentageForRandomTopic); 473 474 if (assignedTopic == null) { 475 sLogger.v( 476 "No topic is available to assign in Epoch %d, do not assign topic to App" 477 + " %s.", 478 epochId, app); 479 continue; 480 } 481 482 // Persist this topic to database as returned topic in this epoch 483 mTopicsDao.persistReturnedAppTopicsMap(epochId, Map.of(appOnlyCaller, assignedTopic)); 484 485 sLogger.v( 486 "Topic %s has been assigned to newly installed App %s in Epoch %d", 487 assignedTopic.getTopic(), app, epochId); 488 } 489 } 490 491 /** 492 * When an app is uninstalled, we need to check whether any of its classified topics has no 493 * contributors on epoch basis for past epochs to look back. Note in an epoch, an app is a 494 * contributor to a topic if the app has called Topics API in this epoch and is classified to 495 * the topic. 496 * 497 * <p>If such topic exists, remove this topic from ReturnedTopicsTable in the epoch. This method 498 * is invoked before {@code deleteAppDataFromTableByApps}, so the uninstalled app will be 499 * cleared in TopicContributors Table there. 500 * 501 * <p>NOTE: We are only interested in the epochs which will be used for getTopics(), i.e. past 502 * numberOfLookBackEpochs epochs. 503 * 504 * @param currentEpochId the id of epoch when the method gets invoked 505 * @param uninstalledApp the newly uninstalled app 506 */ 507 @VisibleForTesting handleTopTopicsWithoutContributors(long currentEpochId, @NonNull String uninstalledApp)508 void handleTopTopicsWithoutContributors(long currentEpochId, @NonNull String uninstalledApp) { 509 // This check is on epoch basis for past epochs to look back 510 for (long epochId = currentEpochId - 1; 511 epochId >= currentEpochId - mFlags.getTopicsNumberOfLookBackEpochs() 512 && epochId >= 0; 513 epochId--) { 514 Map<String, List<Topic>> appClassificationTopics = 515 mTopicsDao.retrieveAppClassificationTopics(epochId); 516 List<Topic> topTopics = mTopicsDao.retrieveTopTopics(epochId); 517 Map<Integer, Set<String>> topTopicsToContributorsMap = 518 mTopicsDao.retrieveTopicToContributorsMap(epochId); 519 520 List<Topic> classifiedTopics = 521 appClassificationTopics.getOrDefault(uninstalledApp, new ArrayList<>()); 522 // Collect all top topics to delete to make only one Db Update 523 List<String> topTopicsToDelete = 524 classifiedTopics.stream() 525 .filter( 526 classifiedTopic -> 527 topTopics.contains(classifiedTopic) 528 && topTopicsToContributorsMap.containsKey( 529 classifiedTopic.getTopic()) 530 // Filter out the topic that has ONLY 531 // the uninstalled app as a contributor 532 && topTopicsToContributorsMap 533 .get(classifiedTopic.getTopic()) 534 .size() 535 == 1 536 && topTopicsToContributorsMap 537 .get(classifiedTopic.getTopic()) 538 .contains(uninstalledApp)) 539 .map(Topic::getTopic) 540 .map(String::valueOf) 541 .collect(Collectors.toList()); 542 543 if (!topTopicsToDelete.isEmpty()) { 544 sLogger.v( 545 "Topics %s will not have contributors at epoch %d. Delete them in" 546 + " epoch %d", 547 topTopicsToDelete, epochId, epochId); 548 } 549 550 mTopicsDao.deleteEntriesFromTableByColumnWithEqualCondition( 551 List.of( 552 Pair.create( 553 TopicsTables.ReturnedTopicContract.TABLE, 554 TopicsTables.ReturnedTopicContract.TOPIC)), 555 topTopicsToDelete, 556 TopicsTables.ReturnedTopicContract.EPOCH_ID, 557 String.valueOf(epochId), 558 /* isStringEqualConditionColumnValue */ false); 559 } 560 } 561 562 /** 563 * Filter out regular topics without any contributors. Note in an epoch, an app is a contributor 564 * to a topic if the app has called Topics API in this epoch and is classified to the topic. 565 * 566 * <p>For padded Topics (Classifier randomly pads top topics if they are not enough), as we put 567 * {@link EpochManager#PADDED_TOP_TOPICS_STRING} into TopicContributors Map, padded topics 568 * actually have "contributor" PADDED_TOP_TOPICS_STRING. Therefore, they won't be filtered out. 569 * 570 * @param regularTopics non-random top topics 571 * @param epochId epochId of current epoch 572 * @return the filtered regular topics 573 */ 574 @NonNull 575 @VisibleForTesting filterRegularTopicsWithoutContributors( @onNull List<Topic> regularTopics, long epochId)576 List<Topic> filterRegularTopicsWithoutContributors( 577 @NonNull List<Topic> regularTopics, long epochId) { 578 Map<Integer, Set<String>> topicToContributorMap = 579 mTopicsDao.retrieveTopicToContributorsMap(epochId); 580 return regularTopics.stream() 581 .filter( 582 regularTopic -> 583 topicToContributorMap.containsKey(regularTopic.getTopic()) 584 && !topicToContributorMap 585 .get(regularTopic.getTopic()) 586 .isEmpty()) 587 .collect(Collectors.toList()); 588 } 589 590 // An app will be regarded as an unhandled uninstalled app if it has an entry in any epoch of 591 // either usage table or returned topics table, but the app doesn't show up in package manager. 592 // 593 // This will be used in reconciliation process. See details in go/rb-topics-app-update. 594 @NonNull 595 @VisibleForTesting getUnhandledUninstalledApps(@onNull Set<String> currentInstalledApps)596 Set<String> getUnhandledUninstalledApps(@NonNull Set<String> currentInstalledApps) { 597 Set<String> appsWithUsage = 598 mTopicsDao.retrieveDistinctAppsFromTables( 599 List.of(TopicsTables.AppUsageHistoryContract.TABLE), 600 List.of(TopicsTables.AppUsageHistoryContract.APP)); 601 Set<String> appsWithReturnedTopics = 602 mTopicsDao.retrieveDistinctAppsFromTables( 603 List.of(TopicsTables.ReturnedTopicContract.TABLE), 604 List.of(TopicsTables.ReturnedTopicContract.APP)); 605 606 // Combine sets of apps that have usage and returned topics 607 appsWithUsage.addAll(appsWithReturnedTopics); 608 609 // Exclude currently installed apps 610 appsWithUsage.removeAll(currentInstalledApps); 611 612 return appsWithUsage; 613 } 614 615 // TODO(b/234444036): Handle apps that don't have usages in last 3 epochs 616 // An app will be regarded as an unhandled installed app if it shows up in package manager, 617 // but doesn't have an entry in neither usage table or returned topic table. 618 // 619 // This will be used in reconciliation process. See details in go/rb-topics-app-update. 620 @NonNull 621 @VisibleForTesting getUnhandledInstalledApps(@onNull Set<String> currentInstalledApps)622 Set<String> getUnhandledInstalledApps(@NonNull Set<String> currentInstalledApps) { 623 // Make a copy of installed apps 624 Set<String> installedApps = new HashSet<>(currentInstalledApps); 625 626 // Get apps with usages or(and) returned topics 627 Set<String> appsWithUsageOrReturnedTopics = 628 mTopicsDao.retrieveDistinctAppsFromTables( 629 List.of( 630 TopicsTables.AppUsageHistoryContract.TABLE, 631 TopicsTables.ReturnedTopicContract.TABLE), 632 List.of( 633 TopicsTables.AppUsageHistoryContract.APP, 634 TopicsTables.ReturnedTopicContract.APP)); 635 636 // Remove apps with usage and returned topics from currently installed apps 637 installedApps.removeAll(appsWithUsageOrReturnedTopics); 638 639 return installedApps; 640 } 641 642 // Get current installed applications from package manager 643 @NonNull 644 @VisibleForTesting getCurrentInstalledApps(Context context)645 Set<String> getCurrentInstalledApps(Context context) { 646 PackageManager packageManager = context.getPackageManager(); 647 List<ApplicationInfo> appInfoList = 648 PackageManagerCompatUtils.getInstalledApplications( 649 packageManager, PackageManager.GET_META_DATA); 650 return appInfoList.stream().map(appInfo -> appInfo.packageName).collect(Collectors.toSet()); 651 } 652 653 /** 654 * Get App Package Name from a Uri. 655 * 656 * <p>Across PPAPI, package Uri is in the form of "package:com.example.adservices.sampleapp". 657 * "package" is a scheme of Uri and "com.example.adservices.sampleapp" is the app package name. 658 * Topics API persists app package name into database so this method extracts it from a Uri. 659 * 660 * @param packageUri the {@link Uri} of a package 661 * @return the app package name 662 */ 663 @VisibleForTesting 664 @NonNull convertUriToAppName(@onNull Uri packageUri)665 String convertUriToAppName(@NonNull Uri packageUri) { 666 return packageUri.getSchemeSpecificPart(); 667 } 668 669 // Handle Uninstalled applications that still have derived data in database 670 // 671 // 1) Delete all derived data for an uninstalled app. 672 // 2) Remove a topic if it has the uninstalled app as the only contributor in an epoch. In an 673 // epoch, an app is a contributor to a topic if the app has called Topics API in this epoch and 674 // is classified to the topic. handleUninstalledAppsInReconciliation( @onNull Set<String> newlyUninstalledApps, long currentEpochId)675 private void handleUninstalledAppsInReconciliation( 676 @NonNull Set<String> newlyUninstalledApps, long currentEpochId) { 677 for (String app : newlyUninstalledApps) { 678 handleTopTopicsWithoutContributors(currentEpochId, app); 679 680 deleteAppDataFromTableByApps(List.of(app)); 681 } 682 } 683 684 // Handle newly installed applications 685 // 686 // Assign topics as real-time service to the app only, if the app isn't assigned with topics. handleInstalledAppsInReconciliation( @onNull Set<String> newlyInstalledApps, long currentEpochId)687 private void handleInstalledAppsInReconciliation( 688 @NonNull Set<String> newlyInstalledApps, long currentEpochId) { 689 for (String newlyInstalledApp : newlyInstalledApps) { 690 assignTopicsToNewlyInstalledApps(newlyInstalledApp, currentEpochId); 691 } 692 } 693 } 694