• 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.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