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