Home | History | Annotate | Download | only in documentsui
      1 /*
      2  * Copyright (C) 2018 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;
     17 
     18 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_GRANTED;
     19 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES;
     20 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COLUMNS;
     21 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COL_PACKAGE;
     22 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS;
     23 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COLUMNS;
     24 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_DIRECTORY;
     25 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_GRANTED;
     26 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_PACKAGE;
     27 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_VOLUME_UUID;
     28 import static android.os.Environment.isStandardDirectory;
     29 
     30 import static com.android.documentsui.base.SharedMinimal.DEBUG;
     31 import static com.android.documentsui.base.SharedMinimal.getExternalDirectoryName;
     32 import static com.android.documentsui.base.SharedMinimal.getInternalDirectoryName;
     33 import static com.android.documentsui.base.SharedMinimal.getUriPermission;
     34 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_ASK_AGAIN;
     35 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_GRANTED;
     36 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_NEVER_ASK;
     37 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.clearScopedAccessPreferences;
     38 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.getAllPackages;
     39 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.getAllPermissions;
     40 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.setScopedAccessPermissionStatus;
     41 import static com.android.internal.util.Preconditions.checkArgument;
     42 
     43 import android.annotation.Nullable;
     44 import android.app.ActivityManager;
     45 import android.app.GrantedUriPermission;
     46 import android.content.ContentProvider;
     47 import android.content.ContentProviderClient;
     48 import android.content.ContentResolver;
     49 import android.content.ContentValues;
     50 import android.content.Context;
     51 import android.content.Intent;
     52 import android.content.UriMatcher;
     53 import android.database.Cursor;
     54 import android.database.MatrixCursor;
     55 import android.net.Uri;
     56 import android.os.Environment;
     57 import android.os.UserHandle;
     58 import android.os.storage.StorageManager;
     59 import android.os.storage.StorageVolume;
     60 import android.provider.DocumentsContract;
     61 import android.util.ArraySet;
     62 import android.util.Log;
     63 
     64 import com.android.documentsui.base.Providers;
     65 import com.android.documentsui.prefs.ScopedAccessLocalPreferences.Permission;
     66 import com.android.internal.util.ArrayUtils;
     67 
     68 import java.io.FileDescriptor;
     69 import java.io.PrintWriter;
     70 import java.util.ArrayList;
     71 import java.util.Arrays;
     72 import java.util.HashMap;
     73 import java.util.HashSet;
     74 import java.util.List;
     75 import java.util.Map;
     76 import java.util.Set;
     77 
     78 //TODO(b/72055774): update javadoc once implementation is finished
     79 /**
     80  * Provider used to manage scoped access directory permissions.
     81  *
     82  * <p>It fetches data from 2 sources:
     83  *
     84  * <ul>
     85  * <li>{@link com.android.documentsui.prefs.ScopedAccessLocalPreferences} for denied permissions.
     86  * <li>{@link ActivityManager} for allowed permissions.
     87  * </ul>
     88  *
     89  * <p>And returns the results in 2 tables:
     90  *
     91  * <ul>
     92  * <li>{@link #TABLE_PACKAGES}: read-only table with the name of all packages
     93  * (column ({@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_PACKAGE}) that
     94  * had a scoped access directory permission granted or denied.
     95  * <li>{@link #TABLE_PERMISSIONS}: writable table with the name of all packages
     96  * (column ({@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_PACKAGE}) that
     97  * had a scoped access directory
     98  * (column ({@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_DIRECTORY})
     99  * permission for a volume (column
    100  * {@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_VOLUME_UUID}, which
    101  * contains the volume UUID or {@code null} if it's the primary partition) granted or denied
    102  * (column ({@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_GRANTED}).
    103  * </ul>
    104  *
    105  * <p><b>Note:</b> the {@code query()} methods return all entries; it does not support selection or
    106  * projections.
    107  */
    108 // TODO(b/72055774): add unit tests
    109 public class ScopedAccessProvider extends ContentProvider {
    110 
    111     private static final String TAG = "ScopedAccessProvider";
    112     private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    113 
    114     private static final int URI_PACKAGES = 1;
    115     private static final int URI_PERMISSIONS = 2;
    116 
    117     public static final String AUTHORITY = "com.android.documentsui.scopedAccess";
    118 
    119     static {
    120         sMatcher.addURI(AUTHORITY, TABLE_PACKAGES + "/*", URI_PACKAGES);
    121         sMatcher.addURI(AUTHORITY, TABLE_PERMISSIONS + "/*", URI_PERMISSIONS);
    122     }
    123 
    124     @Override
    125     public boolean onCreate() {
    126         return true;
    127     }
    128 
    129     @Override
    130     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
    131             String sortOrder) {
    132         if (DEBUG) {
    133             Log.v(TAG, "query(" + uri + "): proj=" + Arrays.toString(projection)
    134                 + ", sel=" + selection);
    135         }
    136         switch (sMatcher.match(uri)) {
    137             case URI_PACKAGES:
    138                 return getPackagesCursor();
    139             case URI_PERMISSIONS:
    140                 if (ArrayUtils.isEmpty(selectionArgs)) {
    141                     throw new UnsupportedOperationException("selections cannot be empty");
    142                 }
    143                 // For simplicity, we only support one package (which is what Settings is passing).
    144                 if (selectionArgs.length > 1) {
    145                     Log.w(TAG, "Using just first entry of " + Arrays.toString(selectionArgs));
    146                 }
    147                 return getPermissionsCursor(selectionArgs[0]);
    148             default:
    149                 throw new UnsupportedOperationException("Unsupported Uri " + uri);
    150         }
    151     }
    152 
    153     private Cursor getPackagesCursor() {
    154         final Context context = getContext();
    155 
    156         // First, get the packages that were denied
    157         final Set<String> pkgs = getAllPackages(context);
    158 
    159         // Second, query AM to get all packages that have a permission.
    160         final ActivityManager am =
    161                 (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    162 
    163         final List<GrantedUriPermission> amPkgs = am.getGrantedUriPermissions(null).getList();
    164         if (!amPkgs.isEmpty()) {
    165             amPkgs.forEach((perm) -> pkgs.add(perm.packageName));
    166         }
    167 
    168         if (ArrayUtils.isEmpty(pkgs)) {
    169             if (DEBUG) Log.v(TAG, "getPackagesCursor(): nothing to do" );
    170             return null;
    171         }
    172 
    173         if (DEBUG) {
    174             Log.v(TAG, "getPackagesCursor(): denied=" + pkgs + ", granted=" + amPkgs);
    175         }
    176 
    177         // Finally, create the cursor
    178         final MatrixCursor cursor = new MatrixCursor(TABLE_PACKAGES_COLUMNS, pkgs.size());
    179         pkgs.forEach((pkg) -> cursor.addRow( new Object[] { pkg }));
    180         return cursor;
    181     }
    182 
    183     // TODO(b/72055774): need to unit tests to handle scenarios where the root permission of
    184     // a secondary volume mismatches a child permission (for example, child is allowed by root
    185     // is denied).
    186     private Cursor getPermissionsCursor(String packageName) {
    187         final Context context = getContext();
    188 
    189         // List of volumes that were granted by AM at the root level - in that case,
    190         // we can ignored individual grants from AM or denials from our preferences
    191         final Set<String> grantedVolumes = new ArraySet<>();
    192 
    193         // List of directories (mapped by volume uuid) that were granted by AM so they can be
    194         // ignored if also found on our preferences
    195         final Map<String, Set<String>> grantedDirsByUuid = new HashMap<>();
    196 
    197         // Cursor rows
    198         final List<Object[]> permissions = new ArrayList<>();
    199 
    200         // First, query AM to get all packages that have a permission.
    201         final ActivityManager am =
    202                 (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    203         final List<GrantedUriPermission> uriPermissions =
    204                 am.getGrantedUriPermissions(packageName).getList();
    205         if (DEBUG) {
    206             Log.v(TAG, "am returned =" + uriPermissions);
    207         }
    208         setGrantedPermissions(packageName, uriPermissions, permissions, grantedVolumes,
    209                 grantedDirsByUuid);
    210 
    211         // Now  gets the packages that were denied
    212         final List<Permission> rawPermissions = getAllPermissions(context);
    213 
    214         if (DEBUG) {
    215             Log.v(TAG, "rawPermissions: " + rawPermissions);
    216         }
    217 
    218         // Merge the permissions granted by AM with the denied permissions saved on our preferences.
    219         for (Permission rawPermission : rawPermissions) {
    220             if (!packageName.equals(rawPermission.pkg)) {
    221                 if (DEBUG) {
    222                     Log.v(TAG,
    223                             "ignoring " + rawPermission + " because package is not " + packageName);
    224                 }
    225                 continue;
    226             }
    227             if (rawPermission.status != PERMISSION_NEVER_ASK
    228                     && rawPermission.status != PERMISSION_ASK_AGAIN) {
    229                 // We only care for status where the user denied a request.
    230                 if (DEBUG) {
    231                     Log.v(TAG, "ignoring " + rawPermission + " because of its status");
    232                 }
    233                 continue;
    234             }
    235             if (grantedVolumes.contains(rawPermission.uuid)) {
    236                 if (DEBUG) {
    237                     Log.v(TAG, "ignoring " + rawPermission + " because whole volume is granted");
    238                 }
    239                 continue;
    240             }
    241             final Set<String> grantedDirs = grantedDirsByUuid.get(rawPermission.uuid);
    242             if (grantedDirs != null
    243                     && grantedDirs.contains(rawPermission.directory)) {
    244                 Log.w(TAG, "ignoring " + rawPermission + " because it was granted already");
    245                 continue;
    246             }
    247             permissions.add(new Object[] {
    248                     packageName, rawPermission.uuid,
    249                     getExternalDirectoryName(rawPermission.directory), 0
    250             });
    251         }
    252 
    253         if (DEBUG) {
    254             Log.v(TAG, "total permissions: " + permissions.size());
    255         }
    256 
    257         // Then create the cursor
    258         final MatrixCursor cursor = new MatrixCursor(TABLE_PERMISSIONS_COLUMNS, permissions.size());
    259         permissions.forEach((row) -> cursor.addRow(row));
    260         return cursor;
    261     }
    262 
    263     /**
    264      * Converts the permissions returned by AM and add it to 3 buckets ({@code permissions},
    265      * {@code grantedVolumes}, and {@code grantedDirsByUuid}).
    266      *
    267      * @param packageName name of package that the permissions were granted to.
    268      * @param uriPermissions permissions returend by AM
    269      * @param permissions list of permissions that can be converted to a {@link #TABLE_PERMISSIONS}
    270      * row.
    271      * @param grantedVolumes volume uuids that were granted full access.
    272      * @param grantedDirsByUuid directories that were granted individual acces (key is volume uuid,
    273      * value is list of directories).
    274      */
    275     private void setGrantedPermissions(String packageName, List<GrantedUriPermission> uriPermissions,
    276             List<Object[]> permissions, Set<String> grantedVolumes,
    277             Map<String, Set<String>> grantedDirsByUuid) {
    278         final List<Permission> grantedPermissions = parseGrantedPermissions(uriPermissions);
    279 
    280         for (Permission p : grantedPermissions) {
    281             // First check if it's for the full volume
    282             if (p.directory == null) {
    283                 if (p.uuid == null) {
    284                     // Should never happen - the Scoped Directory Access API does not allow it.
    285                     Log.w(TAG, "ignoring entry whose uuid and directory is null");
    286                     continue;
    287                 }
    288                 grantedVolumes.add(p.uuid);
    289             } else {
    290                 if (!ArrayUtils.contains(Environment.STANDARD_DIRECTORIES, p.directory)) {
    291                     if (DEBUG) Log.v(TAG, "Ignoring non-standard directory on " + p);
    292                     continue;
    293                 }
    294 
    295                 Set<String> dirs = grantedDirsByUuid.get(p.uuid);
    296                 if (dirs == null) {
    297                     // Life would be so much easier if Android had MultiMaps...
    298                     dirs = new HashSet<>(1);
    299                     grantedDirsByUuid.put(p.uuid, dirs);
    300                 }
    301                 dirs.add(p.directory);
    302             }
    303         }
    304 
    305         if (DEBUG) {
    306             Log.v(TAG, "grantedVolumes=" + grantedVolumes
    307                     + ", grantedDirectories=" + grantedDirsByUuid);
    308         }
    309         // Add granted permissions to full volumes.
    310         grantedVolumes.forEach((uuid) -> permissions.add(new Object[] {
    311                 packageName, uuid, /* dir= */ null, 1
    312         }));
    313 
    314         // Add granted permissions to individual directories
    315         grantedDirsByUuid.forEach((uuid, dirs) -> {
    316             if (grantedVolumes.contains(uuid)) {
    317                 Log.w(TAG, "Ignoring individual grants to " + uuid + ": " + dirs);
    318             } else {
    319                 dirs.forEach((dir) -> permissions.add(new Object[] {packageName, uuid, dir, 1}));
    320             }
    321         });
    322     }
    323 
    324     /**
    325      * Converts the permissions returned by AM to our own format.
    326      */
    327     private List<Permission> parseGrantedPermissions(List<GrantedUriPermission> uriPermissions) {
    328         final List<Permission> permissions = new ArrayList<>(uriPermissions.size());
    329         // TODO(b/72055774): we should query AUTHORITY_STORAGE or call DocumentsContract instead of
    330         // hardcoding the logic here.
    331         for (GrantedUriPermission uriPermission : uriPermissions) {
    332             final Uri uri = uriPermission.uri;
    333             final String authority = uri.getAuthority();
    334             if (!Providers.AUTHORITY_STORAGE.equals(authority)) {
    335                 Log.w(TAG, "Wrong authority on " + uri);
    336                 continue;
    337             }
    338             final List<String> pathSegments = uri.getPathSegments();
    339             if (pathSegments.size() < 2) {
    340                 Log.w(TAG, "wrong path segments on " + uri);
    341                 continue;
    342             }
    343             // TODO(b/72055774): make PATH_TREE private again if not used anymore
    344             if (!DocumentsContract.PATH_TREE.equals(pathSegments.get(0))) {
    345                 Log.w(TAG, "wrong path tree on " + uri);
    346                 continue;
    347             }
    348 
    349             final String[] uuidAndDir = pathSegments.get(1).split(":");
    350             // uuid and dir are either UUID:DIR (for scoped directory) or UUID: (for full volume)
    351             if (uuidAndDir.length != 1 && uuidAndDir.length != 2) {
    352                 Log.w(TAG, "could not parse uuid and directory on " + uri);
    353                 continue;
    354             }
    355             // TODO(b/72055774): to make things uglier, the Documents directory in the primary
    356             // storage is a special case as its URI is "$ROOT_ID_HOME", instead of
    357             // "${ROOT_ID_DEVICE}/Documents. This is another reason to move this logic to the
    358             // provider...
    359             final String uuid, dir;
    360             if (Providers.ROOT_ID_HOME.equals(uuidAndDir[0])) {
    361                 uuid = null;
    362                 dir = Environment.DIRECTORY_DOCUMENTS;
    363             } else {
    364                 uuid = Providers.ROOT_ID_DEVICE.equals(uuidAndDir[0])
    365                         ? null // primary
    366                         : uuidAndDir[0]; // external volume
    367                 dir = uuidAndDir.length == 1 ? null : uuidAndDir[1];
    368             }
    369             permissions
    370                     .add(new Permission(uriPermission.packageName, uuid, dir, PERMISSION_GRANTED));
    371         }
    372         return permissions;
    373     }
    374 
    375     @Override
    376     public String getType(Uri uri) {
    377         return null;
    378     }
    379 
    380     @Override
    381     public Uri insert(Uri uri, ContentValues values) {
    382         throw new UnsupportedOperationException("insert(): unsupported " + uri);
    383     }
    384 
    385     @Override
    386     public int delete(Uri uri, String selection, String[] selectionArgs) {
    387         if (sMatcher.match(uri) != URI_PERMISSIONS) {
    388             throw new UnsupportedOperationException("delete(): unsupported " + uri);
    389         }
    390 
    391         if (DEBUG) {
    392             Log.v(TAG, "delete(" + uri + "): " + Arrays.toString(selectionArgs));
    393         }
    394 
    395         // TODO(b/72055774): add unit tests for invalid input
    396         checkArgument(selectionArgs != null && selectionArgs.length == 1,
    397                 "Must have exactly 1 args: package_name" + Arrays.toString(selectionArgs));
    398         final String packageName = selectionArgs[0];
    399 
    400         // Delete just our preferences - the URI permissions is handled externally
    401         // TODO(b/72055774): move logic to revoke permissions here, so AppStorageSettings does
    402         // not need to call am.clearGrantedUriPermissions(packageName) (then we could remove that
    403         // method from ActivityManager)
    404         return clearScopedAccessPreferences(getContext(), packageName);
    405     }
    406 
    407     @Override
    408     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    409         if (sMatcher.match(uri) != URI_PERMISSIONS) {
    410             throw new UnsupportedOperationException("update(): unsupported " + uri);
    411         }
    412 
    413         if (DEBUG) {
    414             Log.v(TAG, "update(" + uri + "): " + Arrays.toString(selectionArgs) + " = " + values);
    415         }
    416 
    417         // TODO(b/72055774): add unit tests for invalid input
    418         checkArgument(selectionArgs != null && selectionArgs.length == 3,
    419                 "Must have exactly 3 args: package_name, (nullable) uuid, (nullable) directory: "
    420                         + Arrays.toString(selectionArgs));
    421         final String packageName = selectionArgs[0];
    422         final String uuid = selectionArgs[1];
    423         final String dir = selectionArgs[2];
    424         final boolean granted = values.getAsBoolean(COL_GRANTED);
    425 
    426         // First update the effective URI permission ...
    427         if (!persistUriPermission(packageName, uuid, dir, granted)) {
    428             // Failed - nothing left to do...
    429             return 0;
    430         }
    431 
    432         // ...then our preferences.
    433         setScopedAccessPermissionStatus(getContext(), packageName, uuid,
    434                 getInternalDirectoryName(dir), granted ? PERMISSION_GRANTED : PERMISSION_NEVER_ASK);
    435         return 1;
    436     }
    437 
    438     /**
    439      * Calls AM to persist a URI.
    440      *
    441      * @return whether the call succeeded.
    442      */
    443     private boolean persistUriPermission(String packageName, @Nullable String uuid,
    444             @Nullable String directory, boolean granted) {
    445         final Context context = getContext();
    446 
    447         final ContentProviderClient storageClient = context.getContentResolver()
    448                 .acquireContentProviderClient(Providers.AUTHORITY_STORAGE);
    449 
    450         final StorageManager sm = context.getSystemService(StorageManager.class);
    451 
    452         StorageVolume volume = null;
    453         if (uuid == null) {
    454             if (directory == null) {
    455                 Log.w(TAG, "cannot grant full access to the primary volume");
    456                 return false;
    457             }
    458             volume = sm.getPrimaryStorageVolume();
    459         } else {
    460             for (StorageVolume candidate : sm.getVolumeList()) {
    461                 if (uuid.equals(candidate.getUuid())) {
    462                     volume = candidate;
    463                     break;
    464                 }
    465             }
    466             if (volume == null) {
    467                 Log.w(TAG, "didn't find volume for UUID=" + uuid);
    468                 return false;
    469             }
    470             if (directory != null && !isStandardDirectory(directory)) {
    471                 Log.w(TAG, "not a scoped directory: " + directory);
    472                 return false;
    473             }
    474         }
    475 
    476         return getUriPermission(context, storageClient, volume, getInternalDirectoryName(directory),
    477                 UserHandle.getCallingUserId(), /* logMetrics= */ false,
    478                 (file, volumeLabel, isRoot, isPrimary, grantedUri, rootUri) -> {
    479                     updatePermission(context, grantedUri, packageName, granted);
    480                     return true;
    481                 });
    482     }
    483 
    484     private void updatePermission(Context context, Uri grantedUri, String toPackage,
    485             boolean granted) {
    486         final int persistFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION
    487                 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
    488         final int grantFlags = persistFlags
    489                 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
    490                 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
    491 
    492         final ContentResolver cr = context.getContentResolver();
    493         if (granted) {
    494             context.grantUriPermission(toPackage, grantedUri, grantFlags);
    495             cr.takePersistableUriPermission(toPackage, grantedUri, persistFlags);
    496         } else {
    497             context.revokeUriPermission(grantedUri, grantFlags);
    498             // There's no need to release after revoking
    499         }
    500     }
    501 
    502     @Override
    503     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
    504         final String prefix = "  ";
    505 
    506         final List<String> packages = new ArrayList<>();
    507         pw.print("Packages: ");
    508         try (Cursor cursor = getPackagesCursor()) {
    509             if (cursor == null || cursor.getCount() == 0) {
    510                 pw.println("N/A");
    511             } else {
    512                 pw.println(cursor.getCount());
    513                 while (cursor.moveToNext()) {
    514                     final String pkg = cursor.getString(TABLE_PACKAGES_COL_PACKAGE);
    515                     packages.add(pkg);
    516                     pw.print(prefix);
    517                     pw.println(pkg);
    518                 }
    519             }
    520         }
    521 
    522         pw.print("Permissions: ");
    523         for (int i = 0; i < packages.size(); i++) {
    524             final String pkg = packages.get(i);
    525             try (Cursor cursor = getPermissionsCursor(pkg)) {
    526                 if (cursor == null) {
    527                     pw.println("N/A");
    528                 } else {
    529                     pw.println(cursor.getCount());
    530                     while (cursor.moveToNext()) {
    531                         pw.print(prefix); pw.print(cursor.getString(TABLE_PERMISSIONS_COL_PACKAGE));
    532                         pw.print('/');
    533                         final String uuid = cursor.getString(TABLE_PERMISSIONS_COL_VOLUME_UUID);
    534                         if (uuid != null) {
    535                             pw.print(uuid); pw.print('>');
    536                         }
    537                         pw.print(cursor.getString(TABLE_PERMISSIONS_COL_DIRECTORY));
    538                         pw.print(": "); pw.println(cursor.getInt(TABLE_PERMISSIONS_COL_GRANTED) == 1);
    539                     }
    540                 }
    541             }
    542         }
    543 
    544         pw.print("Raw permissions: ");
    545         final List<Permission> rawPermissions = getAllPermissions(getContext());
    546         if (rawPermissions.isEmpty()) {
    547             pw.println("N/A");
    548         } else {
    549             final int size = rawPermissions.size();
    550             pw.println(size);
    551             for (int i = 0; i < size; i++) {
    552                 final Permission permission = rawPermissions.get(i);
    553                 pw.print(prefix); pw.println(permission);
    554             }
    555         }
    556     }
    557 }
    558