summaryrefslogtreecommitdiff
path: root/adservices/service-core/java/com/android/adservices/data/consent/AppConsentDao.java
blob: c2eb8d6e371f447e50b5d0e1dba3ef4bb3cc5267 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.adservices.data.consent;

import android.content.Context;
import android.content.pm.PackageManager;

import androidx.annotation.NonNull;

import com.android.adservices.LogUtil;
import com.android.adservices.data.common.BooleanFileDatastore;
import com.android.internal.annotations.VisibleForTesting;

import com.google.common.base.Preconditions;

import java.io.IOException;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

/**
 * Data access object for the App Consent datastore serving the Privacy Sandbox Consent Manager and
 * the FLEDGE Custom Audience and Ad Selection APIs.
 */
public class AppConsentDao {
    private static final int DATASTORE_VERSION = 1;
    private static final String DATASTORE_NAME = "adservices.appconsent.xml";

    @VisibleForTesting static final String DATASTORE_KEY_SEPARATOR = "  ";

    private static volatile AppConsentDao sAppConsentDao;
    private volatile boolean mInitialized = false;

    /**
     * The {@link BooleanFileDatastore} will store {@code true} if an app has had its consent
     * revoked and {@code false} if the app is allowed (has not had its consent revoked). Keys in
     * the datastore consist of a combination of package name and UID.
     */
    private final BooleanFileDatastore mDatastore;

    private final PackageManager mPackageManager;

    /** Constructs the {@link AppConsentDao}. */
    @VisibleForTesting
    public AppConsentDao(
            @NonNull BooleanFileDatastore datastore, @NonNull PackageManager packageManager) {
        Objects.requireNonNull(datastore);
        Objects.requireNonNull(packageManager);

        mDatastore = datastore;
        mPackageManager = packageManager;
    }

    /** @return the singleton instance of the {@link AppConsentDao} */
    public static AppConsentDao getInstance(@NonNull Context context) {
        Objects.requireNonNull(context, "Context must be provided.");

        if (sAppConsentDao == null) {
            synchronized (AppConsentDao.class) {
                if (sAppConsentDao == null) {
                    BooleanFileDatastore datastore =
                            new BooleanFileDatastore(context, DATASTORE_NAME, DATASTORE_VERSION);
                    PackageManager packageManager = context.getPackageManager();
                    sAppConsentDao = new AppConsentDao(datastore, packageManager);
                }
            }
        }

        return sAppConsentDao;
    }

    /**
     * Lazily initializes the datastore by reading from the written file.
     *
     * <p>Guarantees only one initialization call per singleton object.
     *
     * @throws IOException if datastore initialization fails
     */
    @VisibleForTesting
    void initializeDatastoreIfNeeded() throws IOException {
        if (!mInitialized) {
            synchronized (this) {
                if (!mInitialized) {
                    mDatastore.initialize();
                    mInitialized = true;
                }
            }
        }
    }

    /**
     * @return a set of all known apps in the database that have not had user consent revoked
     * @throws IOException if the operation fails
     */
    public Set<String> getKnownAppsWithConsent() throws IOException {
        initializeDatastoreIfNeeded();
        Set<String> apps = new HashSet<>();
        Set<String> datastoreKeys = mDatastore.keySetFalse();
        for (String key : datastoreKeys) {
            apps.add(datastoreKeyToPackageName(key));
        }

        return apps;
    }

    /**
     * @return a set of all known apps in the database that have had user consent revoked
     * @throws IOException if the operation fails
     */
    public Set<String> getAppsWithRevokedConsent() throws IOException {
        initializeDatastoreIfNeeded();
        Set<String> apps = new HashSet<>();
        Set<String> datastoreKeys = mDatastore.keySetTrue();
        for (String key : datastoreKeys) {
            apps.add(datastoreKeyToPackageName(key));
        }

        return apps;
    }

    /**
     * Sets consent for a given installed application, identified by package name.
     *
     * @throws IllegalArgumentException if the package name is invalid or not found as an installed
     *     application
     * @throws IOException if the operation fails
     */
    public void setConsentForApp(@NonNull String packageName, boolean isConsentRevoked)
            throws IllegalArgumentException, IOException {
        initializeDatastoreIfNeeded();
        mDatastore.put(toDatastoreKey(packageName), isConsentRevoked);
    }

    /**
     * Tries to set consent for a given installed application, identified by package name, if it
     * does not already exist in the datastore, and returns the current consent setting after
     * checking.
     *
     * @return the current consent for the given {@code packageName} after trying to set the {@code
     *     value}
     * @throws IllegalArgumentException if the package name is invalid or not found as an installed
     *     application
     * @throws IOException if the operation fails
     */
    public boolean setConsentForAppIfNew(@NonNull String packageName, boolean isConsentRevoked)
            throws IllegalArgumentException, IOException {
        initializeDatastoreIfNeeded();
        return mDatastore.putIfNew(toDatastoreKey(packageName), isConsentRevoked);
    }

