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