• 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 android.annotation.NonNull;
20 import android.content.Context;
21 import android.os.Build;
22 import android.util.Pair;
23 
24 import androidx.annotation.RequiresApi;
25 
26 import com.android.adservices.LoggerFactory;
27 import com.android.adservices.data.topics.Topic;
28 import com.android.adservices.data.topics.TopicsDao;
29 import com.android.adservices.service.Flags;
30 import com.android.adservices.service.FlagsFactory;
31 import com.android.adservices.service.stats.AdServicesLogger;
32 import com.android.adservices.service.stats.AdServicesLoggerImpl;
33 import com.android.adservices.service.stats.GetTopicsReportedStats;
34 import com.android.internal.annotations.GuardedBy;
35 import com.android.internal.annotations.VisibleForTesting;
36 
37 import com.google.common.collect.ImmutableList;
38 
39 import java.io.PrintWriter;
40 import java.util.ArrayList;
41 import java.util.Collections;
42 import java.util.HashMap;
43 import java.util.HashSet;
44 import java.util.List;
45 import java.util.Map;
46 import java.util.Random;
47 import java.util.Set;
48 import java.util.concurrent.locks.ReadWriteLock;
49 import java.util.concurrent.locks.ReentrantReadWriteLock;
50 import java.util.stream.Collectors;
51 
52 import javax.annotation.concurrent.ThreadSafe;
53 
54 /**
55  * A class to manage Topics Cache.
56  *
57  * <p>This class is thread safe.
58  */
59 // TODO(b/269798827): Enable for R.
60 @RequiresApi(Build.VERSION_CODES.S)
61 @ThreadSafe
62 public class CacheManager {
63     private static final LoggerFactory.Logger sLogger = LoggerFactory.getTopicsLogger();
64     // The verbose level for dumpsys usage
65     private static final int VERBOSE = 1;
66     private static final Object SINGLETON_LOCK = new Object();
67 
68     @GuardedBy("SINGLETON_LOCK")
69     private static CacheManager sSingleton;
70     // Lock for Read and Write on the cached topics map.
71     // This allows concurrent reads but exclusive update to the cache.
72     private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock();
73     private final TopicsDao mTopicsDao;
74     private final BlockedTopicsManager mBlockedTopicsManager;
75     private final Flags mFlags;
76     // Map<EpochId, Map<Pair<App, Sdk>, Topic>
77     private Map<Long, Map<Pair<String, String>, Topic>> mCachedTopics = new HashMap<>();
78     // TODO(b/236422354): merge hashsets to have one point of truth (Taxonomy update)
79     // HashSet<BlockedTopic>
80     private HashSet<Topic> mCachedBlockedTopics = new HashSet<>();
81     // HashSet<TopicId>
82     private HashSet<Integer> mCachedBlockedTopicIds = new HashSet<>();
83 
84     // Set containing Global Blocked Topic Ids
85     private HashSet<Integer> mCachedGlobalBlockedTopicIds;
86     private final AdServicesLogger mLogger;
87 
88     @VisibleForTesting
CacheManager( TopicsDao topicsDao, Flags flags, AdServicesLogger logger, BlockedTopicsManager blockedTopicsManager, GlobalBlockedTopicsManager globalBlockedTopicsManager)89     CacheManager(
90             TopicsDao topicsDao,
91             Flags flags,
92             AdServicesLogger logger,
93             BlockedTopicsManager blockedTopicsManager,
94             GlobalBlockedTopicsManager globalBlockedTopicsManager) {
95         mTopicsDao = topicsDao;
96         mFlags = flags;
97         mLogger = logger;
98         mBlockedTopicsManager = blockedTopicsManager;
99         mCachedGlobalBlockedTopicIds = globalBlockedTopicsManager.getGlobalBlockedTopicIds();
100     }
101 
102     /** Returns an instance of the CacheManager given a context. */
103     @NonNull
getInstance(Context context)104     public static CacheManager getInstance(Context context) {
105         synchronized (SINGLETON_LOCK) {
106             if (sSingleton == null) {
107                 sSingleton =
108                         new CacheManager(
109                                 TopicsDao.getInstance(context),
110                                 FlagsFactory.getFlags(),
111                                 AdServicesLoggerImpl.getInstance(),
112                                 BlockedTopicsManager.getInstance(context),
113                                 GlobalBlockedTopicsManager.getInstance());
114             }
115             return sSingleton;
116         }
117     }
118 
119     /**
120      * Load the cache from DB.
121      *
122      * <p>When first created, the Cache is empty. We will need to retrieve the cache from DB.
123      *
124      * @param currentEpochId current Epoch ID
125      */
loadCache(long currentEpochId)126     public void loadCache(long currentEpochId) {
127         // Retrieve the cache from DB.
128         int lookbackEpochs = mFlags.getTopicsNumberOfLookBackEpochs();
129         // Map<EpochId, Map<Pair<App, Sdk>, Topic>
130         Map<Long, Map<Pair<String, String>, Topic>> cacheFromDb =
131                 mTopicsDao.retrieveReturnedTopics(currentEpochId, lookbackEpochs + 1);
132         // HashSet<BlockedTopic>
133         HashSet<Topic> blockedTopicsCacheFromDb =
134                 new HashSet<>(mBlockedTopicsManager.retrieveAllBlockedTopics());
135         HashSet<Integer> blockedTopicIdsFromDb =
136                 blockedTopicsCacheFromDb.stream()
137                         .map(Topic::getTopic)
138                         .collect(Collectors.toCollection(HashSet::new));
139 
140         sLogger.v(
141                 "CacheManager.loadCache(). CachedTopics mapping size is "
142                         + cacheFromDb.size()
143                         + ", CachedBlockedTopics mapping size is "
144                         + blockedTopicsCacheFromDb.size());
145         mReadWriteLock.writeLock().lock();
146         try {
147             mCachedTopics = cacheFromDb;
148             mCachedBlockedTopics = blockedTopicsCacheFromDb;
149             mCachedBlockedTopicIds = blockedTopicIdsFromDb;
150         } finally {
151             mReadWriteLock.writeLock().unlock();
152         }
153     }
154 
155     /**
156      * Get list of topics for the numberOfLookBackEpochs epoch starting from [epochId -
157      * numberOfLookBackEpochs + 1, epochId] that were not blocked by the user.
158      *
159      * @param numberOfLookBackEpochs how many epochs to look back.
160      * @param currentEpochId current Epoch ID
161      * @param app the app
162      * @param sdk the sdk. In case the app calls the Topics API directly, the sdk == empty string.
163      * @param random a {@link Random} instance for shuffling
164      * @return {@link List<Topic>} a list of Topics
165      */
166     @NonNull
getTopics( int numberOfLookBackEpochs, long currentEpochId, String app, String sdk, Random random)167     public List<Topic> getTopics(
168             int numberOfLookBackEpochs,
169             long currentEpochId,
170             String app,
171             String sdk,
172             Random random) {
173         // We will need to look at the 3 historical epochs starting from last epoch.
174         long epochId = currentEpochId - 1;
175         List<Topic> topics = new ArrayList<>();
176         // To deduplicate returned topics
177         Set<Integer> topicsSet = new HashSet<>();
178 
179         int duplicateTopicCount = 0, blockedTopicCount = 0;
180         mReadWriteLock.readLock().lock();
181         try {
182             for (int numEpoch = 0; numEpoch < numberOfLookBackEpochs; numEpoch++) {
183                 if (mCachedTopics.containsKey(epochId - numEpoch)) {
184                     Topic topic = mCachedTopics.get(epochId - numEpoch).get(Pair.create(app, sdk));
185                     if (topic != null) {
186                         if (topicsSet.contains(topic.getTopic())) {
187                             duplicateTopicCount++;
188                             continue;
189                         }
190                         if (isTopicIdBlocked(topic.getTopic())) {
191                             blockedTopicCount++;
192                             continue;
193                         }
194                         topics.add(topic);
195                         topicsSet.add(topic.getTopic());
196                     }
197                 }
198             }
199         } finally {
200             mReadWriteLock.readLock().unlock();
201         }
202 
203         Collections.shuffle(topics, random);
204 
205         // Log GetTopics stats.
206         ImmutableList.Builder<Integer> topicIds = ImmutableList.builder();
207         for (Topic topic : topics) {
208             topicIds.add(topic.getTopic());
209         }
210         mLogger.logGetTopicsReportedStats(
211                 GetTopicsReportedStats.builder()
212                         .setDuplicateTopicCount(duplicateTopicCount)
213                         .setFilteredBlockedTopicCount(blockedTopicCount)
214                         .setTopicIdsCount(topics.size())
215                         .build());
216 
217         return topics;
218     }
219 
220     /**
221      * Overloading getTopics() method to pass in an initialized Random object.
222      *
223      * @param numberOfLookBackEpochs how many epochs to look back.
224      * @param currentEpochId current Epoch ID
225      * @param app the app
226      * @param sdk the sdk. In case the app calls the Topics API directly, the sdk == empty string.
227      * @return {@link List<Topic>} a list of Topics
228      */
229     @NonNull
getTopics( int numberOfLookBackEpochs, long currentEpochId, String app, String sdk)230     public List<Topic> getTopics(
231             int numberOfLookBackEpochs, long currentEpochId, String app, String sdk) {
232         return getTopics(numberOfLookBackEpochs, currentEpochId, app, sdk, new Random());
233     }
234 
235     /**
236      * Get cached topics within certain epoch range. This is a helper method to get cached topics
237      * for an app-sdk caller, without considering other constraints, like UI blocking logic.
238      *
239      * @param epochLowerBound the earliest epoch to include cached topics from
240      * @param epochUpperBound the latest epoch to included cached topics to
241      * @param app the app
242      * @param sdk the sdk. In case the app calls the Topics API directly, the sdk == empty string.
243      * @return {@link List<Topic>} a list of Topics between {@code epochLowerBound} and {@code
244      *     epochUpperBound}.
245      */
246     @NonNull
getTopicsInEpochRange( long epochLowerBound, long epochUpperBound, @NonNull String app, @NonNull String sdk)247     public List<Topic> getTopicsInEpochRange(
248             long epochLowerBound, long epochUpperBound, @NonNull String app, @NonNull String sdk) {
249         List<Topic> topics = new ArrayList<>();
250         // To deduplicate returned topics
251         Set<Integer> topicsSet = new HashSet<>();
252 
253         mReadWriteLock.readLock().lock();
254         try {
255             for (long epochId = epochLowerBound; epochId <= epochUpperBound; epochId++) {
256                 if (mCachedTopics.containsKey(epochId)) {
257                     Topic topic = mCachedTopics.get(epochId).get(Pair.create(app, sdk));
258                     if (topic != null && !topicsSet.contains(topic.getTopic())) {
259                         topics.add(topic);
260                         topicsSet.add(topic.getTopic());
261                     }
262                 }
263             }
264         } finally {
265             mReadWriteLock.readLock().unlock();
266         }
267 
268         return topics;
269     }
270 
271     /**
272      * Gets a list of all topics that could be returned to the user in the last
273      * numberOfLookBackEpochs epochs. Does not include the current epoch, so range is
274      * [currentEpochId - numberOfLookBackEpochs, currentEpochId - 1].
275      *
276      * @param currentEpochId current Epoch ID
277      * @return The list of Topics.
278      */
279     @NonNull
getKnownTopicsWithConsent(long currentEpochId)280     public ImmutableList<Topic> getKnownTopicsWithConsent(long currentEpochId) {
281         // We will need to look at the 3 historical epochs starting from last epoch.
282         long epochId = currentEpochId - 1;
283         HashSet<Topic> topics = new HashSet<>();
284         mReadWriteLock.readLock().lock();
285         try {
286             for (int numEpoch = 0;
287                     numEpoch < mFlags.getTopicsNumberOfLookBackEpochs();
288                     numEpoch++) {
289                 if (mCachedTopics.containsKey(epochId - numEpoch)) {
290                     topics.addAll(
291                             mCachedTopics.get(epochId - numEpoch).values().stream()
292                                     .filter(topic -> !isTopicIdBlocked(topic.getTopic()))
293                                     .collect(Collectors.toList()));
294                 }
295             }
296         } finally {
297             mReadWriteLock.readLock().unlock();
298         }
299         return ImmutableList.copyOf(topics);
300     }
301 
302     /** Returns true if topic id is a global blocked topic or user blocked topic. */
isTopicIdBlocked(int topicId)303     private boolean isTopicIdBlocked(int topicId) {
304         return mCachedBlockedTopicIds.contains(topicId)
305                 || mCachedGlobalBlockedTopicIds.contains(topicId);
306     }
307 
308     /**
309      * Gets a list of all cached topics that were blocked by the user.
310      *
311      * @return The list of Topics.
312      */
313     @NonNull
getTopicsWithRevokedConsent()314     public ImmutableList<Topic> getTopicsWithRevokedConsent() {
315         mReadWriteLock.readLock().lock();
316         try {
317             return ImmutableList.copyOf(mCachedBlockedTopics);
318         } finally {
319             mReadWriteLock.readLock().unlock();
320         }
321     }
322 
323     /**
324      * Delete all data generated by Topics API, except for tables in the exclusion list.
325      *
326      * @param tablesToExclude a {@link List} of tables that won't be deleted.
327      */
clearAllTopicsData(@onNull List<String> tablesToExclude)328     public void clearAllTopicsData(@NonNull List<String> tablesToExclude) {
329         mReadWriteLock.writeLock().lock();
330         try {
331             mTopicsDao.deleteAllTopicsTables(tablesToExclude);
332         } finally {
333             mReadWriteLock.writeLock().unlock();
334         }
335     }
336 
dump(@onNull PrintWriter writer, String[] args)337     public void dump(@NonNull PrintWriter writer, String[] args) {
338         boolean isVerbose =
339                 args != null
340                         && args.length >= 1
341                         && Integer.parseInt(args[0].toLowerCase()) == VERBOSE;
342         writer.println("==== CacheManager Dump ====");
343         writer.println(String.format("mCachedTopics size: %d", mCachedTopics.size()));
344         writer.println(String.format("mCachedBlockedTopics size: %d", mCachedBlockedTopics.size()));
345         if (isVerbose) {
346             for (Long epochId : mCachedTopics.keySet()) {
347                 writer.println(String.format("Epoch Id: %d \n", epochId));
348                 Map<Pair<String, String>, Topic> epochMapping = mCachedTopics.get(epochId);
349                 for (Pair<String, String> pair : epochMapping.keySet()) {
350                     String app = pair.first;
351                     String sdk = pair.second;
352                     Topic topic = epochMapping.get(pair);
353                     writer.println(String.format("(%s, %s): %s", app, sdk, topic.toString()));
354                 }
355             }
356         }
357     }
358 }
359