diff --git a/source/funkin/mobile/external/android/java/funkin/provider/DataFolderProvider.java b/source/funkin/mobile/external/android/java/funkin/provider/DataFolderProvider.java index f5446c826..39e2f0b68 100644 --- a/source/funkin/mobile/external/android/java/funkin/provider/DataFolderProvider.java +++ b/source/funkin/mobile/external/android/java/funkin/provider/DataFolderProvider.java @@ -1,6 +1,5 @@ package funkin.provider; - import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.database.MatrixCursor; @@ -16,255 +15,282 @@ 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 - * Android/data//files 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 - * - * This is based on the Termux Document Provider!! We tried making our own but it was buggy :( - * https://github.com/termux/termux-app/blob/7bceab88e2272f961d1b94ef736f1a9e20173247/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java + * @see https://github.com/termux/termux-app/blob/7bceab88e2272f961d1b94ef736f1a9e20173247/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java */ -public class DataFolderProvider extends android.provider.DocumentsProvider { +public class DataFolderProvider extends android.provider.DocumentsProvider +{ + private static final String ALL_MIME_TYPES = "*/*"; - private static final String ALL_MIME_TYPES = "*/*"; + private static File BASE_DIR; - private static File BASE_DIR; - private static String BASE_DIR_PATH; + private static String BASE_DIR_PATH; + 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 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 - }; + 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 + }; - // 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); - @Override - public Cursor queryRoots(String[] projection) { - final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION); - final String applicationName = "::APP_TITLE::"; + 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, "Data Folder"); + row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD); + row.add(Root.COLUMN_TITLE, "::APP_TITLE::"); + row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES); + row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace()); + ::if (APP_PACKAGE != ""):: + row.add(Root.COLUMN_ICON, ::APP_PACKAGE::.R.mipmap.ic_launcher); + ::end:: - 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, "Data Folder"); - 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()); - ::if (APP_PACKAGE != "")::row.add(Root.COLUMN_ICON, ::APP_PACKAGE::.R.mipmap.ic_launcher);::end:: - return result; + 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 file : getFileForDocId(parentDocumentId).listFiles()) + includeFile(result, null, file); + + return result; + } + + @Override + public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException + { + return ParcelFileDescriptor.open(getFileForDocId(documentId), ParcelFileDescriptor.parseMode(mode)); + } + + @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() + { + BASE_DIR = getContext().getExternalFilesDir(null); + BASE_DIR_PATH = BASE_DIR.toPath().toString(); + 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++ + ")"); } - @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; + 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()); } - @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 newFile.getPath(); + } + + @Override + public void deleteDocument(String documentId) throws FileNotFoundException + { + if (!deleteRecursive(getFileForDocId(documentId))) + throw new FileNotFoundException("Failed to delete document with id " + documentId); + } + + @Override + public String getDocumentType(String documentId) throws FileNotFoundException + { + return getMimeType(getFileForDocId(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 LinkedList pending = new LinkedList<>(); + + pending.add(getFileForDocId(rootId)); + + final int MAX_SEARCH_RESULTS = 50; + + while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) + { + final File file = pending.removeFirst(); + boolean isInsideHome; + + try + { + isInsideHome = file.getCanonicalPath().startsWith(BASE_DIR_PATH); + } + catch (IOException e) + { + isInsideHome = true; + } + + if (isInsideHome) + { + if (file.isDirectory()) + { + Collections.addAll(pending, file.listFiles()); } - 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() { - BASE_DIR = getContext().getExternalFilesDir(null); - BASE_DIR_PATH = BASE_DIR.toPath().toString(); - 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++ + ")"); + else if (file.getName().toLowerCase().contains(query)) + { + includeFile(result, null, file); } - 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 result; + } + + @Override + public boolean isChildDocument(String parentDocumentId, String documentId) + { + return documentId.startsWith(parentDocumentId); + } + + private boolean deleteRecursive(File file) + { + if (file.isDirectory()) + { + File[] children = file.listFiles(); + + if (children != null) + { + for (File child : children) + { + if (!deleteRecursive(child)) + { + return false; + } } - return newFile.getPath(); + } } - @Override - public void deleteDocument(String documentId) throws FileNotFoundException { - File file = getFileForDocId(documentId); - if (!file.delete()) { - throw new FileNotFoundException("Failed to delete document with id " + documentId); - } + return file.delete(); + } + + private static String getDocIdForFile(File file) + { + return file.getAbsolutePath(); + } + + 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 mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.substring(lastDot + 1).toLowerCase()); + + if (mime != null) + return mime; + } + + return "application/octet-stream"; + } + } + + 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; } - @Override - public String getDocumentType(String documentId) throws FileNotFoundException { - File file = getFileForDocId(documentId); - return getMimeType(file); - } + if (file.getParentFile().canWrite()) + flags |= Document.FLAG_SUPPORTS_DELETE; - @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); + final String mimeType = getMimeType(file); - // 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); + if (mimeType.startsWith("image/")) + flags |= Document.FLAG_SUPPORTS_THUMBNAIL; - 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(BASE_DIR_PATH); - } 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); - ::if (APP_PACKAGE != "")::row.add(Root.COLUMN_ICON, ::APP_PACKAGE::.R.mipmap.ic_launcher);::end:: - } + final MatrixCursor.RowBuilder row = result.newRow(); + row.add(Document.COLUMN_DOCUMENT_ID, docId); + row.add(Document.COLUMN_DISPLAY_NAME, file.getName()); + 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); + ::if (APP_PACKAGE != ""):: + row.add(Root.COLUMN_ICON, ::APP_PACKAGE::.R.mipmap.ic_launcher); + ::end:: + } }