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