• 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.data.consent;
18 
19 import android.content.Context;
20 import android.content.pm.PackageManager;
21 
22 import com.android.adservices.LogUtil;
23 import com.android.adservices.data.common.LegacyAtomicFileDatastoreFactory;
24 import com.android.adservices.service.common.compat.FileCompatUtils;
25 import com.android.adservices.service.common.compat.PackageManagerCompatUtils;
26 import com.android.adservices.shared.common.ApplicationContextSingleton;
27 import com.android.adservices.shared.storage.AtomicFileDatastore;
28 import com.android.internal.annotations.GuardedBy;
29 import com.android.internal.annotations.VisibleForTesting;
30 
31 import com.google.common.base.Preconditions;
32 import com.google.common.base.Supplier;
33 import com.google.common.base.Suppliers;
34 
35 import java.io.IOException;
36 import java.util.HashSet;
37 import java.util.Objects;
38 import java.util.Set;
39 import java.util.stream.Collectors;
40 
41 /**
42  * Data access object for the App Consent datastore serving the Privacy Sandbox Consent Manager and
43  * the FLEDGE Custom Audience and Ad Selection APIs.
44  *
45  * <p>This class is not thread safe. It's methods are not synchronized.
46  */
47 public final class AppConsentDao {
48     @VisibleForTesting public static final int DATASTORE_VERSION = 1;
49 
50     @VisibleForTesting
51     public static final String DATASTORE_NAME =
52             FileCompatUtils.getAdservicesFilename("adservices.appconsent.xml");
53 
54     private static final Object SINGLETON_LOCK = new Object();
55 
56     @VisibleForTesting static final String DATASTORE_KEY_SEPARATOR = "  ";
57 
58     @GuardedBy("SINGLETON_LOCK")
59     private static volatile AppConsentDao sAppConsentDao;
60 
61     private volatile boolean mInitialized;
62     // Using supplier here to lazy initialize the AppConsentDao Singleton.
63     private static volatile Supplier<AppConsentDao> sAppConsentDaoSupplier =
64             Suppliers.memoize(
65                     () -> {
66                         Context context = ApplicationContextSingleton.get();
67                         @SuppressWarnings("deprecation")
68                         AtomicFileDatastore datastore =
69                                 LegacyAtomicFileDatastoreFactory.createAtomicFileDatastore(
70                                         context, DATASTORE_NAME, DATASTORE_VERSION);
71                         PackageManager packageManager = context.getPackageManager();
72                         return new AppConsentDao(datastore, packageManager);
73                     });
74 
75     /**
76      * The {@link AtomicFileDatastore} will store {@code true} if an app has had its consent revoked
77      * and {@code false} if the app is allowed (has not had its consent revoked). Keys in the
78      * datastore consist of a combination of package name and UID.
79      */
80     private final AtomicFileDatastore mDatastore;
81 
82     private final PackageManager mPackageManager;
83 
84     /** Constructs the {@link AppConsentDao}. */
85     @VisibleForTesting
AppConsentDao(AtomicFileDatastore datastore, PackageManager packageManager)86     public AppConsentDao(AtomicFileDatastore datastore, PackageManager packageManager) {
87         mDatastore = Objects.requireNonNull(datastore, "datastore cannot be null");
88         mPackageManager = Objects.requireNonNull(packageManager, "packageManager cannot be null");
89     }
90 
91     /** Gets the singleton instance of the {@link AppConsentDao} */
getInstance()92     public static AppConsentDao getInstance() {
93         if (sAppConsentDao == null) {
94             synchronized (SINGLETON_LOCK) {
95                 if (sAppConsentDao == null) {
96                     sAppConsentDao = sAppConsentDaoSupplier.get();
97                 }
98             }
99         }
100         return sAppConsentDao;
101     }
102 
103     /**
104      * @return the singleton supplier of the {@link AppConsentDao}
105      */
getSingletonSupplier()106     public static Supplier<AppConsentDao> getSingletonSupplier() {
107         return sAppConsentDaoSupplier;
108     }
109 
110     /**
111      * Lazily initializes the datastore by reading from the written file.
112      *
113      * <p>Guarantees only one initialization call per singleton object.
114      *
115      * @throws IOException if datastore initialization fails
116      */
117     @VisibleForTesting
initializeDatastoreIfNeeded()118     void initializeDatastoreIfNeeded() throws IOException {
119         if (!mInitialized) {
120             synchronized (SINGLETON_LOCK) {
121                 if (!mInitialized) {
122                     mDatastore.initialize();
123                     mInitialized = true;
124                 }
125             }
126         }
127     }
128 
129     /**
130      * @return a set of all known apps in the database that have not had user consent revoked
131      * @throws IOException if the operation fails
132      */
getKnownAppsWithConsent()133     public Set<String> getKnownAppsWithConsent() throws IOException {
134         initializeDatastoreIfNeeded();
135         Set<String> apps = new HashSet<>();
136         Set<String> datastoreKeys = mDatastore.keySetFalse();
137         Set<String> installedPackages = getInstalledPackages();
138         for (String key : datastoreKeys) {
139             String packageName = datastoreKeyToPackageName(key);
140             if (installedPackages.contains(packageName)) {
141                 apps.add(packageName);
142             }
143         }
144 
145         return apps;
146     }
147 
148     /**
149      * @return a set of all known apps in the database that have had user consent revoked
150      * @throws IOException if the operation fails
151      */
getAppsWithRevokedConsent()152     public Set<String> getAppsWithRevokedConsent() throws IOException {
153         initializeDatastoreIfNeeded();
154         Set<String> apps = new HashSet<>();
155         Set<String> datastoreKeys = mDatastore.keySetTrue();
156         Set<String> installedPackages = getInstalledPackages();
157         for (String key : datastoreKeys) {
158             String packageName = datastoreKeyToPackageName(key);
159             if (installedPackages.contains(packageName)) {
160                 apps.add(packageName);
161             }
162         }
163 
164         return apps;
165     }
166 
167     /**
168      * Sets consent for a given installed application, identified by package name.
169      *
170      * @throws IllegalArgumentException if the package name is invalid or not found as an installed
171      *     application
172      * @throws IOException if the operation fails
173      */
setConsentForApp(String packageName, boolean isConsentRevoked)174     public void setConsentForApp(String packageName, boolean isConsentRevoked)
175             throws IllegalArgumentException, IOException {
176         initializeDatastoreIfNeeded();
177         mDatastore.putBoolean(toDatastoreKey(packageName), isConsentRevoked);
178     }
179 
180     /**
181      * Tries to set consent for a given installed application, identified by package name, if it
182      * does not already exist in the datastore, and returns the current consent setting after
183      * checking.
184      *
185      * @return the current consent for the given {@code packageName} after trying to set the {@code
186      *     value}
187      * @throws IllegalArgumentException if the package name is invalid or not found as an installed
188      *     application
189      * @throws IOException if the operation fails
190      */
setConsentForAppIfNew(String packageName, boolean isConsentRevoked)191     public boolean setConsentForAppIfNew(String packageName, boolean isConsentRevoked)
192             throws IllegalArgumentException, IOException {
193         initializeDatastoreIfNeeded();
194         return mDatastore.putBooleanIfNew(toDatastoreKey(packageName), isConsentRevoked);
195     }
196 
197     /**
198      * Returns whether a given application (identified by package name) has had user consent
199      * revoked.
200      *
201      * <p>If the given application is installed but is not found in the datastore, the application
202      * is treated as having user consent, and this method returns {@code false}.
203      *
204      * @throws IllegalArgumentException if the package name is invalid or not found as an installed
205      *     application
206      * @throws IOException if the operation fails
207      */
isConsentRevokedForApp(String packageName)208     public boolean isConsentRevokedForApp(String packageName)
209             throws IllegalArgumentException, IOException {
210         initializeDatastoreIfNeeded();
211         return Boolean.TRUE.equals(mDatastore.getBoolean(toDatastoreKey(packageName)));
212     }
213 
214     /**
215      * Clears the consent datastore of all settings.
216      *
217      * @throws IOException if the operation fails
218      */
clearAllConsentData()219     public void clearAllConsentData() throws IOException {
220         initializeDatastoreIfNeeded();
221         mDatastore.clear();
222     }
223 
224     /**
225      * Clears the consent datastore of all known apps with consent. Apps with revoked consent are
226      * not removed.
227      *
228      * @throws IOException if the operation fails
229      */
clearKnownAppsWithConsent()230     public void clearKnownAppsWithConsent() throws IOException {
231         initializeDatastoreIfNeeded();
232         mDatastore.clearAllFalse();
233     }
234 
235     /**
236      * Removes the consent setting for an application (if it exists in the datastore).
237      *
238      * @throws IllegalArgumentException if the package name or package UID is invalid
239      * @throws IOException if the operation fails
240      */
clearConsentForUninstalledApp(String packageName, int packageUid)241     public void clearConsentForUninstalledApp(String packageName, int packageUid)
242             throws IllegalArgumentException, IOException {
243         initializeDatastoreIfNeeded();
244         // Do not check whether the application has been uninstalled; in an edge case where the app
245         // may have been reinstalled, data that should have been cleared might then be persisted
246         mDatastore.remove(toDatastoreKey(packageName, packageUid));
247     }
248 
249     /**
250      * Removes the consent setting for an application (if it exists in the datastore). <strong>All
251      * entries matching this package name will be removed.</strong>
252      *
253      * <p>This method is meant for backwards-compatibility to Android R & S, and should only be
254      * invoked on Android versions prior to T, where the package UID is not available when the
255      * package is uninstalled.
256      *
257      * @throws IllegalArgumentException if the package name is invalid
258      * @throws IOException if the operation fails
259      */
clearConsentForUninstalledApp(String packageName)260     public void clearConsentForUninstalledApp(String packageName) throws IOException {
261         Objects.requireNonNull(packageName, "Package name must be provided");
262         Preconditions.checkArgument(!packageName.isEmpty(), "Invalid package name");
263 
264         initializeDatastoreIfNeeded();
265 
266         // It's not possible to use the toDatastoreKey method to look up the key because the
267         // package has been uninstalled. Instead, ask the datastore to clear data for all entries
268         // beginning with the package name + separator, since the datastore stores keys in the
269         // form of package name + separator + package uid.
270         mDatastore.removeByPrefix(packageName + DATASTORE_KEY_SEPARATOR);
271     }
272 
273     /**
274      * Returns the key that corresponds to the given package name and UID.
275      *
276      * <p>The given package name and UID are not checked for installation status.
277      *
278      * @throws IllegalArgumentException if the package UID is not valid
279      */
280     @VisibleForTesting
toDatastoreKey(String packageName, int packageUid)281     String toDatastoreKey(String packageName, int packageUid) throws IllegalArgumentException {
282         Objects.requireNonNull(packageName, "Package name must be provided");
283         Preconditions.checkArgument(!packageName.isEmpty(), "Invalid package name");
284         Preconditions.checkArgument(packageUid > 0, "Invalid package UID");
285         return packageName.concat(DATASTORE_KEY_SEPARATOR).concat(Integer.toString(packageUid));
286     }
287 
288     /**
289      * Returns the key that corresponds to the given package name.
290      *
291      * <p>The given package name is checked for installation status.
292      *
293      * @throws IllegalArgumentException if the package name does not correspond to an installed
294      *     application
295      */
296     @VisibleForTesting
toDatastoreKey(String packageName)297     String toDatastoreKey(String packageName) throws IllegalArgumentException {
298         Objects.requireNonNull(packageName);
299 
300         int packageUid = getUidForInstalledPackageName(packageName);
301 
302         return toDatastoreKey(packageName, packageUid);
303     }
304 
305     /**
306      * Returns the package name extracted from the given datastore key.
307      *
308      * <p>The package name returned is not guaranteed to correspond to a currently installed
309      * package.
310      *
311      * @throws IllegalArgumentException if the given key does not match the expected schema
312      */
313     @VisibleForTesting
datastoreKeyToPackageName(String datastoreKey)314     String datastoreKeyToPackageName(String datastoreKey) throws IllegalArgumentException {
315         Objects.requireNonNull(datastoreKey);
316         Preconditions.checkArgument(!datastoreKey.isEmpty(), "Empty input datastore key");
317         int separatorIndex = datastoreKey.lastIndexOf(DATASTORE_KEY_SEPARATOR);
318         Preconditions.checkArgument(separatorIndex > 0, "Invalid datastore key");
319         return datastoreKey.substring(0, separatorIndex);
320     }
321 
322     /**
323      * Checks if a package name corresponds to a valid installed app for the user and returns its
324      * UID if so.
325      *
326      * @return the UID for the installed application, if found
327      */
getUidForInstalledPackageName(String packageName)328     public int getUidForInstalledPackageName(String packageName) {
329         Objects.requireNonNull(packageName);
330 
331         try {
332             return PackageManagerCompatUtils.getPackageUid(mPackageManager, packageName, 0);
333         } catch (PackageManager.NameNotFoundException exception) {
334             LogUtil.e(exception, "Package name not found");
335             throw new IllegalArgumentException(exception);
336         }
337     }
338 
339     /** Returns the list of packages installed on the device of the user. */
getInstalledPackages()340     public Set<String> getInstalledPackages() {
341         return PackageManagerCompatUtils.getInstalledApplications(mPackageManager, 0).stream()
342                 .map(applicationInfo -> applicationInfo.packageName)
343                 .collect(Collectors.toSet());
344     }
345 }
346