From e2e664715c60319fbbafd5d98b7fa01f6dcb1b74 Mon Sep 17 00:00:00 2001 From: Boulay Mathias Date: Fri, 3 Jun 2022 22:54:11 +0200 Subject: [PATCH] Steal Termux file provider + add support for moving/deleting folders --- .../src/main/AndroidManifest.xml | 6 +- .../pojavlaunch/scoped/FolderProvider.java | 278 ++++++++++++++++++ .../scoped/GameFolderProvider.java | 176 ----------- 3 files changed, 281 insertions(+), 179 deletions(-) create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/scoped/FolderProvider.java delete mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/scoped/GameFolderProvider.java diff --git a/app_pojavlauncher/src/main/AndroidManifest.xml b/app_pojavlauncher/src/main/AndroidManifest.xml index 143d4e014..95bec0602 100644 --- a/app_pojavlauncher/src/main/AndroidManifest.xml +++ b/app_pojavlauncher/src/main/AndroidManifest.xml @@ -92,13 +92,13 @@ android:screenOrientation="sensorLandscape" android:name=".MainActivity" android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|keyboard|navigation"/> + + android:permission="android.permission.MANAGE_DOCUMENTS"> diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/scoped/FolderProvider.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/scoped/FolderProvider.java new file mode 100644 index 000000000..e46dfe7e2 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/scoped/FolderProvider.java @@ -0,0 +1,278 @@ +package net.kdt.pojavlaunch.scoped; + +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.graphics.Point; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract.Document; +import android.provider.DocumentsContract.Root; +import android.provider.DocumentsProvider; +import android.webkit.MimeTypeMap; + +import net.kdt.pojavlaunch.R; +import net.kdt.pojavlaunch.Tools; + +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedList; + +/** + * A document provider for the Storage Access Framework which exposes the files in the + * $HOME/ directory to other apps. + *

+ * Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent: + *

+ * "A document provider and ACTION_GET_CONTENT should be considered mutually exclusive. If you + * support both of them simultaneously, your app will appear twice in the system picker UI, + * offering two different ways of accessing your stored data. This would be confusing for users." + * - http://developer.android.com/guide/topics/providers/document-provider.html#43 + */ +public class FolderProvider extends DocumentsProvider { + + private static final String ALL_MIME_TYPES = "*/*"; + + private static final File BASE_DIR = new File(Tools.DIR_GAME_HOME); + + + // The default columns to return information about a root if no specific + // columns are requested in a query. + private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{ + Root.COLUMN_ROOT_ID, + Root.COLUMN_MIME_TYPES, + Root.COLUMN_FLAGS, + Root.COLUMN_ICON, + Root.COLUMN_TITLE, + Root.COLUMN_SUMMARY, + Root.COLUMN_DOCUMENT_ID, + Root.COLUMN_AVAILABLE_BYTES + }; + + // The default columns to return information about a document if no specific + // columns are requested in a query. + 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 + }; + + @Override + public Cursor queryRoots(String[] projection) { + final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION); + final String applicationName = getContext().getString(R.string.app_short_name); + + final MatrixCursor.RowBuilder row = result.newRow(); + row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR)); + row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR)); + row.add(Root.COLUMN_SUMMARY, null); + row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD); + row.add(Root.COLUMN_TITLE, applicationName); + row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES); + row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace()); + row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher); + return result; + } + + @Override + public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { + final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION); + includeFile(result, documentId, null); + return result; + } + + @Override + public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { + final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION); + final File parent = getFileForDocId(parentDocumentId); + for (File file : parent.listFiles()) { + includeFile(result, null, file); + } + return result; + } + + @Override + public ParcelFileDescriptor openDocument(final String documentId, String mode, CancellationSignal signal) throws FileNotFoundException { + final File file = getFileForDocId(documentId); + final int accessMode = ParcelFileDescriptor.parseMode(mode); + return ParcelFileDescriptor.open(file, accessMode); + } + + @Override + public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { + final File file = getFileForDocId(documentId); + final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + return new AssetFileDescriptor(pfd, 0, file.length()); + } + + @Override + public boolean onCreate() { + return true; + } + + @Override + public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException { + File newFile = new File(parentDocumentId, displayName); + int noConflictId = 2; + while (newFile.exists()) { + newFile = new File(parentDocumentId, displayName + " (" + noConflictId++ + ")"); + } + try { + boolean succeeded; + if (Document.MIME_TYPE_DIR.equals(mimeType)) { + succeeded = newFile.mkdir(); + } else { + succeeded = newFile.createNewFile(); + } + if (!succeeded) { + throw new FileNotFoundException("Failed to create document with id " + newFile.getPath()); + } + } catch (IOException e) { + throw new FileNotFoundException("Failed to create document with id " + newFile.getPath()); + } + return newFile.getPath(); + } + + @Override + public void deleteDocument(String documentId) throws FileNotFoundException { + File file = getFileForDocId(documentId); + if(file.isDirectory()){ + try { + FileUtils.deleteDirectory(file); + } catch (IOException e) { + throw new FileNotFoundException("Failed to delete document with id " + documentId); + } + }else{ + if (!file.delete()) { + throw new FileNotFoundException("Failed to delete document with id " + documentId); + } + } + } + + @Override + public String getDocumentType(String documentId) throws FileNotFoundException { + File file = getFileForDocId(documentId); + return getMimeType(file); + } + + @Override + public Cursor querySearchDocuments(String rootId, String query, String[] projection) throws FileNotFoundException { + final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION); + final File parent = getFileForDocId(rootId); + + // This example implementation searches file names for the query and doesn't rank search + // results, so we can stop as soon as we find a sufficient number of matches. Other + // implementations might rank results and use other data about files, rather than the file + // name, to produce a match. + final LinkedList pending = new LinkedList<>(); + pending.add(parent); + + final int MAX_SEARCH_RESULTS = 50; + while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) { + final File file = pending.removeFirst(); + // Avoid directories outside the $HOME directory linked with symlinks (to avoid e.g. search + // through the whole SD card). + boolean isInsideHome; + try { + isInsideHome = file.getCanonicalPath().startsWith(Tools.DIR_GAME_HOME); + } catch (IOException e) { + isInsideHome = true; + } + if (isInsideHome) { + if (file.isDirectory()) { + Collections.addAll(pending, file.listFiles()); + } else { + if (file.getName().toLowerCase().contains(query)) { + includeFile(result, null, file); + } + } + } + } + + return result; + } + + @Override + public boolean isChildDocument(String parentDocumentId, String documentId) { + return documentId.startsWith(parentDocumentId); + } + + /** + * Get the document id given a file. This document id must be consistent across time as other + * applications may save the ID and use it to reference documents later. + *

