• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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