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