+ * The reverse of @{link #getFileForDocId}. + */ + private static String getDocIdForFile(File file) { + return file.getAbsolutePath(); + } + + /** + * Get the file given a document id (the reverse of {@link #getDocIdForFile(File)}). + */ + private static File getFileForDocId(String docId) throws FileNotFoundException { + final File f = new File(docId); + if (!f.exists()) throw new FileNotFoundException(f.getAbsolutePath() + " not found"); + return f; + } + + private static String getMimeType(File file) { + if (file.isDirectory()) { + return Document.MIME_TYPE_DIR; + } else { + final String name = file.getName(); + final int lastDot = name.lastIndexOf('.'); + if (lastDot >= 0) { + final String extension = name.substring(lastDot + 1).toLowerCase(); + final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + if (mime != null) return mime; + } + return "application/octet-stream"; + } + } + + /** + * Add a representation of a file to a cursor. + * + * @param result the cursor to modify + * @param docId the document ID representing the desired file (may be null if given file) + * @param file the File object representing the desired file (may be null if given docID) + */ + private void includeFile(MatrixCursor result, String docId, File file) + throws FileNotFoundException { + if (docId == null) { + docId = getDocIdForFile(file); + } else { + file = getFileForDocId(docId); + } + + int flags = 0; + if (file.isDirectory()) { + if (file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE; + } else if (file.canWrite()) { + flags |= Document.FLAG_SUPPORTS_WRITE; + } + if (file.getParentFile().canWrite()) flags |= Document.FLAG_SUPPORTS_DELETE; + + final String displayName = file.getName(); + final String mimeType = getMimeType(file); + if (mimeType.startsWith("image/")) flags |= Document.FLAG_SUPPORTS_THUMBNAIL; + + final MatrixCursor.RowBuilder row = result.newRow(); + row.add(Document.COLUMN_DOCUMENT_ID, docId); + row.add(Document.COLUMN_DISPLAY_NAME, displayName); + row.add(Document.COLUMN_SIZE, file.length()); + row.add(Document.COLUMN_MIME_TYPE, mimeType); + row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified()); + row.add(Document.COLUMN_FLAGS, flags); + row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher); + } + +} \ No newline at end of file diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/scoped/GameFolderProvider.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/scoped/GameFolderProvider.java deleted file mode 100644 index c8a92ba1f..000000000 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/scoped/GameFolderProvider.java +++ /dev/null @@ -1,176 +0,0 @@ -package net.kdt.pojavlaunch.scoped; - -import android.content.res.AssetFileDescriptor; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.graphics.Point; -import android.os.CancellationSignal; -import android.os.ParcelFileDescriptor; -import android.provider.DocumentsContract.Document; -import android.provider.DocumentsContract.Root; -import android.provider.DocumentsProvider; -import android.webkit.MimeTypeMap; - -import androidx.annotation.Nullable; - -import net.kdt.pojavlaunch.R; -import net.kdt.pojavlaunch.Tools; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.Collections; -import java.util.LinkedList; - -public class GameFolderProvider extends DocumentsProvider { - static File sBaseDir = new File(Tools.DIR_GAME_HOME); - private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{ - Root.COLUMN_ROOT_ID, - Root.COLUMN_MIME_TYPES, - Root.COLUMN_FLAGS, - Root.COLUMN_ICON, - Root.COLUMN_TITLE, - Root.COLUMN_SUMMARY, - Root.COLUMN_DOCUMENT_ID, - Root.COLUMN_AVAILABLE_BYTES - }; - 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 - }; - - @Override - public Cursor queryRoots(String[] projection) throws FileNotFoundException { - final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION); - final MatrixCursor.RowBuilder row = result.newRow(); - row.add(Root.COLUMN_ROOT_ID, sBaseDir.getAbsolutePath()); - row.add(Root.COLUMN_DOCUMENT_ID, sBaseDir.getAbsolutePath()); - row.add(Root.COLUMN_SUMMARY, null); - row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_RECENTS | Root.FLAG_SUPPORTS_SEARCH); - row.add(Root.COLUMN_TITLE, getContext().getString(R.string.app_name)); - row.add(Root.COLUMN_MIME_TYPES, "*/*"); - row.add(Root.COLUMN_AVAILABLE_BYTES, sBaseDir.getFreeSpace()); - row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher); - return result; - } - - @Override - public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException { - final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION); - includeFile(result,documentId,null); - return result; - } - - @Override - public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException { - final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION); - for(File f : new File(parentDocumentId).listFiles()) { - includeFile(result,null,f); - } - return result; - } - - @Override - public ParcelFileDescriptor openDocument(String documentId, String mode, @Nullable CancellationSignal signal) throws FileNotFoundException { - return ParcelFileDescriptor.open(new File(documentId),ParcelFileDescriptor.parseMode(mode)); - } - - @Override - public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException { - File f = new File(documentId); - return new AssetFileDescriptor(ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY), 0, f.length()); - } - - @Override - public boolean onCreate() { - return true; - } - - @Override - public void deleteDocument(String documentId) throws FileNotFoundException { - if (!new File(documentId).delete()) throw new FileNotFoundException("Can't remove "+documentId); - } - - @Override - public String getDocumentType(String documentId) throws FileNotFoundException { - return getMimeType(new File(documentId)); - } - - @Override - public Cursor querySearchDocuments(String rootId, String query, String[] projection) throws FileNotFoundException { - final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION); - final File parent = new File(rootId); - final LinkedList pending = new LinkedList<>(); - pending.add(parent); - - final int MAX_SEARCH_RESULTS = 50; - while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) { - final File file = pending.removeFirst(); - boolean isInsideGameDir; - try { - isInsideGameDir = file.getCanonicalPath().startsWith(sBaseDir.getCanonicalPath()); - } catch (IOException e) { - isInsideGameDir = true; - } - if (isInsideGameDir) { - if (file.isDirectory()) { - Collections.addAll(pending, file.listFiles()); - } else { - if (file.getName().toLowerCase().contains(query)) { - includeFile(result, null, file); - } - } - } - } - - return result; - } - private static String getMimeType(File file) { - if (file.isDirectory()) { - return Document.MIME_TYPE_DIR; - } else { - final String name = file.getName(); - final int lastDot = name.lastIndexOf('.'); - if (lastDot >= 0) { - final String extension = name.substring(lastDot + 1); - final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - if (mime != null) return mime; - } - return "application/octet-stream"; - } - } - private void includeFile(MatrixCursor result, String docId, File file) - throws FileNotFoundException { - if (docId == null) { - docId = file.getAbsolutePath(); - } else { - file = new File(docId); - } - - int flags = 0; - if (file.isDirectory()) { - if (file.isDirectory() && file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE; - } else if (file.canWrite()) { - flags |= Document.FLAG_SUPPORTS_WRITE | Document.FLAG_SUPPORTS_DELETE; - } - - final String displayName = file.getName(); - final String mimeType = getMimeType(file); - if (mimeType.startsWith("image/")) flags |= Document.FLAG_SUPPORTS_THUMBNAIL; - - final MatrixCursor.RowBuilder row = result.newRow(); - row.add(Document.COLUMN_DOCUMENT_ID, docId); - row.add(Document.COLUMN_DISPLAY_NAME, displayName); - row.add(Document.COLUMN_SIZE, file.length()); - row.add(Document.COLUMN_MIME_TYPE, mimeType); - row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified()); - row.add(Document.COLUMN_FLAGS, flags); - row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher); - } - - -}