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