Home | History | Annotate | Download | only in prefs
      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 
     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