/*
* Copyright (C) 2018 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;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_GRANTED;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COLUMNS;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COL_PACKAGE;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COLUMNS;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_DIRECTORY;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_GRANTED;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_PACKAGE;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_VOLUME_UUID;
import static android.os.Environment.isStandardDirectory;
import static com.android.documentsui.base.SharedMinimal.DEBUG;
import static com.android.documentsui.base.SharedMinimal.getExternalDirectoryName;
import static com.android.documentsui.base.SharedMinimal.getInternalDirectoryName;
import static com.android.documentsui.base.SharedMinimal.getUriPermission;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_ASK_AGAIN;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_GRANTED;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_NEVER_ASK;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.clearScopedAccessPreferences;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.getAllPackages;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.getAllPermissions;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.setScopedAccessPermissionStatus;
import static com.android.internal.util.Preconditions.checkArgument;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.GrantedUriPermission;
import android.content.ContentProvider;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Environment;
import android.os.UserHandle;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.provider.DocumentsContract;
import android.util.ArraySet;
import android.util.Log;
import com.android.documentsui.base.Providers;
import com.android.documentsui.prefs.ScopedAccessLocalPreferences.Permission;
import com.android.internal.util.ArrayUtils;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
//TODO(b/72055774): update javadoc once implementation is finished
/**
* Provider used to manage scoped access directory permissions.
*
*
It fetches data from 2 sources:
*
*
*
{@link com.android.documentsui.prefs.ScopedAccessLocalPreferences} for denied permissions.
*
{@link ActivityManager} for allowed permissions.
*
*
*
And returns the results in 2 tables:
*
*
*
{@link #TABLE_PACKAGES}: read-only table with the name of all packages
* (column ({@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_PACKAGE}) that
* had a scoped access directory permission granted or denied.
*
{@link #TABLE_PERMISSIONS}: writable table with the name of all packages
* (column ({@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_PACKAGE}) that
* had a scoped access directory
* (column ({@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_DIRECTORY})
* permission for a volume (column
* {@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_VOLUME_UUID}, which
* contains the volume UUID or {@code null} if it's the primary partition) granted or denied
* (column ({@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_GRANTED}).
*
*
*
Note: the {@code query()} methods return all entries; it does not support selection or
* projections.
*/
// TODO(b/72055774): add unit tests
public class ScopedAccessProvider extends ContentProvider {
private static final String TAG = "ScopedAccessProvider";
private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final int URI_PACKAGES = 1;
private static final int URI_PERMISSIONS = 2;
public static final String AUTHORITY = "com.android.documentsui.scopedAccess";
static {
sMatcher.addURI(AUTHORITY, TABLE_PACKAGES + "/*", URI_PACKAGES);
sMatcher.addURI(AUTHORITY, TABLE_PERMISSIONS + "/*", URI_PERMISSIONS);
}
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
if (DEBUG) {
Log.v(TAG, "query(" + uri + "): proj=" + Arrays.toString(projection)
+ ", sel=" + selection);
}
switch (sMatcher.match(uri)) {
case URI_PACKAGES:
return getPackagesCursor();
case URI_PERMISSIONS:
if (ArrayUtils.isEmpty(selectionArgs)) {
throw new UnsupportedOperationException("selections cannot be empty");
}
// For simplicity, we only support one package (which is what Settings is passing).
if (selectionArgs.length > 1) {
Log.w(TAG, "Using just first entry of " + Arrays.toString(selectionArgs));
}
return getPermissionsCursor(selectionArgs[0]);
default:
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
private Cursor getPackagesCursor() {
final Context context = getContext();
// First, get the packages that were denied
final Set pkgs = getAllPackages(context);
// Second, query AM to get all packages that have a permission.
final ActivityManager am =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
final List amPkgs = am.getGrantedUriPermissions(null).getList();
if (!amPkgs.isEmpty()) {
amPkgs.forEach((perm) -> pkgs.add(perm.packageName));
}
if (ArrayUtils.isEmpty(pkgs)) {
if (DEBUG) Log.v(TAG, "getPackagesCursor(): nothing to do" );
return null;
}
if (DEBUG) {
Log.v(TAG, "getPackagesCursor(): denied=" + pkgs + ", granted=" + amPkgs);
}
// Finally, create the cursor
final MatrixCursor cursor = new MatrixCursor(TABLE_PACKAGES_COLUMNS, pkgs.size());
pkgs.forEach((pkg) -> cursor.addRow( new Object[] { pkg }));
return cursor;
}
// TODO(b/72055774): need to unit tests to handle scenarios where the root permission of
// a secondary volume mismatches a child permission (for example, child is allowed by root
// is denied).
private Cursor getPermissionsCursor(String packageName) {
final Context context = getContext();
// List of volumes that were granted by AM at the root level - in that case,
// we can ignored individual grants from AM or denials from our preferences
final Set grantedVolumes = new ArraySet<>();
// List of directories (mapped by volume uuid) that were granted by AM so they can be
// ignored if also found on our preferences
final Map> grantedDirsByUuid = new HashMap<>();
// Cursor rows
final List