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