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