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.server.adservices.consent;
18 
19 import android.annotation.NonNull;
20 
21 import com.android.adservices.shared.storage.BooleanFileDatastore;
22 import com.android.internal.annotations.VisibleForTesting;
23 import com.android.internal.util.Preconditions;
24 
25 import java.io.IOException;
26 import java.io.PrintWriter;
27 import java.util.ArrayList;
28 import java.util.HashSet;
29 import java.util.List;
30 import java.util.Objects;
31 import java.util.Set;
32 
33 /**
34  * Manager to handle user's consent for a certain App. We will have one AppConsentManager instance
35  * per user.
36  *
37  * @hide
38  */
39 public class AppConsentManager {
40     @VisibleForTesting public static final int DATASTORE_VERSION = 1;
41     @VisibleForTesting public static final String DATASTORE_NAME = "adservices.appconsent.xml";
42 
43     @VisibleForTesting static final String DATASTORE_KEY_SEPARATOR = "  ";
44 
45     @VisibleForTesting static final String VERSION_KEY = "android.app.adservices.consent.VERSION";
46 
47     @VisibleForTesting
48     static final String BASE_DIR_MUST_BE_PROVIDED_ERROR_MESSAGE = "Base dir must be provided.";
49 
50     @VisibleForTesting static final String DUMP_PREFIX = "  ";
51 
52     /**
53      * The {@link BooleanFileDatastore} will store {@code true} if an app has had its consent
54      * revoked and {@code false} if the app is allowed (has not had its consent revoked). Keys in
55      * the datastore consist of a combination of package name and UID.
56      */
57     private final BooleanFileDatastore mDatastore;
58 
59     /** Constructs the {@link AppConsentManager}. */
60     @VisibleForTesting
AppConsentManager(@onNull BooleanFileDatastore datastore)61     public AppConsentManager(@NonNull BooleanFileDatastore datastore) {
62         Objects.requireNonNull(datastore);
63 
64         mDatastore = datastore;
65     }
66 
67     /** @return the singleton instance of the {@link AppConsentManager} */
createAppConsentManager(String baseDir, int userIdentifier)68     public static AppConsentManager createAppConsentManager(String baseDir, int userIdentifier)
69             throws IOException {
70         Objects.requireNonNull(baseDir, BASE_DIR_MUST_BE_PROVIDED_ERROR_MESSAGE);
71 
72         // The Data store is in folder with the following format.
73         // /data/system/adservices/user_id/consent/data_schema_version/
74         // Create the consent directory if needed.
75         String consentDataStoreDir =
76                 ConsentDatastoreLocationHelper.getConsentDataStoreDirAndCreateDir(
77                         baseDir, userIdentifier);
78 
79         BooleanFileDatastore datastore =
80                 new BooleanFileDatastore(
81                         consentDataStoreDir, DATASTORE_NAME, DATASTORE_VERSION, VERSION_KEY);
82         datastore.initialize();
83 
84         return new AppConsentManager(datastore);
85     }
86 
87     /** @return a set of all known apps in the database that have not had user consent revoked */
88     @NonNull
getKnownAppsWithConsent(@onNull List<String> installedPackages)89     public List<String> getKnownAppsWithConsent(@NonNull List<String> installedPackages) {
90         Objects.requireNonNull(installedPackages);
91 
92         Set<String> apps = new HashSet<>();
93         Set<String> appWithConsentInDatastore = mDatastore.keySetFalse();
94         for (String appDatastoreKey : appWithConsentInDatastore) {
95             String packageName = datastoreKeyToPackageName(appDatastoreKey);
96             if (installedPackages.contains(packageName)) {
97                 apps.add(packageName);
98             }
99         }
100 
101         return new ArrayList<>(apps);
102     }
103 
104     /**
105      * @return a set of all known apps in the database that have had user consent revoked
106      * @throws IOException if the operation fails
107      */
108     @NonNull
getAppsWithRevokedConsent(@onNull List<String> installedPackages)109     public List<String> getAppsWithRevokedConsent(@NonNull List<String> installedPackages)
110             throws IOException {
111         Objects.requireNonNull(installedPackages);
112 
113         Set<String> apps = new HashSet<>();
114         Set<String> appWithoutConsentInDatastore = mDatastore.keySetTrue();
115         for (String appDatastoreKey : appWithoutConsentInDatastore) {
116             String packageName = datastoreKeyToPackageName(appDatastoreKey);
117             if (installedPackages.contains(packageName)) {
118                 apps.add(packageName);
119             }
120         }
121 
122         return new ArrayList<>(apps);
123     }
124 
125     /**
126      * Sets consent for a given installed application, identified by package name.
127      *
128      * @throws IllegalArgumentException if the package name is invalid or not found as an installed
129      *     application
130      * @throws IOException if the operation fails
131      */
setConsentForApp( @onNull String packageName, int packageUid, boolean isConsentRevoked)132     public void setConsentForApp(
133             @NonNull String packageName, int packageUid, boolean isConsentRevoked)
134             throws IllegalArgumentException, IOException {
135         mDatastore.put(toDatastoreKey(packageName, packageUid), isConsentRevoked);
136     }
137 
138     /**
139      * Tries to set consent for a given installed application, identified by package name, if it
140      * does not already exist in the datastore, and returns the current consent setting after
141      * checking.
142      *
143      * @return the current consent for the given {@code packageName} after trying to set the {@code
144      *     value}
145      * @throws IllegalArgumentException if the package name is invalid or not found as an installed
146      *     application
147      * @throws IOException if the operation fails
148      */
setConsentForAppIfNew( @onNull String packageName, int packageUid, boolean isConsentRevoked)149     public boolean setConsentForAppIfNew(
150             @NonNull String packageName, int packageUid, boolean isConsentRevoked)
151             throws IllegalArgumentException, IOException {
152         return mDatastore.putIfNew(toDatastoreKey(packageName, packageUid), isConsentRevoked);
153     }
154 
155     /**
156      * Returns whether a given application (identified by package name) has had user consent
157      * revoked.
158      *
159      * <p>If the given application is installed but is not found in the datastore, the application
160      * is treated as having user consent, and this method returns {@code false}.
161      *
162      * @throws IllegalArgumentException if the package name is invalid or not found as an installed
163      *     application
164      * @throws IOException if the operation fails
165      */
isConsentRevokedForApp(@onNull String packageName, int packageUid)166     public boolean isConsentRevokedForApp(@NonNull String packageName, int packageUid)
167             throws IllegalArgumentException, IOException {
168         return Boolean.TRUE.equals(mDatastore.get(toDatastoreKey(packageName, packageUid)));
169     }
170 
171     /**
172      * Clears the consent datastore of all settings.
173      *
174      * @throws IOException if the operation fails
175      */
clearAllAppConsentData()176     public void clearAllAppConsentData() throws IOException {
177         mDatastore.clear();
178     }
179 
180     /**
181      * Clears the consent datastore of all known apps with consent. Apps with revoked consent are
182      * not removed.
183      *
184      * @throws IOException if the operation fails
185      */
clearKnownAppsWithConsent()186     public void clearKnownAppsWithConsent() throws IOException {
187         mDatastore.clearAllFalse();
188     }
189 
190     /**
191      * Removes the consent setting for an application (if it exists in the datastore).
192      *
193      * @throws IllegalArgumentException if the package name or package UID is invalid
194      * @throws IOException if the operation fails
195      */
clearConsentForUninstalledApp(@onNull String packageName, int packageUid)196     public void clearConsentForUninstalledApp(@NonNull String packageName, int packageUid)
197             throws IllegalArgumentException, IOException {
198         // Do not check whether the application has been uninstalled; in an edge case where the app
199         // may have been reinstalled, data that should have been cleared might then be persisted
200         mDatastore.remove(toDatastoreKey(packageName, packageUid));
201     }
202 
203     /**
204      * Returns the key that corresponds to the given package name and UID.
205      *
206      * <p>The given package name and UID are not checked for installation status.
207      *
208      * @throws IllegalArgumentException if the package UID is not valid
209      */
210     @VisibleForTesting
211     @NonNull
toDatastoreKey(@onNull String packageName, int packageUid)212     String toDatastoreKey(@NonNull String packageName, int packageUid)
213             throws IllegalArgumentException {
214         Objects.requireNonNull(packageName, "Package name must be provided");
215         Preconditions.checkArgument(!packageName.isEmpty(), "Invalid package name");
216         Preconditions.checkArgument(packageUid > 0, "Invalid package UID");
217         return packageName.concat(DATASTORE_KEY_SEPARATOR).concat(Integer.toString(packageUid));
218     }
219 
220     /**
221      * Returns the package name extracted from the given datastore key.
222      *
223      * <p>The package name returned is not guaranteed to correspond to a currently installed
224      * package.
225      *
226      * @throws IllegalArgumentException if the given key does not match the expected schema
227      */
228     @VisibleForTesting
229     @NonNull
datastoreKeyToPackageName(@onNull String datastoreKey)230     String datastoreKeyToPackageName(@NonNull String datastoreKey) throws IllegalArgumentException {
231         Objects.requireNonNull(datastoreKey);
232         Preconditions.checkArgument(!datastoreKey.isEmpty(), "Empty input datastore key");
233         int separatorIndex = datastoreKey.lastIndexOf(DATASTORE_KEY_SEPARATOR);
234         Preconditions.checkArgument(separatorIndex > 0, "Invalid datastore key");
235         return datastoreKey.substring(0, separatorIndex);
236     }
237 
238     /** Dumps its internal state. */
dump(PrintWriter writer, String prefix)239     public void dump(PrintWriter writer, String prefix) {
240         writer.printf("%sAppConsentManager:\n", prefix);
241         String prefix2 = prefix + DUMP_PREFIX;
242         String prefix3 = prefix2 + DUMP_PREFIX;
243 
244         writer.printf("%sDatastore:\n", prefix2);
245         mDatastore.dump(writer, prefix3);
246     }
247 
248     /** tearDown method used for Testing only. */
249     @VisibleForTesting
tearDownForTesting()250     public void tearDownForTesting() {
251         synchronized (this) {
252             mDatastore.tearDownForTesting();
253         }
254     }
255 }
256