• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  */
16 package com.android.documentsui.prefs;
17 
18 import static com.android.documentsui.base.SharedMinimal.DEBUG;
19 import static com.android.documentsui.base.SharedMinimal.DIRECTORY_ROOT;
20 import static com.android.internal.util.Preconditions.checkArgument;
21 
22 import android.annotation.IntDef;
23 import android.annotation.Nullable;
24 import android.content.Context;
25 import android.content.SharedPreferences;
26 import android.content.SharedPreferences.Editor;
27 import android.os.UserHandle;
28 import android.preference.PreferenceManager;
29 import android.text.TextUtils;
30 import android.util.ArraySet;
31 import android.util.Log;
32 
33 import java.lang.annotation.Retention;
34 import java.lang.annotation.RetentionPolicy;
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.Map.Entry;
38 import java.util.Set;
39 import java.util.regex.Matcher;
40 import 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
46 public class ScopedAccessLocalPreferences {
47 
48     private static final String TAG = "ScopedAccessLocalPreferences";
49 
getPrefs(Context context)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      */
getScopedAccessPermissionStatus(Context context, String packageName, @Nullable String uuid, String directory)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 
setScopedAccessPermissionStatus(Context context, String packageName, @Nullable String uuid, String directory, @PermissionStatus int status)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 
clearScopedAccessPreferences(Context context, String packageName)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 
getScopedAccessDenialsKey(String packageName, @Nullable String uuid, String directory)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      */
clearPackagePreferences(Context context, String packageName)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      */
getAllPackages(Context context)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      */
getAllPermissions(Context context)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 
statusAsString(@ermissionStatus int status)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
getPackage(String key)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 
getPermission(String key, Integer status)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 
Permission(String pkg, String uuid, String directory, Integer status)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
toString()233         public String toString() {
234             return "Permission: [pkg=" + pkg + ", uuid=" + uuid + ", dir=" + directory + ", status="
235                     + statusAsString(status) + " (" + status + ")]";
236         }
237     }
238 }
239