From 62cc55ec3d480454f9954372b00cab32237e7f97 Mon Sep 17 00:00:00 2001 From: Tim Schneeberger Date: Sun, 27 Oct 2024 04:08:22 +0100 Subject: [PATCH 1/6] refactor: move ChangeDataDirectoryLocationPatch.kt to new sub-package --- .../{ => redirect}/ChangeDataDirectoryLocationPatch.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename patches/src/main/kotlin/app/revanced/patches/all/misc/directory/{ => redirect}/ChangeDataDirectoryLocationPatch.kt (97%) diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/redirect/ChangeDataDirectoryLocationPatch.kt similarity index 97% rename from patches/src/main/kotlin/app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/all/misc/directory/redirect/ChangeDataDirectoryLocationPatch.kt index 7256d1edb3..d758a91d4d 100644 --- a/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/redirect/ChangeDataDirectoryLocationPatch.kt @@ -1,4 +1,4 @@ -package app.revanced.patches.all.misc.directory +package app.revanced.patches.all.misc.directory.redirect import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction From f5c1549ede2c3fd33d6998b987a979be4029ecb1 Mon Sep 17 00:00:00 2001 From: Tim Schneeberger Date: Sun, 27 Oct 2024 04:12:22 +0100 Subject: [PATCH 2/6] feat: Add `Internal data documents provider` patch --- .../build.gradle.kts | 11 + .../src/main/AndroidManifest.xml | 1 + .../InternalDataDocumentsProvider.java | 333 ++++++++++++++++++ patches/api/patches.api | 7 +- .../AddInternalDataDocumentsProvider.kt | 58 +++ 5 files changed, 409 insertions(+), 1 deletion(-) create mode 100644 extensions/internal-data-documents-provider/build.gradle.kts create mode 100644 extensions/internal-data-documents-provider/src/main/AndroidManifest.xml create mode 100644 extensions/internal-data-documents-provider/src/main/java/app/revanced/extension/all/directory/documentsprovider/InternalDataDocumentsProvider.java create mode 100644 patches/src/main/kotlin/app/revanced/patches/all/misc/directory/documentsprovider/AddInternalDataDocumentsProvider.kt diff --git a/extensions/internal-data-documents-provider/build.gradle.kts b/extensions/internal-data-documents-provider/build.gradle.kts new file mode 100644 index 0000000000..ffe72a7774 --- /dev/null +++ b/extensions/internal-data-documents-provider/build.gradle.kts @@ -0,0 +1,11 @@ +extension { + name = "extensions/all/directory/documentsprovider.rve" +} + +android { + namespace = "app.revanced.extension" +} + +dependencies { + compileOnly(libs.annotation) +} diff --git a/extensions/internal-data-documents-provider/src/main/AndroidManifest.xml b/extensions/internal-data-documents-provider/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..15e7c2ae67 --- /dev/null +++ b/extensions/internal-data-documents-provider/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/internal-data-documents-provider/src/main/java/app/revanced/extension/all/directory/documentsprovider/InternalDataDocumentsProvider.java b/extensions/internal-data-documents-provider/src/main/java/app/revanced/extension/all/directory/documentsprovider/InternalDataDocumentsProvider.java new file mode 100644 index 0000000000..4a7a2b0893 --- /dev/null +++ b/extensions/internal-data-documents-provider/src/main/java/app/revanced/extension/all/directory/documentsprovider/InternalDataDocumentsProvider.java @@ -0,0 +1,333 @@ +package app.revanced.extension.all.directory.documentsprovider; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.ProviderInfo; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; +import android.provider.DocumentsProvider; +import android.system.ErrnoException; +import android.system.Os; +import android.system.StructStat; +import android.util.Log; +import android.webkit.MimeTypeMap; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.Objects; + +/** + * A DocumentsProvider that allows access to the app's internal data directory. + */ +public class InternalDataDocumentsProvider extends DocumentsProvider { + private static final String[] rootColumns = + {"root_id", "mime_types", "flags", "icon", "title", "summary", "document_id"}; + private static final String[] directoryColumns = + {"document_id", "mime_type", "_display_name", "last_modified", "flags", + "_size", "full_path", "lstat_info"}; + + private String packageName; + private File dataDirectory; + + @Override + public final boolean onCreate() { + return true; + } + + @Override + public final void attachInfo(Context context, ProviderInfo providerInfo) { + super.attachInfo(context, providerInfo); + + this.packageName = context.getPackageName(); + this.dataDirectory = context.getFilesDir().getParentFile(); + } + + @Override + public final String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { + File directory = resolveDocumentId(parentDocumentId); + File file = new File(directory, displayName); + + // If file already exists, append a number to the name + int i = 2; + while (file.exists()) { + file = new File(directory, displayName + " (" + i + ")"); + i++; + } + + try { + // Create the file or directory + if (mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR) ? file.mkdir() : file.createNewFile()) { + // Return the document ID of the new entity + if (!parentDocumentId.endsWith("/")) { + parentDocumentId = parentDocumentId + "/"; + } + return parentDocumentId + file.getName(); + } + } catch (IOException e) { + // Do nothing. We are throwing a FileNotFoundException later if the file could not be created. + } + throw new FileNotFoundException("Failed to create document in " + parentDocumentId + " with name " + displayName); + } + + @Override + public final void deleteDocument(String documentId) throws FileNotFoundException { + File file = resolveDocumentId(documentId); + if (!deleteRecursively(file)) { + throw new FileNotFoundException("Failed to delete document " + documentId); + } + } + + @Override + public final String getDocumentType(String documentId) throws FileNotFoundException { + return resolveMimeType(resolveDocumentId(documentId)); + } + + @Override + public final boolean isChildDocument(String parentDocumentId, String documentId) { + return documentId.startsWith(parentDocumentId); + } + + @Override + public final String moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId) throws FileNotFoundException { + File source = resolveDocumentId(sourceDocumentId); + File dest = resolveDocumentId(targetParentDocumentId); + + File file = new File(dest, source.getName()); + if (!file.exists() && source.renameTo(file)) { + // Return the new document ID + if (targetParentDocumentId.endsWith("/")) { + return targetParentDocumentId + file.getName(); + } + return targetParentDocumentId + "/" + file.getName(); + } + + throw new FileNotFoundException("Failed to move document from " + sourceDocumentId + " to " + targetParentDocumentId); + } + + @Override + public final ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException { + File file = resolveDocumentId(documentId); + return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode)); + } + + @Override + public final Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { + if (parentDocumentId.endsWith("/")) { + parentDocumentId = parentDocumentId.substring(0, parentDocumentId.length() - 1); + } + + if (projection == null) { + projection = directoryColumns; + } + + MatrixCursor cursor = new MatrixCursor(projection); + File children = resolveDocumentId(parentDocumentId); + + // Collect all children + File[] files = children.listFiles(); + if (files != null) { + for (File file : files) { + addRowForDocument(cursor, parentDocumentId + "/" + file.getName(), file); + } + } + return cursor; + } + + @Override + public final Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { + if (projection == null) { + projection = directoryColumns; + } + + MatrixCursor cursor = new MatrixCursor(projection); + addRowForDocument(cursor, documentId, null); + return cursor; + } + + @Override + public final Cursor queryRoots(String[] projection) { + ApplicationInfo info = Objects.requireNonNull(getContext()).getApplicationInfo(); + String appName = info.loadLabel(getContext().getPackageManager()).toString(); + + if (projection == null) { + projection = rootColumns; + } + + MatrixCursor cursor = new MatrixCursor(projection); + MatrixCursor.RowBuilder row = cursor.newRow(); + row.add(DocumentsContract.Root.COLUMN_ROOT_ID, this.packageName); + row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, this.packageName); + row.add(DocumentsContract.Root.COLUMN_SUMMARY, this.packageName); + row.add(DocumentsContract.Root.COLUMN_FLAGS, + DocumentsContract.Root.FLAG_LOCAL_ONLY | + DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD); + row.add(DocumentsContract.Root.COLUMN_TITLE, appName); + row.add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*"); + row.add(DocumentsContract.Root.COLUMN_ICON, info.icon); + return cursor; + } + + @Override + public final void removeDocument(String documentId, String parentDocumentId) throws FileNotFoundException { + deleteDocument(documentId); + } + + @Override + public final String renameDocument(String documentId, String displayName) throws FileNotFoundException { + File file = resolveDocumentId(documentId); + if (!file.renameTo(new File(file.getParentFile(), displayName))) { + throw new FileNotFoundException("Failed to rename document from " + documentId + " to " + displayName); + } + + // Return the new document ID + return documentId.substring(0, documentId.lastIndexOf('/', documentId.length() - 2)) + "/" + displayName; + } + + /** + * Recursively delete a file or directory and all its children. + * + * @param root The file or directory to delete. + * @return True if the file or directory and all its children were successfully deleted. + */ + private static boolean deleteRecursively(File root) { + // If root is a directory, delete all children first + if (root.isDirectory()) { + try { + // Only delete recursively if the directory is not a symlink + if ((Os.lstat(root.getPath()).st_mode & 0x8000 /* S_IFLNK */) != 0x8000) { + File[] files = root.listFiles(); + if (files != null) { + for (File file : files) { + if (!deleteRecursively(file)) { + return false; + } + } + } + } + } catch (ErrnoException e) { + Log.e("InternalDocumentsProvider", "Failed to lstat " + root.getPath(), e); + } + } + + // Delete file or empty directory + return root.delete(); + } + + /** + * Resolve the MIME type of a file based on its extension. + * + * @param file The file to resolve the MIME type for. + * @return The MIME type of the file. + */ + private static String resolveMimeType(File file) { + if (file.isDirectory()) { + return DocumentsContract.Document.MIME_TYPE_DIR; + } + + String name = file.getName(); + int indexOfExtDot = name.lastIndexOf('.'); + if (indexOfExtDot < 0) { + // No extension + return "application/octet-stream"; + } + + String extension = name.substring(indexOfExtDot + 1).toLowerCase(); + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + return mimeType != null ? mimeType : "application/octet-stream"; + } + + /** + * Resolve a file instance for a given document ID. + * + * @param fullContentPath The document ID to resolve. + * @return File object for the given document ID. + * @throws FileNotFoundException If the document ID is invalid or the file does not exist. + */ + private File resolveDocumentId(String fullContentPath) throws FileNotFoundException { + if (!fullContentPath.startsWith(this.packageName)) { + throw new FileNotFoundException(fullContentPath + " not found"); + } + String path = fullContentPath.substring(this.packageName.length()); + + // Resolve the relative path within /data/data/{PKG} + File file; + if (path.equals("/") || path.isEmpty()) { + file = this.dataDirectory; + } else { + String relativePath = path.substring(path.indexOf('/') + 1); + file = new File(this.dataDirectory, relativePath); + } + + if(!file.exists()) { + throw new FileNotFoundException(fullContentPath + " not found"); + } + return file; + } + + /** + * Add a row containing all file properties to a MatrixCursor for a given document ID. + * + * @param cursor The cursor to add the row to. + * @param documentId The document ID to add the row for. + * @param file The file to add the row for. If null, the file will be resolved from the document ID. + * @throws FileNotFoundException If the file does not exist. + */ + private void addRowForDocument(MatrixCursor cursor, String documentId, File file) throws FileNotFoundException { + if (file == null) { + file = resolveDocumentId(documentId); + } + + int flags = 0; + if (file.isDirectory()) { + // Prefer list view for directories + flags = flags | DocumentsContract.Document.FLAG_DIR_PREFERS_LAST_MODIFIED; + } + + if (file.canWrite()) { + if (file.isDirectory()) { + flags = flags | DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE; + } + + flags = flags | DocumentsContract.Document.FLAG_SUPPORTS_WRITE | + DocumentsContract.Document.FLAG_SUPPORTS_DELETE | + DocumentsContract.Document.FLAG_SUPPORTS_RENAME | + DocumentsContract.Document.FLAG_SUPPORTS_MOVE; + } + + MatrixCursor.RowBuilder row = cursor.newRow(); + row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, documentId); + row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, file.getName()); + row.add(DocumentsContract.Document.COLUMN_SIZE, file.length()); + row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, resolveMimeType(file)); + row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, file.lastModified()); + row.add(DocumentsContract.Document.COLUMN_FLAGS, flags); + + // Custom columns + row.add("full_path", file.getAbsolutePath()); + + // Add lstat column + String path = file.getPath(); + try { + StringBuilder sb = new StringBuilder(); + StructStat lstat = Os.lstat(path); + sb.append(lstat.st_mode); + sb.append(";"); + sb.append(lstat.st_uid); + sb.append(";"); + sb.append(lstat.st_gid); + // Append symlink target if it is a symlink + if ((lstat.st_mode & 0x8000) == 0x8000 /* S_IFLNK */) { + sb.append(";"); + sb.append(Os.readlink(path)); + } + row.add("lstat_info", sb.toString()); + } catch (Exception ex) { + Log.e("InternalDocumentsProvider", "Failed to get lstat info for " + path, ex); + } + } +} diff --git a/patches/api/patches.api b/patches/api/patches.api index 1bed18e283..911b78d928 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -56,7 +56,12 @@ public final class app/revanced/patches/all/misc/debugging/EnableAndroidDebuggin public static final fun getEnableAndroidDebuggingPatch ()Lapp/revanced/patcher/patch/ResourcePatch; } -public final class app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatchKt { +public final class app/revanced/patches/all/misc/directory/documentsprovider/AddInternalDataDocumentsProviderKt { + public static final fun getInternalDataDocumentsProviderExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; + public static final fun getInternalDataDocumentsProviderPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/all/misc/directory/redirect/ChangeDataDirectoryLocationPatchKt { public static final fun getChangeDataDirectoryLocationPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/documentsprovider/AddInternalDataDocumentsProvider.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/documentsprovider/AddInternalDataDocumentsProvider.kt new file mode 100644 index 0000000000..b5bd2ccc94 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/documentsprovider/AddInternalDataDocumentsProvider.kt @@ -0,0 +1,58 @@ +package app.revanced.patches.all.misc.directory.documentsprovider + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.util.asSequence + +internal const val DOCUMENTS_PROVIDER_CLASS = + "app.revanced.extension.all.directory.documentsprovider.InternalDataDocumentsProvider" + +@Suppress("unused") +val internalDataDocumentsProviderExtensionPatch = bytecodePatch { + extendWith("extensions/all/directory/documentsprovider.rve") +} + +@Suppress("unused") +val internalDataDocumentsProviderPatch = resourcePatch( + name = "Internal data documents provider", + description = "Exports an documents provider that grants access to the internal data directory of this app" + + "to file managers and other apps that support the Storage Access Framework.", + use = false, +) { + dependsOn(internalDataDocumentsProviderExtensionPatch) + + execute { + document("AndroidManifest.xml").use { document -> + // Check if the provider is already declared + if(document.getElementsByTagName("provider") + .asSequence() + .any { it.attributes.getNamedItem("android:name")?.nodeValue == DOCUMENTS_PROVIDER_CLASS }) { + return@execute + } + + val authority = + document.getElementsByTagName("manifest").item(0).attributes.getNamedItem("package").let { + // Select a URI authority name that is unique to the current app + "${it.nodeValue}.${DOCUMENTS_PROVIDER_CLASS}" + } + + // Register the documents provider + with(document.getElementsByTagName("application").item(0)) { + document.createElement("provider").apply { + setAttribute("android:name", DOCUMENTS_PROVIDER_CLASS) + setAttribute("android:authorities", authority) + setAttribute("android:exported", "true") + setAttribute("android:grantUriPermissions", "true") + setAttribute("android:permission", "android.permission.MANAGE_DOCUMENTS") + + document.createElement("intent-filter").apply { + document.createElement("action").apply { + setAttribute("android:name", "android.content.action.DOCUMENTS_PROVIDER") + }.let(this::appendChild) + }.let(this::appendChild) + }.let(this::appendChild) + } + } + } +} + From 73f2638a7ff74f0bedca99c73df4cb11f88c7f7a Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 19 Nov 2024 11:28:56 +0400 Subject: [PATCH 3/6] fix: Deprecate and remove old patch --- patches/api/patches.api | 8 +-- .../ChangeDataDirectoryLocationPatch.kt | 19 ++++++ .../ChangeDataDirectoryLocationPatch.kt | 58 ------------------- 3 files changed, 23 insertions(+), 62 deletions(-) create mode 100644 patches/src/main/kotlin/app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatch.kt delete mode 100644 patches/src/main/kotlin/app/revanced/patches/all/misc/directory/redirect/ChangeDataDirectoryLocationPatch.kt diff --git a/patches/api/patches.api b/patches/api/patches.api index 911b78d928..0d62221701 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -56,15 +56,15 @@ public final class app/revanced/patches/all/misc/debugging/EnableAndroidDebuggin public static final fun getEnableAndroidDebuggingPatch ()Lapp/revanced/patcher/patch/ResourcePatch; } +public final class app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatchKt { + public static final fun getChangeDataDirectoryLocationPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + public final class app/revanced/patches/all/misc/directory/documentsprovider/AddInternalDataDocumentsProviderKt { public static final fun getInternalDataDocumentsProviderExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; public static final fun getInternalDataDocumentsProviderPatch ()Lapp/revanced/patcher/patch/ResourcePatch; } -public final class app/revanced/patches/all/misc/directory/redirect/ChangeDataDirectoryLocationPatchKt { - public static final fun getChangeDataDirectoryLocationPatch ()Lapp/revanced/patcher/patch/BytecodePatch; -} - public final class app/revanced/patches/all/misc/hex/HexPatchKt { public static final fun getHexPatch ()Lapp/revanced/patcher/patch/RawResourcePatch; } diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatch.kt new file mode 100644 index 0000000000..0be0e0f427 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatch.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.all.misc.directory + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.directory.documentsprovider.internalDataDocumentsProviderExtensionPatch + +@Suppress("unused") +@Deprecated( + "Superseded by internalDataDocumentsProviderExtensionPatch", + ReplaceWith("internalDataDocumentsProviderExtensionPatch") +) +val changeDataDirectoryLocationPatch = bytecodePatch( + // name = "Change data directory location", + description = "Changes the data directory in the application from " + + "the app internal storage directory to /sdcard/android/data accessible by root-less devices." + + "Using this patch can cause unexpected issues with some apps.", + use = false, +) { + dependsOn(internalDataDocumentsProviderExtensionPatch) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/redirect/ChangeDataDirectoryLocationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/redirect/ChangeDataDirectoryLocationPatch.kt deleted file mode 100644 index d758a91d4d..0000000000 --- a/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/redirect/ChangeDataDirectoryLocationPatch.kt +++ /dev/null @@ -1,58 +0,0 @@ -package app.revanced.patches.all.misc.directory.redirect - -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction -import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patches.all.misc.transformation.transformInstructionsPatch -import app.revanced.util.getReference -import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c -import com.android.tools.smali.dexlib2.iface.reference.MethodReference -import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference -import com.android.tools.smali.dexlib2.util.MethodUtil - -@Suppress("unused") -val changeDataDirectoryLocationPatch = bytecodePatch( - name = "Change data directory location", - description = "Changes the data directory in the application from " + - "the app internal storage directory to /sdcard/android/data accessible by root-less devices." + - "Using this patch can cause unexpected issues with some apps.", - use = false, -) { - dependsOn( - transformInstructionsPatch( - filterMap = filter@{ _, _, instruction, instructionIndex -> - val reference = instruction.getReference() ?: return@filter null - - if (!MethodUtil.methodSignaturesMatch(reference, MethodCall.GetDir.reference)) { - return@filter null - } - - return@filter instructionIndex - }, - transform = { method, index -> - val getDirInstruction = method.getInstruction(index) - val contextRegister = getDirInstruction.registerC - val dataRegister = getDirInstruction.registerD - - method.replaceInstruction( - index, - "invoke-virtual { v$contextRegister, v$dataRegister }, " + - "Landroid/content/Context;->getExternalFilesDir(Ljava/lang/String;)Ljava/io/File;", - ) - }, - ), - ) -} - -private enum class MethodCall( - val reference: MethodReference, -) { - GetDir( - ImmutableMethodReference( - "Landroid/content/Context;", - "getDir", - listOf("Ljava/lang/String;", "I"), - "Ljava/io/File;", - ), - ), -} From 643393579741ef12ecb9dd3aef7030aa184fc4a7 Mon Sep 17 00:00:00 2001 From: Tim Schneeberger Date: Sat, 23 Nov 2024 13:17:30 +0100 Subject: [PATCH 4/6] refactor: introduce constant for S_IFLNK --- .../documentsprovider/InternalDataDocumentsProvider.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/extensions/internal-data-documents-provider/src/main/java/app/revanced/extension/all/directory/documentsprovider/InternalDataDocumentsProvider.java b/extensions/internal-data-documents-provider/src/main/java/app/revanced/extension/all/directory/documentsprovider/InternalDataDocumentsProvider.java index 4a7a2b0893..5a5b554396 100644 --- a/extensions/internal-data-documents-provider/src/main/java/app/revanced/extension/all/directory/documentsprovider/InternalDataDocumentsProvider.java +++ b/extensions/internal-data-documents-provider/src/main/java/app/revanced/extension/all/directory/documentsprovider/InternalDataDocumentsProvider.java @@ -30,6 +30,7 @@ public class InternalDataDocumentsProvider extends DocumentsProvider { private static final String[] directoryColumns = {"document_id", "mime_type", "_display_name", "last_modified", "flags", "_size", "full_path", "lstat_info"}; + private static final int S_IFLNK = 0x8000; private String packageName; private File dataDirectory; @@ -199,7 +200,7 @@ private static boolean deleteRecursively(File root) { if (root.isDirectory()) { try { // Only delete recursively if the directory is not a symlink - if ((Os.lstat(root.getPath()).st_mode & 0x8000 /* S_IFLNK */) != 0x8000) { + if ((Os.lstat(root.getPath()).st_mode & S_IFLNK) != S_IFLNK) { File[] files = root.listFiles(); if (files != null) { for (File file : files) { @@ -321,7 +322,7 @@ private void addRowForDocument(MatrixCursor cursor, String documentId, File file sb.append(";"); sb.append(lstat.st_gid); // Append symlink target if it is a symlink - if ((lstat.st_mode & 0x8000) == 0x8000 /* S_IFLNK */) { + if ((lstat.st_mode & S_IFLNK) == S_IFLNK) { sb.append(";"); sb.append(Os.readlink(path)); } From 451788d29c9701a6dab70e1f90cf63af60a872d0 Mon Sep 17 00:00:00 2001 From: Tim Schneeberger Date: Sat, 23 Nov 2024 13:18:50 +0100 Subject: [PATCH 5/6] refactor: remove String.indexOf call --- .../documentsprovider/InternalDataDocumentsProvider.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/internal-data-documents-provider/src/main/java/app/revanced/extension/all/directory/documentsprovider/InternalDataDocumentsProvider.java b/extensions/internal-data-documents-provider/src/main/java/app/revanced/extension/all/directory/documentsprovider/InternalDataDocumentsProvider.java index 5a5b554396..a4017b1d7a 100644 --- a/extensions/internal-data-documents-provider/src/main/java/app/revanced/extension/all/directory/documentsprovider/InternalDataDocumentsProvider.java +++ b/extensions/internal-data-documents-provider/src/main/java/app/revanced/extension/all/directory/documentsprovider/InternalDataDocumentsProvider.java @@ -260,7 +260,8 @@ private File resolveDocumentId(String fullContentPath) throws FileNotFoundExcept if (path.equals("/") || path.isEmpty()) { file = this.dataDirectory; } else { - String relativePath = path.substring(path.indexOf('/') + 1); + // Remove leading slash + String relativePath = path.substring(1); file = new File(this.dataDirectory, relativePath); } From 26e59d3a078fa67802e4221832acdde34557a74e Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 9 Dec 2024 06:53:40 +0100 Subject: [PATCH 6/6] refactor --- .../build.gradle.kts | 3 + .../src/main/AndroidManifest.xml | 0 .../InternalDataDocumentsProvider.java | 117 +++++++++--------- .../build.gradle.kts | 11 -- patches/api/patches.api | 5 +- .../ChangeDataDirectoryLocationPatch.kt | 14 +-- ...portInternalDataDocumentsProviderPatch.kt} | 42 +++---- 7 files changed, 91 insertions(+), 101 deletions(-) create mode 100644 extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/build.gradle.kts rename extensions/{internal-data-documents-provider => all/misc/directory/documentsprovider/export-internal-data-documents-provider}/src/main/AndroidManifest.xml (100%) rename extensions/{internal-data-documents-provider/src/main/java/app/revanced/extension/all => all/misc/directory/documentsprovider/export-internal-data-documents-provider/src/main/java/app/revanced/extension/all/misc}/directory/documentsprovider/InternalDataDocumentsProvider.java (97%) delete mode 100644 extensions/internal-data-documents-provider/build.gradle.kts rename patches/src/main/kotlin/app/revanced/patches/all/misc/directory/documentsprovider/{AddInternalDataDocumentsProvider.kt => ExportInternalDataDocumentsProviderPatch.kt} (53%) diff --git a/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/build.gradle.kts b/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/build.gradle.kts new file mode 100644 index 0000000000..8618c67e1a --- /dev/null +++ b/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/build.gradle.kts @@ -0,0 +1,3 @@ +dependencies { + compileOnly(libs.annotation) +} diff --git a/extensions/internal-data-documents-provider/src/main/AndroidManifest.xml b/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/src/main/AndroidManifest.xml similarity index 100% rename from extensions/internal-data-documents-provider/src/main/AndroidManifest.xml rename to extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/src/main/AndroidManifest.xml diff --git a/extensions/internal-data-documents-provider/src/main/java/app/revanced/extension/all/directory/documentsprovider/InternalDataDocumentsProvider.java b/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/src/main/java/app/revanced/extension/all/misc/directory/documentsprovider/InternalDataDocumentsProvider.java similarity index 97% rename from extensions/internal-data-documents-provider/src/main/java/app/revanced/extension/all/directory/documentsprovider/InternalDataDocumentsProvider.java rename to extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/src/main/java/app/revanced/extension/all/misc/directory/documentsprovider/InternalDataDocumentsProvider.java index a4017b1d7a..cd4577c76e 100644 --- a/extensions/internal-data-documents-provider/src/main/java/app/revanced/extension/all/directory/documentsprovider/InternalDataDocumentsProvider.java +++ b/extensions/all/misc/directory/documentsprovider/export-internal-data-documents-provider/src/main/java/app/revanced/extension/all/misc/directory/documentsprovider/InternalDataDocumentsProvider.java @@ -1,4 +1,4 @@ -package app.revanced.extension.all.directory.documentsprovider; +package app.revanced.extension.all.misc.directory.documentsprovider; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -14,11 +14,10 @@ import android.system.StructStat; import android.util.Log; import android.webkit.MimeTypeMap; + import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; import java.util.Objects; /** @@ -35,6 +34,59 @@ public class InternalDataDocumentsProvider extends DocumentsProvider { private String packageName; private File dataDirectory; + /** + * Recursively delete a file or directory and all its children. + * + * @param root The file or directory to delete. + * @return True if the file or directory and all its children were successfully deleted. + */ + private static boolean deleteRecursively(File root) { + // If root is a directory, delete all children first + if (root.isDirectory()) { + try { + // Only delete recursively if the directory is not a symlink + if ((Os.lstat(root.getPath()).st_mode & S_IFLNK) != S_IFLNK) { + File[] files = root.listFiles(); + if (files != null) { + for (File file : files) { + if (!deleteRecursively(file)) { + return false; + } + } + } + } + } catch (ErrnoException e) { + Log.e("InternalDocumentsProvider", "Failed to lstat " + root.getPath(), e); + } + } + + // Delete file or empty directory + return root.delete(); + } + + /** + * Resolve the MIME type of a file based on its extension. + * + * @param file The file to resolve the MIME type for. + * @return The MIME type of the file. + */ + private static String resolveMimeType(File file) { + if (file.isDirectory()) { + return DocumentsContract.Document.MIME_TYPE_DIR; + } + + String name = file.getName(); + int indexOfExtDot = name.lastIndexOf('.'); + if (indexOfExtDot < 0) { + // No extension + return "application/octet-stream"; + } + + String extension = name.substring(indexOfExtDot + 1).toLowerCase(); + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + return mimeType != null ? mimeType : "application/octet-stream"; + } + @Override public final boolean onCreate() { return true; @@ -189,59 +241,6 @@ public final String renameDocument(String documentId, String displayName) throws return documentId.substring(0, documentId.lastIndexOf('/', documentId.length() - 2)) + "/" + displayName; } - /** - * Recursively delete a file or directory and all its children. - * - * @param root The file or directory to delete. - * @return True if the file or directory and all its children were successfully deleted. - */ - private static boolean deleteRecursively(File root) { - // If root is a directory, delete all children first - if (root.isDirectory()) { - try { - // Only delete recursively if the directory is not a symlink - if ((Os.lstat(root.getPath()).st_mode & S_IFLNK) != S_IFLNK) { - File[] files = root.listFiles(); - if (files != null) { - for (File file : files) { - if (!deleteRecursively(file)) { - return false; - } - } - } - } - } catch (ErrnoException e) { - Log.e("InternalDocumentsProvider", "Failed to lstat " + root.getPath(), e); - } - } - - // Delete file or empty directory - return root.delete(); - } - - /** - * Resolve the MIME type of a file based on its extension. - * - * @param file The file to resolve the MIME type for. - * @return The MIME type of the file. - */ - private static String resolveMimeType(File file) { - if (file.isDirectory()) { - return DocumentsContract.Document.MIME_TYPE_DIR; - } - - String name = file.getName(); - int indexOfExtDot = name.lastIndexOf('.'); - if (indexOfExtDot < 0) { - // No extension - return "application/octet-stream"; - } - - String extension = name.substring(indexOfExtDot + 1).toLowerCase(); - String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - return mimeType != null ? mimeType : "application/octet-stream"; - } - /** * Resolve a file instance for a given document ID. * @@ -265,7 +264,7 @@ private File resolveDocumentId(String fullContentPath) throws FileNotFoundExcept file = new File(this.dataDirectory, relativePath); } - if(!file.exists()) { + if (!file.exists()) { throw new FileNotFoundException(fullContentPath + " not found"); } return file; @@ -274,9 +273,9 @@ private File resolveDocumentId(String fullContentPath) throws FileNotFoundExcept /** * Add a row containing all file properties to a MatrixCursor for a given document ID. * - * @param cursor The cursor to add the row to. + * @param cursor The cursor to add the row to. * @param documentId The document ID to add the row for. - * @param file The file to add the row for. If null, the file will be resolved from the document ID. + * @param file The file to add the row for. If null, the file will be resolved from the document ID. * @throws FileNotFoundException If the file does not exist. */ private void addRowForDocument(MatrixCursor cursor, String documentId, File file) throws FileNotFoundException { diff --git a/extensions/internal-data-documents-provider/build.gradle.kts b/extensions/internal-data-documents-provider/build.gradle.kts deleted file mode 100644 index ffe72a7774..0000000000 --- a/extensions/internal-data-documents-provider/build.gradle.kts +++ /dev/null @@ -1,11 +0,0 @@ -extension { - name = "extensions/all/directory/documentsprovider.rve" -} - -android { - namespace = "app.revanced.extension" -} - -dependencies { - compileOnly(libs.annotation) -} diff --git a/patches/api/patches.api b/patches/api/patches.api index 61213f7b1d..5f7861ff2f 100644 --- a/patches/api/patches.api +++ b/patches/api/patches.api @@ -60,9 +60,8 @@ public final class app/revanced/patches/all/misc/directory/ChangeDataDirectoryLo public static final fun getChangeDataDirectoryLocationPatch ()Lapp/revanced/patcher/patch/BytecodePatch; } -public final class app/revanced/patches/all/misc/directory/documentsprovider/AddInternalDataDocumentsProviderKt { - public static final fun getInternalDataDocumentsProviderExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; - public static final fun getInternalDataDocumentsProviderPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +public final class app/revanced/patches/all/misc/directory/documentsprovider/ExportInternalDataDocumentsProviderPatchKt { + public static final fun getExportInternalDataDocumentsProviderPatch ()Lapp/revanced/patcher/patch/ResourcePatch; } public final class app/revanced/patches/all/misc/hex/HexPatchKt { diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatch.kt index 0be0e0f427..8046c11fc3 100644 --- a/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatch.kt @@ -1,19 +1,19 @@ package app.revanced.patches.all.misc.directory import app.revanced.patcher.patch.bytecodePatch -import app.revanced.patches.all.misc.directory.documentsprovider.internalDataDocumentsProviderExtensionPatch +import app.revanced.patches.all.misc.directory.documentsprovider.exportInternalDataDocumentsProviderPatch @Suppress("unused") @Deprecated( - "Superseded by internalDataDocumentsProviderExtensionPatch", - ReplaceWith("internalDataDocumentsProviderExtensionPatch") + "Superseded by internalDataDocumentsProviderPatch", + ReplaceWith("internalDataDocumentsProviderPatch"), ) val changeDataDirectoryLocationPatch = bytecodePatch( // name = "Change data directory location", description = "Changes the data directory in the application from " + - "the app internal storage directory to /sdcard/android/data accessible by root-less devices." + - "Using this patch can cause unexpected issues with some apps.", + "the app internal storage directory to /sdcard/android/data accessible by root-less devices." + + "Using this patch can cause unexpected issues with some apps.", use = false, ) { - dependsOn(internalDataDocumentsProviderExtensionPatch) -} \ No newline at end of file + dependsOn(exportInternalDataDocumentsProviderPatch) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/documentsprovider/AddInternalDataDocumentsProvider.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/documentsprovider/ExportInternalDataDocumentsProviderPatch.kt similarity index 53% rename from patches/src/main/kotlin/app/revanced/patches/all/misc/directory/documentsprovider/AddInternalDataDocumentsProvider.kt rename to patches/src/main/kotlin/app/revanced/patches/all/misc/directory/documentsprovider/ExportInternalDataDocumentsProviderPatch.kt index b5bd2ccc94..1f2a2d53c4 100644 --- a/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/documentsprovider/AddInternalDataDocumentsProvider.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/documentsprovider/ExportInternalDataDocumentsProviderPatch.kt @@ -3,43 +3,44 @@ package app.revanced.patches.all.misc.directory.documentsprovider import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.resourcePatch import app.revanced.util.asSequence - -internal const val DOCUMENTS_PROVIDER_CLASS = - "app.revanced.extension.all.directory.documentsprovider.InternalDataDocumentsProvider" - -@Suppress("unused") -val internalDataDocumentsProviderExtensionPatch = bytecodePatch { - extendWith("extensions/all/directory/documentsprovider.rve") -} +import app.revanced.util.getNode @Suppress("unused") -val internalDataDocumentsProviderPatch = resourcePatch( - name = "Internal data documents provider", - description = "Exports an documents provider that grants access to the internal data directory of this app" + - "to file managers and other apps that support the Storage Access Framework.", +val exportInternalDataDocumentsProviderPatch = resourcePatch( + name = "Export internal data documents provider", + description = "Exports a documents provider that grants access to the internal data directory of this app " + + "to file managers and other apps that support the Storage Access Framework.", use = false, ) { - dependsOn(internalDataDocumentsProviderExtensionPatch) + dependsOn( + bytecodePatch { + extendWith("extensions/all/misc/directory/export-internal-data-documents-provider.rve") + }, + ) execute { + val documentsProviderClass = + "app.revanced.extension.all.misc.directory.documentsprovider.InternalDataDocumentsProvider" + document("AndroidManifest.xml").use { document -> // Check if the provider is already declared - if(document.getElementsByTagName("provider") - .asSequence() - .any { it.attributes.getNamedItem("android:name")?.nodeValue == DOCUMENTS_PROVIDER_CLASS }) { + if (document.getElementsByTagName("provider") + .asSequence() + .any { it.attributes.getNamedItem("android:name")?.nodeValue == documentsProviderClass } + ) { return@execute } val authority = - document.getElementsByTagName("manifest").item(0).attributes.getNamedItem("package").let { + document.getNode("manifest").attributes.getNamedItem("package").let { // Select a URI authority name that is unique to the current app - "${it.nodeValue}.${DOCUMENTS_PROVIDER_CLASS}" + "${it.nodeValue}.$documentsProviderClass" } // Register the documents provider - with(document.getElementsByTagName("application").item(0)) { + with(document.getNode("application")) { document.createElement("provider").apply { - setAttribute("android:name", DOCUMENTS_PROVIDER_CLASS) + setAttribute("android:name", documentsProviderClass) setAttribute("android:authorities", authority) setAttribute("android:exported", "true") setAttribute("android:grantUriPermissions", "true") @@ -55,4 +56,3 @@ val internalDataDocumentsProviderPatch = resourcePatch( } } } -