/* * 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.base; import static android.os.Environment.isStandardDirectory; import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_ERROR; import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY; import static com.android.documentsui.ScopedAccessMetrics.logInvalidScopedAccessRequest; import android.annotation.Nullable; import android.content.ContentProviderClient; import android.content.Context; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.RemoteException; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; import android.os.storage.VolumeInfo; import android.provider.DocumentsContract; import android.text.TextUtils; import android.util.Log; import java.io.File; import java.io.IOException; import java.util.List; /** * Contains the minimum number of utilities (contants, helpers, etc...) that can be used by both the * main package and the minimal APK that's used by Android TV (and other devices). * *
In other words, it should not include any external dependency that would increase the APK
* size.
*/
public final class SharedMinimal {
public static final String TAG = "Documents";
public static final boolean DEBUG = Build.IS_DEBUGGABLE;
public static final boolean VERBOSE = DEBUG && Log.isLoggable(TAG, Log.VERBOSE);
/**
* Special directory name representing the full volume of a scoped directory request.
*/
public static final String DIRECTORY_ROOT = "ROOT_DIRECTORY";
/**
* Callback for {@link SharedMinimal#getUriPermission(Context, ContentProviderClient,
* StorageVolume, String, int, boolean, GetUriPermissionCallback)}.
*/
public static interface GetUriPermissionCallback {
/**
* Evaluates the result of the request.
*
* @param file the path of the requested URI.
* @param volumeLabel user-friendly label of the volume.
* @param isRoot whether the requested directory is the root directory.
* @param isPrimary whether the requested volume is the primary storage volume.
* @param requestedUri the requested URI.
* @param rootUri the URI for the volume's root directory.
* @return whethe the result was sucessfully.
*/
boolean onResult(File file, String volumeLabel, boolean isRoot, boolean isPrimary,
Uri requestedUri, Uri rootUri);
}
/**
* Gets the name of a directory name in the format that's used internally by the app
* (i.e., mapping {@code null} to {@link #DIRECTORY_ROOT});
* if necessary.
*/
public static String getInternalDirectoryName(@Nullable String name) {
return name == null ? DIRECTORY_ROOT : name;
}
/**
* Gets the name of a directory name in the format that is used externally
* (i.e., mapping {@link #DIRECTORY_ROOT} to {@code null} if necessary);
*/
@Nullable
public static String getExternalDirectoryName(String name) {
return name.equals(DIRECTORY_ROOT) ? null : name;
}
/**
* Gets the URI permission for the given volume and directory.
*
* @param context caller's context.
* @param storageClient storage provider client.
* @param storageVolume volume.
* @param directoryName directory name, or {@link #DIRECTORY_ROOT} for full volume.
* @param userId caller's user handle.
* @param logMetrics whether intermediate errors should be logged.
* @param callback callback that receives the results.
*
* @return whether the call was succesfull or not.
*/
public static boolean getUriPermission(Context context,
ContentProviderClient storageClient, StorageVolume storageVolume,
String directoryName, int userId, boolean logMetrics,
GetUriPermissionCallback callback) {
if (DEBUG) {
Log.d(TAG, "getUriPermission() for volume " + storageVolume.dump() + ", directory "
+ directoryName + ", and user " + userId);
}
final boolean isRoot = directoryName.equals(DIRECTORY_ROOT);
final boolean isPrimary = storageVolume.isPrimary();
if (isRoot && isPrimary) {
if (DEBUG) Log.d(TAG, "root access requested on primary volume");
return false;
}
final File volumeRoot = storageVolume.getPathFile();
File file;
try {
file = isRoot ? volumeRoot : new File(volumeRoot, directoryName).getCanonicalFile();
} catch (IOException e) {
Log.e(TAG, "Could not get canonical file for volume " + storageVolume.dump()
+ " and directory " + directoryName);
if (logMetrics) logInvalidScopedAccessRequest(context, SCOPED_DIRECTORY_ACCESS_ERROR);
return false;
}
final StorageManager sm = context.getSystemService(StorageManager.class);
final String root, directory;
if (isRoot) {
root = volumeRoot.getAbsolutePath();
directory = ".";
} else {
root = file.getParent();
directory = file.getName();
// Verify directory is valid.
if (TextUtils.isEmpty(directory) || !isStandardDirectory(directory)) {
if (DEBUG) {
Log.d(TAG, "Directory '" + directory + "' is not standard (full path: '"
+ file.getAbsolutePath() + "')");
}
if (logMetrics) {
logInvalidScopedAccessRequest(context,
SCOPED_DIRECTORY_ACCESS_INVALID_DIRECTORY);
}
return false;
}
}
// Gets volume label and converted path.
String volumeLabel = null;
final List