Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d120d884 authored by Dipankar Bhardwaj's avatar Dipankar Bhardwaj Committed by Himanshu Arora
Browse files

Implement document trash flow APIs

Add support for document trash flow operations across
browsable reliable volumes, including external_primary
and SD cards. Also splitting FlakeStorageManagerTests and
ExternalStorageProviderTests for clarity.

Bug: 409260889
Test: atest ExternalStorageProviderTest
Flag: android.provider.enable_documents_trash_api
Change-Id: Ibdd6f0c9b44d50243c25d3ba92ed0ae996d44475
parent 5694fb8d
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -128,6 +128,7 @@ aconfig_declarations_group {
        "libcore_exported_aconfig_flags_lib",
        "libcore_readonly_aconfig_flags_lib",
        "libgui_flags_java_lib",
        "mediaprovider_aconfig_flag_lib",
        "networksecurity_exported_aconfig_flags_lib",
        "power_flags_lib",
        "sdk_sandbox_exported_flags_lib",
@@ -2110,3 +2111,14 @@ java_aconfig_library {
    aconfig_declarations: "android.hardware.serial.flags-aconfig",
    defaults: ["framework-minus-apex-aconfig-java-defaults"],
}

java_aconfig_library {
    name: "mediaprovider_aconfig_flag_lib",
    aconfig_declarations: "mediaprovider_flags",
    min_sdk_version: "30",
    apex_available: [
        "//apex_available:platform",
        "com.android.mediaprovider",
    ],
    defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+166 −7
Original line number Diff line number Diff line
@@ -16,6 +16,10 @@

package com.android.internal.content;

import static android.provider.Flags.enableDocumentsTrashApi;

import static com.android.providers.media.flags.Flags.enableTrashAndRestoreByFilePathApi;

import android.annotation.CallSuper;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -71,6 +75,8 @@ import java.util.Locale;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * A helper class for {@link android.provider.DocumentsProvider} to perform file operations on local
@@ -92,6 +98,19 @@ public abstract class FileSystemProvider extends DocumentsProvider {
    private static final int DEFAULT_SEARCH_RESULT_LIMIT = 23;
    private static final int MAX_SEARCH_RESULT_LIMIT = 1000;

    /**
     * File prefix indicating that the file {@link MediaStore.MediaColumns#IS_TRASHED}.
     */
    protected static final String PREFIX_TRASHED = "trashed";

    /**
     * Default directory for trashed items
     */
    protected static final String DIRECTORY_TRASH_STORAGE = ".trash-storage";

    private static final Pattern PATTERN_EXPIRES_FILE = Pattern.compile(
            "(?i)^\\.(pending|trashed)-(\\d+)-([^/]+)$");

    private static String joinNewline(String... args) {
        return TextUtils.join("\n", args);
    }
@@ -609,6 +628,117 @@ public abstract class FileSystemProvider extends DocumentsProvider {
        return DocumentsContract.openImageThumbnail(file);
    }

    @Nullable
    @Override
    public String trashDocument(@NonNull String documentId)
            throws FileNotFoundException {
        if (!enableTrashAndRestoreByFilePathApi()) {
            throw new UnsupportedOperationException(
                    "MediaStore feature for trash is not supported");
        }

        File file = getFileForDocId(documentId);
        if (!file.exists()) {
            throw new FileNotFoundException("File does not exist for " + documentId);
        }

        String trashedPath = MediaStore.trashFile(getContext().getContentResolver(),
                file.getPath());
        File trashedFile = new File(trashedPath);
        final String trashedDocId = getDocIdForFile(trashedFile);
        onDocIdChanged(documentId);
        onDocIdDeleted(documentId, /* shouldRevokeUriPermission */ true);
        onDocIdChanged(trashedDocId);
        return trashedDocId;
    }

    protected final Cursor queryTrashDocuments(File parent, String[] projection)
            throws FileNotFoundException {
        MatrixCursor result = new MatrixCursor(resolveProjection(projection));
        includeTrashFiles(result, parent);
        // include MediaStore trashed files which are not in .trash-storage location
        includeMediaStoreTrashFiles(result);
        return result;
    }

    @Nullable
    @Override
    public String restoreDocumentFromTrash(@NonNull String documentId, @Nullable String targetId)
            throws FileNotFoundException {
        if (!enableTrashAndRestoreByFilePathApi()) {
            throw new UnsupportedOperationException(
                    "MediaStore feature for trash is not supported");
        }

        File file = getFileForDocId(documentId);
        if (!file.exists()) {
            throw new FileNotFoundException("File does not exist for " + documentId);
        }

        if (!isTrashFile(file)) {
            throw new IllegalArgumentException("DocumentId represents a non-trashed file");
        }

        String targetPath = null;
        if (targetId != null) {
            File targetFile = getFileForDocId(targetId);
            if (targetFile != null) {
                targetPath = targetFile.getAbsolutePath();
            }
        }
        String restoredPath = MediaStore.restoreFileFromTrash(getContext().getContentResolver(),
                file.getPath(), targetPath);

        File restoredFile = new File(restoredPath);
        final String restoredDocId = getDocIdForFile(restoredFile);
        onDocIdChanged(documentId);
        onDocIdChanged(restoredDocId);

        return restoredDocId;
    }


    private boolean isTrashFile(File file) {
        final Matcher matcher = PATTERN_EXPIRES_FILE.matcher(file.getName());
        return matcher.matches() && matcher.group(1).equals(PREFIX_TRASHED);
    }

    private void includeTrashFiles(MatrixCursor result, File parent) throws FileNotFoundException  {
        for (File file : parent.listFiles()) {
            if (isTrashFile(file)) {
                includeFile(result, null, file);
                continue;
            }
            if (file.isDirectory()) {
                includeTrashFiles(result, file);
            }
        }
    }

    private void includeMediaStoreTrashFiles(MatrixCursor result)
            throws FileNotFoundException {
        final Uri uri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL);
        final Bundle queryArgs = new Bundle();
        queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY);
        queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
                MediaStore.MediaColumns.RELATIVE_PATH + " NOT LIKE ?");
        queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
                new String[]{DIRECTORY_TRASH_STORAGE + "/%"});
        String[] projection = new String[]{MediaStore.Files.FileColumns.DATA};

        try (Cursor cursor = getContext().getContentResolver().query(uri, projection,
                queryArgs, null)) {
            if (cursor != null) {
                while (cursor.moveToNext()) {
                    final String data = cursor.getString(
                            cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA));
                    File file = new File(data);
                    includeFile(result, null, file);
                }
            }
        }
    }

    protected RowBuilder includeFile(final MatrixCursor result, String docId, File file)
            throws FileNotFoundException {
        final String[] columns = result.getColumnNames();
@@ -627,17 +757,29 @@ public abstract class FileSystemProvider extends DocumentsProvider {
        final int flagIndex = ArrayUtils.indexOf(columns, Document.COLUMN_FLAGS);
        if (flagIndex != -1) {
            final boolean isDir = mimeType.equals(Document.MIME_TYPE_DIR);
            boolean isTrashedFile = isTrashFile(file);
            int flags = 0;
            if (file.canWrite()) {
                flags |= Document.FLAG_SUPPORTS_DELETE;
                if (!isTrashedFile) {
                    flags |= Document.FLAG_SUPPORTS_RENAME;
                    flags |= Document.FLAG_SUPPORTS_MOVE;

                    if (isDir) {
                        flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
                    } else {
                        flags |= Document.FLAG_SUPPORTS_WRITE;
                    }
                }
            }

            if (enableDocumentsTrashApi()) {
                if (isTrashFile(file)) {
                    flags |= Document.FLAG_SUPPORTS_RESTORE;
                } else if (isTrashSupported(file)) {
                    flags |= Document.FLAG_SUPPORTS_TRASH;
                }
            }

            if (isDir && shouldBlockDirectoryFromTree(docId)) {
                flags |= Document.FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE;
@@ -655,7 +797,13 @@ public abstract class FileSystemProvider extends DocumentsProvider {

        final int displayNameIndex = ArrayUtils.indexOf(columns, Document.COLUMN_DISPLAY_NAME);
        if (displayNameIndex != -1) {
            row.add(displayNameIndex, file.getName());
            String name = file.getName();
            final Matcher matcher = PATTERN_EXPIRES_FILE.matcher(name);
            if (matcher.matches() && matcher.group(1).equals(PREFIX_TRASHED)) {
                // .trashed-<timestamp>-<name>
                name = matcher.group(3);
            }
            row.add(displayNameIndex, name);
        }

        final int lastModifiedIndex = ArrayUtils.indexOf(columns, Document.COLUMN_LAST_MODIFIED);
@@ -686,6 +834,17 @@ public abstract class FileSystemProvider extends DocumentsProvider {
        return false;
    }

    /**
     * Some providers may want to restrict access to certain directories and files,
     * e.g. <i>"Android/data"</i> and <i>"Android/obb"</i> on the shared storage for
     * privacy reasons.
     * Such providers should override this method.
     */
    protected boolean isTrashSupported(@NonNull File document)
            throws FileNotFoundException {
        return false;
    }

    /**
     * A variant of the {@link #shouldHideDocument(String)} that takes a {@link File} instead of
     * a {@link String} {@code documentId}.
+30 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.externalstorage;


import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.usage.StorageStatsManager;
@@ -638,6 +639,35 @@ public class ExternalStorageProvider extends FileSystemProvider {
        return result;
    }

    @Override
    protected boolean isTrashSupported(File file) {
        try {
            String documentId = getDocIdForFile(file);
            // Trash not supported on USB devices
            if (isOnRemovableUsbStorage(documentId)) {
                return false;
            }

            final RootInfo root = getRootFromDocId(documentId);
            final String canonicalPath = getPathFromDocId(documentId);
            return !isRestrictedPath(root.rootId, canonicalPath);
        } catch (Exception e) {
            return false;
        }
    }

    @Nullable
    @Override
    public Cursor queryTrashDocuments(String[] projection) throws FileNotFoundException {
        if (!mRoots.containsKey(ROOT_ID_PRIMARY_EMULATED)) {
            return null;
        }

        RootInfo rootInfo = mRoots.get(ROOT_ID_PRIMARY_EMULATED);
        File trashDir = new File(rootInfo.path, DIRECTORY_TRASH_STORAGE);
        return queryTrashDocuments(trashDir, projection);
    }

    @Override
    public Path findDocumentPath(@Nullable String parentDocId, String childDocId)
            throws FileNotFoundException {
+4 −0
Original line number Diff line number Diff line
@@ -50,6 +50,10 @@ public class CleanupTemporaryFilesRule implements TestRule {
     * @param directory the path to start from
     */
    public static void removeFilesRecursively(File directory) {
        if (directory == null || !directory.exists() || directory.listFiles().length == 0) {
            return;
        }

        for (File childFile : directory.listFiles()) {
            if (childFile.isDirectory()) {
                removeFilesRecursively(childFile);
+271 −236

File changed.

Preview size limit exceeded, changes collapsed.

Loading