From 18a65cefb7c6409de3b9ce9a5cbd9ab7a5ab72c9 Mon Sep 17 00:00:00 2001 From: artdeell Date: Mon, 12 Jul 2021 13:08:41 +0300 Subject: [PATCH] Allow accessing PojavLauncher folder with SAF --- .../src/main/AndroidManifest.xml | 12 ++ .../scoped/GameFolderProvider.java | 176 ++++++++++++++++++ 2 files changed, 188 insertions(+) create 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 157659467..5e49a14a4 100644 --- a/app_pojavlauncher/src/main/AndroidManifest.xml +++ b/app_pojavlauncher/src/main/AndroidManifest.xml @@ -70,6 +70,18 @@ android:screenOrientation="sensorLandscape" android:name=".MainActivity" android:configChanges="keyboardHidden|orientation|screenSize|keyboard|navigation"/> + + + + + + 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 new file mode 100644 index 000000000..1f15b1330 --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/scoped/GameFolderProvider.java @@ -0,0 +1,176 @@ +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 baseDir = 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, baseDir.getAbsolutePath()); + row.add(Root.COLUMN_DOCUMENT_ID, baseDir.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, baseDir.getFreeSpace()); + row.add(Root.COLUMN_ICON, R.drawable.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(baseDir.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.drawable.ic_launcher); + } + + +}