    /**
     * Returns whether a given application (identified by package name) has had user consent
     * revoked.
     *
     * <p>If the given application is installed but is not found in the datastore, the application
     * is treated as having user consent, and this method returns {@code false}.
     *
     * @throws IllegalArgumentException if the package name is invalid or not found as an installed
     *     application
     * @throws IOException if the operation fails
     */
    public boolean isConsentRevokedForApp(@NonNull String packageName)
            throws IllegalArgumentException, IOException {
        initializeDatastoreIfNeeded();
        return Boolean.TRUE.equals(mDatastore.get(toDatastoreKey(packageName)));
    }

    /**
     * Clears the consent datastore of all settings.
     *
     * @throws IOException if the operation fails
     */
    public void clearAllConsentData() throws IOException {
        initializeDatastoreIfNeeded();
        mDatastore.clear();
    }

    /**
     * Clears the consent datastore of all known apps with consent. Apps with revoked consent are
     * not removed.
     *
     * @throws IOException if the operation fails
     */
    public void clearKnownAppsWithConsent() throws IOException {
        initializeDatastoreIfNeeded();
        mDatastore.clearAllFalse();
    }

    /**
     * Removes the consent setting for an application (if it exists in the datastore).
     *
     * @throws IllegalArgumentException if the package name or package UID is invalid
     * @throws IOException if the operation fails
     */
    public void clearConsentForUninstalledApp(@NonNull String packageName, int packageUid)
            throws IllegalArgumentException, IOException {
        initializeDatastoreIfNeeded();
        // Do not check whether the application has been uninstalled; in an edge case where the app
        // may have been reinstalled, data that should have been cleared might then be persisted
        mDatastore.remove(toDatastoreKey(packageName, packageUid));
    }

    /**
     * Returns the key that corresponds to the given package name and UID.
     *
     * <p>The given package name and UID are not checked for installation status.
     *
     * @throws IllegalArgumentException if the package UID is not valid
     */
    @VisibleForTesting
    @NonNull
    String toDatastoreKey(@NonNull String packageName, int packageUid)
            throws IllegalArgumentException {
        Objects.requireNonNull(packageName, "Package name must be provided");
        Preconditions.checkArgument(!packageName.isEmpty(), "Invalid package name");
        Preconditions.checkArgument(packageUid > 0, "Invalid package UID");
        return packageName.concat(DATASTORE_KEY_SEPARATOR).concat(Integer.toString(packageUid));
    }

    /**
     * Returns the key that corresponds to the given package name.
     *
     * <p>The given package name is checked for installation status.
     *
     * @throws IllegalArgumentException if the package name does not correspond to an installed
     *     application
     */
    @VisibleForTesting
    @NonNull
    String toDatastoreKey(@NonNull String packageName) throws IllegalArgumentException {
        Objects.requireNonNull(packageName);

        int packageUid;
        try {
            packageUid = getUidForInstalledPackageName(mPackageManager, packageName);
        } catch (PackageManager.NameNotFoundException exception) {
            LogUtil.e(exception, "Package name not found");
            throw new IllegalArgumentException(exception);
        }

        return toDatastoreKey(packageName, packageUid);
    }

    /**
     * Returns the package name extracted from the given datastore key.
     *
     * <p>The package name returned is not guaranteed to correspond to a currently installed
     * package.
     *
     * @throws IllegalArgumentException if the given key does not match the expected schema
     */
    @VisibleForTesting
    @NonNull
    String datastoreKeyToPackageName(@NonNull String datastoreKey) throws IllegalArgumentException {
        Objects.requireNonNull(datastoreKey);
        Preconditions.checkArgument(!datastoreKey.isEmpty(), "Empty input datastore key");
        int separatorIndex = datastoreKey.lastIndexOf(DATASTORE_KEY_SEPARATOR);
        Preconditions.checkArgument(separatorIndex > 0, "Invalid datastore key");
        return datastoreKey.substring(0, separatorIndex);
    }

    /**
     * Checks if a package name corresponds to a valid installed app for the user and returns its
     * UID if so.
     *
     * @return the UID for the installed application, if found
     * @throws PackageManager.NameNotFoundException if the package name could not be found
     */
    @VisibleForTesting
    static int getUidForInstalledPackageName(
            @NonNull PackageManager packageManager, @NonNull String packageName)
            throws PackageManager.NameNotFoundException {
        Objects.requireNonNull(packageManager);
        Objects.requireNonNull(packageName);
        return packageManager.getPackageUid(packageName, PackageManager.PackageInfoFlags.of(0L));
    }
}