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.ResultCode.RESULT_OK; 20 21 import android.adservices.topics.GetTopicsResult; 22 import android.annotation.NonNull; 23 import android.annotation.WorkerThread; 24 import android.content.Context; 25 import android.net.Uri; 26 import android.os.Build; 27 28 import androidx.annotation.RequiresApi; 29 30 import com.android.adservices.LoggerFactory; 31 import com.android.adservices.data.topics.CombinedTopic; 32 import com.android.adservices.data.topics.EncryptedTopic; 33 import com.android.adservices.data.topics.Topic; 34 import com.android.adservices.data.topics.TopicsTables; 35 import com.android.adservices.service.Flags; 36 import com.android.adservices.service.FlagsFactory; 37 import com.android.internal.annotations.GuardedBy; 38 import com.android.internal.annotations.VisibleForTesting; 39 40 import com.google.common.base.Supplier; 41 import com.google.common.base.Suppliers; 42 import com.google.common.collect.ImmutableList; 43 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.concurrent.locks.ReadWriteLock; 47 import java.util.concurrent.locks.ReentrantReadWriteLock; 48 49 import javax.annotation.concurrent.ThreadSafe; 50 51 /** 52 * Worker class to handle Topics API Implementation. 53 * 54 * <p>This class is thread safe. 55 * 56 * @hide 57 */ 58 @RequiresApi(Build.VERSION_CODES.S) 59 @ThreadSafe 60 @WorkerThread 61 public class TopicsWorker { 62 private static final LoggerFactory.Logger sLogger = LoggerFactory.getTopicsLogger(); 63 private static final Object SINGLETON_LOCK = new Object(); 64 65 // Singleton instance of the TopicsWorker. 66 @GuardedBy("SINGLETON_LOCK") 67 private static volatile TopicsWorker sTopicsWorker; 68 69 private static Supplier<TopicsWorker> sTopicsWorkerSupplier = 70 Suppliers.memoize( 71 () -> 72 new TopicsWorker( 73 EpochManager.getInstance(), 74 CacheManager.getInstance(), 75 BlockedTopicsManager.getInstance(), 76 AppUpdateManager.getInstance(), 77 FlagsFactory.getFlags())); 78 79 // Lock for concurrent Read and Write processing in TopicsWorker. 80 // Read-only API will only need to acquire Read Lock. 81 // Write API (can update data) will need to acquire Write Lock. 82 // This lock allows concurrent Read API and exclusive Write API. 83 private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock(); 84 85 private final EpochManager mEpochManager; 86 private final CacheManager mCacheManager; 87 private final BlockedTopicsManager mBlockedTopicsManager; 88 private final AppUpdateManager mAppUpdateManager; 89 private final Flags mFlags; 90 91 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED) TopicsWorker( @onNull EpochManager epochManager, @NonNull CacheManager cacheManager, @NonNull BlockedTopicsManager blockedTopicsManager, @NonNull AppUpdateManager appUpdateManager, Flags flags)92 public TopicsWorker( 93 @NonNull EpochManager epochManager, 94 @NonNull CacheManager cacheManager, 95 @NonNull BlockedTopicsManager blockedTopicsManager, 96 @NonNull AppUpdateManager appUpdateManager, 97 Flags flags) { 98 mEpochManager = epochManager; 99 mCacheManager = cacheManager; 100 mBlockedTopicsManager = blockedTopicsManager; 101 mAppUpdateManager = appUpdateManager; 102 mFlags = flags; 103 } 104 105 /** 106 * Gets an instance of TopicsWorker to be used. 107 * 108 * <p>If no instance has been initialized yet, a new one will be created. Otherwise, the 109 * existing instance will be returned. 110 */ 111 @NonNull getInstance()112 public static TopicsWorker getInstance() { 113 if (sTopicsWorker == null) { 114 synchronized (SINGLETON_LOCK) { 115 if (sTopicsWorker == null) { 116 sTopicsWorker = sTopicsWorkerSupplier.get(); 117 } 118 } 119 } 120 return sTopicsWorker; 121 } 122 123 /** Gets the singleton to be used for lazy initialization. */ 124 @NonNull getSingletonSupplier()125 public static Supplier<TopicsWorker> getSingletonSupplier() { 126 return sTopicsWorkerSupplier; 127 } 128 129 /** 130 * Returns a list of all topics that could be returned to the {@link TopicsWorker} client. 131 * 132 * @return The list of Topics. 133 */ 134 @NonNull getKnownTopicsWithConsent()135 public ImmutableList<Topic> getKnownTopicsWithConsent() { 136 sLogger.v("TopicsWorker.getKnownTopicsWithConsent"); 137 mReadWriteLock.readLock().lock(); 138 try { 139 return mCacheManager.getKnownTopicsWithConsent(mEpochManager.getCurrentEpochId()); 140 } finally { 141 mReadWriteLock.readLock().unlock(); 142 } 143 } 144 145 /** 146 * Returns a list of all topics that were blocked by the user. 147 * 148 * @return The list of Topics. 149 */ 150 @NonNull getTopicsWithRevokedConsent()151 public ImmutableList<Topic> getTopicsWithRevokedConsent() { 152 sLogger.v("TopicsWorker.getTopicsWithRevokedConsent"); 153 mReadWriteLock.readLock().lock(); 154 try { 155 return ImmutableList.copyOf(mBlockedTopicsManager.retrieveAllBlockedTopics()); 156 } finally { 157 mReadWriteLock.readLock().unlock(); 158 } 159 } 160 161 /** 162 * Revoke consent for provided {@link Topic} (block topic). This topic will not be returned by 163 * any of the {@link TopicsWorker} methods. 164 * 165 * @param topic {@link Topic} to block. 166 */ revokeConsentForTopic(@onNull Topic topic)167 public void revokeConsentForTopic(@NonNull Topic topic) { 168 sLogger.v("TopicsWorker.revokeConsentForTopic"); 169 mReadWriteLock.writeLock().lock(); 170 try { 171 mBlockedTopicsManager.blockTopic(topic); 172 } finally { 173 // TODO(b/234978199): optimize it - implement loading only blocked topics, not whole 174 // cache 175 loadCache(); 176 mReadWriteLock.writeLock().unlock(); 177 } 178 } 179 180 /** 181 * Restore consent for provided {@link Topic} (unblock the topic). This topic can be returned by 182 * any of the {@link TopicsWorker} methods. 183 * 184 * @param topic {@link Topic} to restore consent for. 185 */ restoreConsentForTopic(@onNull Topic topic)186 public void restoreConsentForTopic(@NonNull Topic topic) { 187 sLogger.v("TopicsWorker.restoreConsentForTopic"); 188 mReadWriteLock.writeLock().lock(); 189 try { 190 mBlockedTopicsManager.unblockTopic(topic); 191 } finally { 192 // TODO(b/234978199): optimize it - implement loading only blocked topics, not whole 193 // cache 194 loadCache(); 195 mReadWriteLock.writeLock().unlock(); 196 } 197 } 198 199 /** 200 * Get topics for the specified app and sdk. 201 * 202 * @param app the app 203 * @param sdk the sdk. In case the app calls the Topics API directly, the skd == empty string. 204 * @return the Topics Response. 205 */ 206 @NonNull getTopics(@onNull String app, @NonNull String sdk)207 public GetTopicsResult getTopics(@NonNull String app, @NonNull String sdk) { 208 sLogger.v("TopicsWorker.getTopics for %s, %s", app, sdk); 209 210 // We will generally handle the App and SDK topics assignment through 211 // PackageChangedReceiver. However, this is to catch the case we miss the broadcast. 212 handleSdkTopicsAssignment(app, sdk); 213 214 mReadWriteLock.readLock().lock(); 215 try { 216 List<CombinedTopic> combinedTopics = 217 mCacheManager.getTopics( 218 mFlags.getTopicsNumberOfLookBackEpochs(), 219 mEpochManager.getCurrentEpochId(), 220 app, 221 sdk); 222 223 List<Long> taxonomyVersions = new ArrayList<>(combinedTopics.size()); 224 List<Long> modelVersions = new ArrayList<>(combinedTopics.size()); 225 List<Integer> topicIds = new ArrayList<>(combinedTopics.size()); 226 List<byte[]> encryptedTopics = new ArrayList<>(combinedTopics.size()); 227 List<String> keyIdentifiers = new ArrayList<>(combinedTopics.size()); 228 List<byte[]> encapsulatedKeys = new ArrayList<>(combinedTopics.size()); 229 230 for (CombinedTopic combinedTopic : combinedTopics) { 231 if (!mFlags.getTopicsDisablePlaintextResponse()) { 232 // Set plaintext unencrypted topics only when flag is false. 233 taxonomyVersions.add(combinedTopic.getTopic().getTaxonomyVersion()); 234 modelVersions.add(combinedTopic.getTopic().getModelVersion()); 235 topicIds.add(combinedTopic.getTopic().getTopic()); 236 } 237 238 if (!combinedTopic 239 .getEncryptedTopic() 240 .equals(EncryptedTopic.getDefaultInstance())) { 241 encryptedTopics.add(combinedTopic.getEncryptedTopic().getEncryptedTopic()); 242 keyIdentifiers.add(combinedTopic.getEncryptedTopic().getKeyIdentifier()); 243 encapsulatedKeys.add(combinedTopic.getEncryptedTopic().getEncapsulatedKey()); 244 } 245 } 246 247 GetTopicsResult result = 248 new GetTopicsResult.Builder() 249 .setResultCode(RESULT_OK) 250 .setTaxonomyVersions(taxonomyVersions) 251 .setModelVersions(modelVersions) 252 .setTopics(topicIds) 253 .setEncryptedTopics(encryptedTopics) 254 .setEncryptionKeys(keyIdentifiers) 255 .setEncapsulatedKeys(encapsulatedKeys) 256 .build(); 257 sLogger.v( 258 "The result of TopicsWorker.getTopics for %s, %s is %s", 259 app, sdk, result.toString()); 260 return result; 261 } finally { 262 mReadWriteLock.readLock().unlock(); 263 } 264 } 265 266 /** 267 * Record the call from App and Sdk to usage history. This UsageHistory will be used to 268 * determine if a caller (app or sdk) has observed a topic before. 269 * 270 * @param app the app 271 * @param sdk the sdk of the app. In case the app calls the Topics API directly, the sdk == 272 * empty string. 273 */ 274 @NonNull recordUsage(@onNull String app, @NonNull String sdk)275 public void recordUsage(@NonNull String app, @NonNull String sdk) { 276 mReadWriteLock.readLock().lock(); 277 try { 278 mEpochManager.recordUsageHistory(app, sdk); 279 } finally { 280 mReadWriteLock.readLock().unlock(); 281 } 282 } 283 284 /** Load the Topics Cache from DB. */ 285 @NonNull loadCache()286 public void loadCache() { 287 // This loadCache happens when the TopicsService is created. The Cache is empty at that 288 // time. Since the load happens async, clients can call getTopics API during the cache load. 289 // Here we use Write lock to block Read during that loading time. 290 mReadWriteLock.writeLock().lock(); 291 try { 292 mCacheManager.loadCache(mEpochManager.getCurrentEpochId()); 293 } finally { 294 mReadWriteLock.writeLock().unlock(); 295 } 296 } 297 298 /** Compute Epoch algorithm. If the computation succeed, it will reload the cache. */ 299 @NonNull computeEpoch()300 public void computeEpoch() { 301 // This computeEpoch happens in the EpochJobService which happens every epoch. Since the 302 // epoch computation happens async, clients can call getTopics API during the epoch 303 // computation. Here we use Write lock to block Read during that computation time. 304 mReadWriteLock.writeLock().lock(); 305 try { 306 mEpochManager.processEpoch(); 307 308 // TODO(b/227179955): Handle error in mEpochManager.processEpoch and only reload Cache 309 // when the computation succeeded. 310 loadCache(); 311 } finally { 312 mReadWriteLock.writeLock().unlock(); 313 } 314 } 315 316 /** 317 * Delete all data generated by Topics API, except for tables in the exclusion list. 318 * 319 * @param tablesToExclude an {@link ArrayList} of tables that won't be deleted. 320 */ clearAllTopicsData(@onNull ArrayList<String> tablesToExclude)321 public void clearAllTopicsData(@NonNull ArrayList<String> tablesToExclude) { 322 // Here we use Write lock to block Read during that computation time. 323 mReadWriteLock.writeLock().lock(); 324 try { 325 // Do not clear encrypted topics table if the v9 db flag has not been ramped up. 326 if (!mFlags.getEnableDatabaseSchemaVersion9()) { 327 tablesToExclude.add(TopicsTables.ReturnedEncryptedTopicContract.TABLE); 328 } 329 mCacheManager.clearAllTopicsData(tablesToExclude); 330 331 // If clearing all Topics data, clear preserved blocked topics in system server. 332 if (!tablesToExclude.contains(TopicsTables.BlockedTopicsContract.TABLE)) { 333 mBlockedTopicsManager.clearAllBlockedTopics(); 334 } 335 336 loadCache(); 337 sLogger.v( 338 "All derived data are cleaned for Topics API except: %s", 339 tablesToExclude.toString()); 340 } finally { 341 mReadWriteLock.writeLock().unlock(); 342 } 343 } 344 345 /** 346 * Reconcile unhandled app update in real-time service. 347 * 348 * <p>Uninstallation: Wipe out data in all tables for an uninstalled application with data still 349 * persisted in database. 350 * 351 * <p>Installation: Assign a random top topic from last 3 epochs to app only. 352 * 353 * @param context the context 354 */ reconcileApplicationUpdate(Context context)355 public void reconcileApplicationUpdate(Context context) { 356 mReadWriteLock.writeLock().lock(); 357 try { 358 mAppUpdateManager.reconcileUninstalledApps(context, mEpochManager.getCurrentEpochId()); 359 mAppUpdateManager.reconcileInstalledApps(context, mEpochManager.getCurrentEpochId()); 360 361 loadCache(); 362 } finally { 363 mReadWriteLock.writeLock().unlock(); 364 sLogger.d("App Update Reconciliation is done!"); 365 } 366 } 367 368 /** 369 * Handle application uninstallation for Topics API. 370 * 371 * @param packageUri The {@link Uri} got from Broadcast Intent 372 */ handleAppUninstallation(@onNull Uri packageUri)373 public void handleAppUninstallation(@NonNull Uri packageUri) { 374 mReadWriteLock.writeLock().lock(); 375 try { 376 mAppUpdateManager.handleAppUninstallationInRealTime( 377 packageUri, mEpochManager.getCurrentEpochId()); 378 379 loadCache(); 380 sLogger.v("Derived data is cleared for %s", packageUri.toString()); 381 } finally { 382 mReadWriteLock.writeLock().unlock(); 383 } 384 } 385 386 /** 387 * Handle application installation for Topics API 388 * 389 * @param packageUri The {@link Uri} got from Broadcast Intent 390 */ handleAppInstallation(@onNull Uri packageUri)391 public void handleAppInstallation(@NonNull Uri packageUri) { 392 mReadWriteLock.writeLock().lock(); 393 try { 394 mAppUpdateManager.handleAppInstallationInRealTime( 395 packageUri, mEpochManager.getCurrentEpochId()); 396 397 loadCache(); 398 sLogger.v( 399 "Topics have been assigned to newly installed %s and cache" + "is reloaded", 400 packageUri); 401 } finally { 402 mReadWriteLock.writeLock().unlock(); 403 } 404 } 405 406 // Handle topic assignment to SDK for newly installed applications. Cached topics need to be 407 // reloaded if any topic assignment happens. handleSdkTopicsAssignment(@onNull String app, @NonNull String sdk)408 private void handleSdkTopicsAssignment(@NonNull String app, @NonNull String sdk) { 409 // Return if any topic has been assigned to this app-sdk. 410 List<Topic> existingTopics = getExistingTopicsForAppSdk(app, sdk); 411 if (!existingTopics.isEmpty()) { 412 return; 413 } 414 415 mReadWriteLock.writeLock().lock(); 416 try { 417 if (mAppUpdateManager.assignTopicsToSdkForAppInstallation( 418 app, sdk, mEpochManager.getCurrentEpochId())) { 419 loadCache(); 420 sLogger.v( 421 "Topics have been assigned to sdk %s as app %s is newly installed in" 422 + " current epoch", 423 sdk, app); 424 } 425 } finally { 426 mReadWriteLock.writeLock().unlock(); 427 } 428 } 429 430 // Get all existing topics from cache for a pair of app and sdk. 431 // The epoch range is [currentEpochId - numberOfLookBackEpochs, currentEpochId]. 432 @NonNull getExistingTopicsForAppSdk(@onNull String app, @NonNull String sdk)433 private List<Topic> getExistingTopicsForAppSdk(@NonNull String app, @NonNull String sdk) { 434 List<Topic> existingTopics; 435 436 mReadWriteLock.readLock().lock(); 437 // Get existing returned topics map for last 3 epochs and current epoch. 438 try { 439 long currentEpochId = mEpochManager.getCurrentEpochId(); 440 existingTopics = 441 mCacheManager.getTopicsInEpochRange( 442 currentEpochId - mFlags.getTopicsNumberOfLookBackEpochs(), 443 currentEpochId, 444 app, 445 sdk); 446 } finally { 447 mReadWriteLock.readLock().unlock(); 448 } 449 450 return existingTopics == null ? new ArrayList<>() : existingTopics; 451 } 452 } 453