1/*
2 * Copyright (C) 2017 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 */
16package com.android.documentsui.prefs;
17
18import static com.android.documentsui.base.SharedMinimal.DEBUG;
19import static com.android.documentsui.base.SharedMinimal.DIRECTORY_ROOT;
20import static com.android.internal.util.Preconditions.checkArgument;
21
22import android.annotation.IntDef;
23import android.annotation.Nullable;
24import android.content.Context;
25import android.content.SharedPreferences;
26import android.content.SharedPreferences.Editor;
27import android.os.UserHandle;
28import android.preference.PreferenceManager;
29import android.text.TextUtils;
30import android.util.ArraySet;
31import android.util.Log;
32
33import java.lang.annotation.Retention;
34import java.lang.annotation.RetentionPolicy;
35import java.util.ArrayList;
36import java.util.List;
37import java.util.Map.Entry;
38import java.util.Set;
39import java.util.regex.Matcher;
40import java.util.regex.Pattern;
41
42/**
43 * Methods for accessing the local preferences with regards to scoped directory access.
44 */
45//TODO(b/72055774): add unit tests
46public class ScopedAccessLocalPreferences {
47
48    private static final String TAG = "ScopedAccessLocalPreferences";
49
50    private static SharedPreferences getPrefs(Context context) {
51        return PreferenceManager.getDefaultSharedPreferences(context);
52    }
53
54    public static final int PERMISSION_ASK = 0;
55    public static final int PERMISSION_ASK_AGAIN = 1;
56    public static final int PERMISSION_NEVER_ASK = -1;
57    // NOTE: this status is not used on preferences, but on permissions granted by AM
58    public static final int PERMISSION_GRANTED = 2;
59
60    @IntDef(flag = true, value = {
61            PERMISSION_ASK,
62            PERMISSION_ASK_AGAIN,
63            PERMISSION_NEVER_ASK,
64            PERMISSION_GRANTED
65    })
66    @Retention(RetentionPolicy.SOURCE)
67    public @interface PermissionStatus {}
68
69    private static final String KEY_REGEX = "^.+\\|(.+)\\|(.*)\\|(.+)$";
70    private static final Pattern KEY_PATTERN = Pattern.compile(KEY_REGEX);
71
72    /**
73     * Methods below are used to keep track of denied user requests on scoped directory access so
74     * the dialog is not offered when user checked the 'Do not ask again' box
75     *
76     * <p>It uses a shared preferences, whose key is:
77     * <ol>
78     * <li>{@code USER_ID|PACKAGE_NAME|VOLUME_UUID|DIRECTORY} for storage volumes that have a UUID
79     * (typically physical volumes like SD cards).
80     * <li>{@code USER_ID|PACKAGE_NAME||DIRECTORY} for storage volumes that do not have a UUID
81     * (typically the emulated volume used for primary storage
82     * </ol>
83     */
84    public static @PermissionStatus int getScopedAccessPermissionStatus(Context context,
85            String packageName, @Nullable String uuid, String directory) {
86        final String key = getScopedAccessDenialsKey(packageName, uuid, directory);
87        return getPrefs(context).getInt(key, PERMISSION_ASK);
88    }
89
90    public static void setScopedAccessPermissionStatus(Context context, String packageName,
91            @Nullable String uuid, String directory, @PermissionStatus int status) {
92        checkArgument(!TextUtils.isEmpty(directory),
93                "Cannot pass empty directory - did you mean %s?", DIRECTORY_ROOT);
94        final String key = getScopedAccessDenialsKey(packageName, uuid, directory);
95        if (DEBUG) {
96            Log.d(TAG, "Setting permission of " + packageName + ":" + uuid + ":" + directory
97                    + " to " + statusAsString(status));
98        }
99
100        getPrefs(context).edit().putInt(key, status).apply();
101    }
102
103    public static int clearScopedAccessPreferences(Context context, String packageName) {
104        final String keySubstring = "|" + packageName + "|";
105        final SharedPreferences prefs = getPrefs(context);
106        Editor editor = null;
107        int removed = 0;
108        for (final String key : prefs.getAll().keySet()) {
109            if (key.contains(keySubstring)) {
110                if (editor == null) {
111                    editor = prefs.edit();
112                }
113                editor.remove(key);
114                removed ++;
115            }
116        }
117        if (editor != null) {
118            editor.apply();
119        }
120        return removed;
121    }
122
123    private static String getScopedAccessDenialsKey(String packageName, @Nullable String uuid,
124            String directory) {
125        final int userId = UserHandle.myUserId();
126        return uuid == null
127                ? userId + "|" + packageName + "||" + directory
128                : userId + "|" + packageName + "|" + uuid + "|" + directory;
129    }
130
131    /**
132     * Clears all preferences associated with a given package.
133     *
134     * <p>Typically called when a package is removed or when user asked to clear its data.
135     */
136    public static void clearPackagePreferences(Context context, String packageName) {
137        ScopedAccessLocalPreferences.clearScopedAccessPreferences(context, packageName);
138    }
139
140    /**
141     * Gets all packages that have entries in the preferences
142     */
143    public static Set<String> getAllPackages(Context context) {
144        final SharedPreferences prefs = getPrefs(context);
145
146        final ArraySet<String> pkgs = new ArraySet<>();
147        for (Entry<String, ?> pref : prefs.getAll().entrySet()) {
148            final String key = pref.getKey();
149            final String pkg = getPackage(key);
150            if (pkg == null) {
151                Log.w(TAG, "getAllPackages(): error parsing pref '" + key + "'");
152                continue;
153            }
154            pkgs.add(pkg);
155        }
156        return pkgs;
157    }
158
159    /**
160     * Gets all permissions.
161     */
162    public static List<Permission> getAllPermissions(Context context) {
163        final SharedPreferences prefs = getPrefs(context);
164        final ArrayList<Permission> permissions = new ArrayList<>();
165
166        for (Entry<String, ?> pref : prefs.getAll().entrySet()) {
167            final String key = pref.getKey();
168            final Object value = pref.getValue();
169            final Integer status;
170            try {
171                status = (Integer) value;
172            } catch (Exception e) {
173                Log.w(TAG, "error gettting value for key '" + key + "': " + value);
174                continue;
175            }
176            final Permission permission = getPermission(key, status);
177            if (permission != null) {
178                permissions.add(permission);
179            }
180        }
181
182        return permissions;
183    }
184
185    public static String statusAsString(@PermissionStatus int status) {
186        switch (status) {
187            case PERMISSION_ASK:
188                return "PERMISSION_ASK";
189            case PERMISSION_ASK_AGAIN:
190                return "PERMISSION_ASK_AGAIN";
191            case PERMISSION_NEVER_ASK:
192                return "PERMISSION_NEVER_ASK";
193            case PERMISSION_GRANTED:
194                return "PERMISSION_GRANTED";
195            default:
196                return "UNKNOWN";
197        }
198    }
199
200    @Nullable
201    private static String getPackage(String key) {
202        final Matcher matcher = KEY_PATTERN.matcher(key);
203        return matcher.matches() ? matcher.group(1) : null;
204    }
205
206    private static Permission getPermission(String key, Integer status) {
207        final Matcher matcher = KEY_PATTERN.matcher(key);
208        if (!matcher.matches()) return null;
209
210        final String pkg = matcher.group(1);
211        final String uuid = matcher.group(2);
212        final String directory = matcher.group(3);
213
214        return new Permission(pkg, uuid, directory, status);
215    }
216
217    public static final class Permission {
218        public final String pkg;
219
220        @Nullable
221        public final String uuid;
222        public final String directory;
223        public final int status;
224
225        public Permission(String pkg, String uuid, String directory, Integer status) {
226            this.pkg = pkg;
227            this.uuid = TextUtils.isEmpty(uuid) ? null : uuid;
228            this.directory = directory;
229            this.status = status.intValue();
230        }
231
232        @Override
233        public String toString() {
234            return "Permission: [pkg=" + pkg + ", uuid=" + uuid + ", dir=" + directory + ", status="
235                    + statusAsString(status) + " (" + status + ")]";
236        }
237    }
238}
239