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