/* * Copyright (C) 2017 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.documentsui.prefs; import static com.android.documentsui.base.SharedMinimal.DEBUG; import static com.android.documentsui.base.SharedMinimal.DIRECTORY_ROOT; import static com.android.internal.util.Preconditions.checkArgument; import android.annotation.IntDef; import android.annotation.Nullable; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.os.UserHandle; import android.preference.PreferenceManager; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Map.Entry; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Methods for accessing the local preferences with regards to scoped directory access. */ //TODO(b/72055774): add unit tests public class ScopedAccessLocalPreferences { private static final String TAG = "ScopedAccessLocalPreferences"; private static SharedPreferences getPrefs(Context context) { return PreferenceManager.getDefaultSharedPreferences(context); } public static final int PERMISSION_ASK = 0; public static final int PERMISSION_ASK_AGAIN = 1; public static final int PERMISSION_NEVER_ASK = -1; // NOTE: this status is not used on preferences, but on permissions granted by AM public static final int PERMISSION_GRANTED = 2; @IntDef(flag = true, value = { PERMISSION_ASK, PERMISSION_ASK_AGAIN, PERMISSION_NEVER_ASK, PERMISSION_GRANTED }) @Retention(RetentionPolicy.SOURCE) public @interface PermissionStatus {} private static final String KEY_REGEX = "^.+\\|(.+)\\|(.*)\\|(.+)$"; private static final Pattern KEY_PATTERN = Pattern.compile(KEY_REGEX); /** * Methods below are used to keep track of denied user requests on scoped directory access so * the dialog is not offered when user checked the 'Do not ask again' box * *

It uses a shared preferences, whose key is: *

    *
  1. {@code USER_ID|PACKAGE_NAME|VOLUME_UUID|DIRECTORY} for storage volumes that have a UUID * (typically physical volumes like SD cards). *
  2. {@code USER_ID|PACKAGE_NAME||DIRECTORY} for storage volumes that do not have a UUID * (typically the emulated volume used for primary storage *
*/ public static @PermissionStatus int getScopedAccessPermissionStatus(Context context, String packageName, @Nullable String uuid, String directory) { final String key = getScopedAccessDenialsKey(packageName, uuid, directory); return getPrefs(context).getInt(key, PERMISSION_ASK); } public static void setScopedAccessPermissionStatus(Context context, String packageName, @Nullable String uuid, String directory, @PermissionStatus int status) { checkArgument(!TextUtils.isEmpty(directory), "Cannot pass empty directory - did you mean %s?", DIRECTORY_ROOT); final String key = getScopedAccessDenialsKey(packageName, uuid, directory); if (DEBUG) { Log.d(TAG, "Setting permission of " + packageName + ":" + uuid + ":" + directory + " to " + statusAsString(status)); } getPrefs(context).edit().putInt(key, status).apply(); } public static int clearScopedAccessPreferences(Context context, String packageName) { final String keySubstring = "|" + packageName + "|"; final SharedPreferences prefs = getPrefs(context); Editor editor = null; int removed = 0; for (final String key : prefs.getAll().keySet()) { if (key.contains(keySubstring)) { if (editor == null) { editor = prefs.edit(); } editor.remove(key); removed ++; } } if (editor != null) { editor.apply(); } return removed; } private static String getScopedAccessDenialsKey(String packageName, @Nullable String uuid, String directory) { final int userId = UserHandle.myUserId(); return uuid == null ? userId + "|" + packageName + "||" + directory : userId + "|" + packageName + "|" + uuid + "|" + directory; } /** * Clears all preferences associated with a given package. * *

Typically called when a package is removed or when user asked to clear its data. */ public static void clearPackagePreferences(Context context, String packageName) { ScopedAccessLocalPreferences.clearScopedAccessPreferences(context, packageName); } /** * Gets all packages that have entries in the preferences */ public static Set getAllPackages(Context context) { final SharedPreferences prefs = getPrefs(context); final ArraySet pkgs = new ArraySet<>(); for (Entry pref : prefs.getAll().entrySet()) { final String key = pref.getKey(); final String pkg = getPackage(key); if (pkg == null) { Log.w(TAG, "getAllPackages(): error parsing pref '" + key + "'"); continue; } pkgs.add(pkg); } return pkgs; } /** * Gets all permissions. */ public static List getAllPermissions(Context context) { final SharedPreferences prefs = getPrefs(context); final ArrayList permissions = new ArrayList<>(); for (Entry pref : prefs.getAll().entrySet()) { final String key = pref.getKey(); final Object value = pref.getValue(); final Integer status; try { status = (Integer) value; } catch (Exception e) { Log.w(TAG, "error gettting value for key '" + key + "': " + value); continue; } final Permission permission = getPermission(key, status); if (permission != null) { permissions.add(permission); } } return permissions; } public static String statusAsString(@PermissionStatus int status) { switch (status) { case PERMISSION_ASK: return "PERMISSION_ASK"; case PERMISSION_ASK_AGAIN: return "PERMISSION_ASK_AGAIN"; case PERMISSION_NEVER_ASK: return "PERMISSION_NEVER_ASK"; case PERMISSION_GRANTED: return "PERMISSION_GRANTED"; default: return "UNKNOWN"; } } @Nullable private static String getPackage(String key) { final Matcher matcher = KEY_PATTERN.matcher(key); return matcher.matches() ? matcher.group(1) : null; } private static Permission getPermission(String key, Integer status) { final Matcher matcher = KEY_PATTERN.matcher(key); if (!matcher.matches()) return null; final String pkg = matcher.group(1); final String uuid = matcher.group(2); final String directory = matcher.group(3); return new Permission(pkg, uuid, directory, status); } public static final class Permission { public final String pkg; @Nullable public final String uuid; public final String directory; public final int status; public Permission(String pkg, String uuid, String directory, Integer status) { this.pkg = pkg; this.uuid = TextUtils.isEmpty(uuid) ? null : uuid; this.directory = directory; this.status = status.intValue(); } @Override public String toString() { return "Permission: [pkg=" + pkg + ", uuid=" + uuid + ", dir=" + directory + ", status=" + statusAsString(status) + " (" + status + ")]"; } } }