/* * Copyright (C) 2013 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.externalstorage; import static java.util.regex.Pattern.CASE_INSENSITIVE; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.usage.StorageStatsManager; import android.content.AttributionSource; import android.content.ContentResolver; import android.content.UriPermission; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MatrixCursor.RowBuilder; import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.os.Environment; import android.os.UserHandle; import android.os.UserManager; import android.os.storage.DiskInfo; import android.os.storage.StorageEventListener; import android.os.storage.StorageManager; import android.os.storage.VolumeInfo; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Path; import android.provider.DocumentsContract.Root; import android.provider.Settings; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.text.TextUtils; import android.util.ArrayMap; import android.util.DebugUtils; import android.util.Log; import android.util.Pair; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.FileSystemProvider; import com.android.internal.util.IndentingPrintWriter; import java.io.File; import java.io.FileDescriptor; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.UUID; import java.util.regex.Pattern; /** * Presents content of the shared (a.k.a. "external") storage. *
* Starting with Android 11 (R), restricts access to the certain sections of the shared storage: * {@code Android/data/}, {@code Android/obb/} and {@code Android/sandbox/}, that will be hidden in * the DocumentsUI by default. * See * Storage updates in Android 11. *
* Documents ID format: {@code root:path/to/file}.
*/
public class ExternalStorageProvider extends FileSystemProvider {
private static final String TAG = "ExternalStorage";
private static final boolean DEBUG = false;
public static final String AUTHORITY = DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
private static final Uri BASE_URI =
new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build();
/**
* Regex for detecting {@code /Android/data/}, {@code /Android/obb/} and
* {@code /Android/sandbox/} along with all their subdirectories and content.
*/
private static final Pattern PATTERN_RESTRICTED_ANDROID_SUBTREES =
Pattern.compile("^Android/(?:data|obb|sandbox)(?:/.+)?", CASE_INSENSITIVE);
private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES, Root.COLUMN_QUERY_ARGS
};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
};
private static class RootInfo {
public String rootId;
public String volumeId;
public UUID storageUuid;
public int flags;
public String title;
public String docId;
public File visiblePath;
public File path;
// TODO (b/157033915): Make getFreeBytes() faster
public boolean reportAvailableBytes = false;
}
private static final String ROOT_ID_PRIMARY_EMULATED =
DocumentsContract.EXTERNAL_STORAGE_PRIMARY_EMULATED_ROOT_ID;
private static final String GET_DOCUMENT_URI_CALL = "get_document_uri";
private static final String GET_MEDIA_URI_CALL = "get_media_uri";
private StorageManager mStorageManager;
private UserManager mUserManager;
private final Object mRootsLock = new Object();
@GuardedBy("mRootsLock")
private ArrayMap
* Note, that this is different from hidden documents: blocked documents WILL appear
* the UI, but the user WILL NOT be able to select them.
*
* @param documentId the docId of the directory to be checked
* @return true, should be blocked from tree. Otherwise, false.
*
* @see Document#FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE
*/
@Override
protected boolean shouldBlockDirectoryFromTree(@NonNull String documentId)
throws FileNotFoundException {
final File dir = getFileForDocId(documentId, false);
// The file is null or it is not a directory
if (dir == null || !dir.isDirectory()) {
return false;
}
// Allow all directories on USB, including the root.
if (isOnRemovableUsbStorage(documentId)) {
return false;
}
// Get canonical(!) path. Note that this path will have neither leading nor training "/".
// This the root's path will be just an empty string.
final String path = getPathFromDocId(documentId);
// Block the root of the storage
if (path.isEmpty()) {
return true;
}
// Block /Download/ and /Android/ folders from the tree.
if (equalIgnoringCase(path, Environment.DIRECTORY_DOWNLOADS) ||
equalIgnoringCase(path, Environment.DIRECTORY_ANDROID)) {
return true;
}
// This shouldn't really make a difference, but just in case - let's block hidden
// directories as well.
if (shouldHideDocument(documentId)) {
return true;
}
return false;
}
private boolean isOnRemovableUsbStorage(@NonNull String documentId) {
final RootInfo rootInfo;
try {
rootInfo = getRootFromDocId(documentId);
} catch (FileNotFoundException e) {
Log.e(TAG, "Failed to determine rootInfo for docId\"" + documentId + '"');
return false;
}
return (rootInfo.flags & Root.FLAG_REMOVABLE_USB) != 0;
}
@NonNull
@Override
protected String getDocIdForFile(@NonNull File file) throws FileNotFoundException {
return getDocIdForFileMaybeCreate(file, false);
}
@NonNull
private String getDocIdForFileMaybeCreate(@NonNull File file, boolean createNewDir)
throws FileNotFoundException {
String path = file.getAbsolutePath();
// Find the most-specific root path
boolean visiblePath = false;
RootInfo mostSpecificRoot = getMostSpecificRootForPath(path, false);
if (mostSpecificRoot == null) {
// Try visible path if no internal path matches. MediaStore uses visible paths.
visiblePath = true;
mostSpecificRoot = getMostSpecificRootForPath(path, true);
}
if (mostSpecificRoot == null) {
throw new FileNotFoundException("Failed to find root that contains " + path);
}
// Start at first char of path under root
final String rootPath = visiblePath
? mostSpecificRoot.visiblePath.getAbsolutePath()
: mostSpecificRoot.path.getAbsolutePath();
if (rootPath.equals(path)) {
path = "";
} else if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
}
if (!file.exists() && createNewDir) {
Log.i(TAG, "Creating new directory " + file);
if (!file.mkdir()) {
Log.e(TAG, "Could not create directory " + file);
}
}
return mostSpecificRoot.rootId + ':' + path;
}
private RootInfo getMostSpecificRootForPath(String path, boolean visible) {
// Find the most-specific root path
RootInfo mostSpecificRoot = null;
String mostSpecificPath = null;
synchronized (mRootsLock) {
for (int i = 0; i < mRoots.size(); i++) {
final RootInfo root = mRoots.valueAt(i);
final File rootFile = visible ? root.visiblePath : root.path;
if (rootFile != null) {
final String rootPath = rootFile.getAbsolutePath();
if (path.startsWith(rootPath) && (mostSpecificPath == null
|| rootPath.length() > mostSpecificPath.length())) {
mostSpecificRoot = root;
mostSpecificPath = rootPath;
}
}
}
}
return mostSpecificRoot;
}
@Override
protected File getFileForDocId(String docId, boolean visible) throws FileNotFoundException {
return getFileForDocId(docId, visible, true);
}
private File getFileForDocId(String docId, boolean visible, boolean mustExist)
throws FileNotFoundException {
RootInfo root = getRootFromDocId(docId);
return buildFile(root, docId, mustExist);
}
private Pair
* adb shell dumpsys activity provider com.android.externalstorage/.ExternalStorageProvider
*
*/
@Override
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ", 160);
synchronized (mRootsLock) {
for (int i = 0; i < mRoots.size(); i++) {
final RootInfo root = mRoots.valueAt(i);
pw.println("Root{" + root.rootId + "}:");
pw.increaseIndent();
pw.printPair("flags", DebugUtils.flagsToString(Root.class, "FLAG_", root.flags));
pw.println();
pw.printPair("title", root.title);
pw.printPair("docId", root.docId);
pw.println();
pw.printPair("path", root.path);
pw.printPair("visiblePath", root.visiblePath);
pw.decreaseIndent();
pw.println();
}
}
}
@Override
public Bundle call(String method, String arg, Bundle extras) {
Bundle bundle = super.call(method, arg, extras);
if (bundle == null && !TextUtils.isEmpty(method)) {
switch (method) {
case "getDocIdForFileCreateNewDir": {
getContext().enforceCallingPermission(
android.Manifest.permission.MANAGE_DOCUMENTS, null);
if (TextUtils.isEmpty(arg)) {
return null;
}
try {
final String docId = getDocIdForFileMaybeCreate(new File(arg), true);
bundle = new Bundle();
bundle.putString("DOC_ID", docId);
} catch (FileNotFoundException e) {
Log.w(TAG, "file '" + arg + "' not found");
return null;
}
break;
}
case GET_DOCUMENT_URI_CALL: {
// All callers must go through MediaProvider
getContext().enforceCallingPermission(
android.Manifest.permission.WRITE_MEDIA_STORAGE, TAG);
final Uri fileUri = extras.getParcelable(DocumentsContract.EXTRA_URI);
final List