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