diff --git a/project.hxp b/project.hxp index 57774a06e..4e4d68f58 100644 --- a/project.hxp +++ b/project.hxp @@ -497,6 +497,8 @@ class Project extends HXProject configureHaxelibs(); configureAssets(); configureIcons(); + if (isAndroid()) + configureDocumentProvider(); readASTCExclusion(); @@ -1259,6 +1261,32 @@ class Project extends HXProject } } + function configureDocumentProvider() + { + final providerXml = [' + + + + + ']; + + final xmlChildren:Null> = config.get('android.application').xmlChildren; + + if (xmlChildren != null) + { + config.get('android.application').xmlChildren = xmlChildren.concat(providerXml); + } + else + { + config.get('android.application').xmlChildren = providerXml; + } + } + /** * Configure the platforms' Ad Mob application keys. */ diff --git a/templates/android/template/app/src/main/java/me/funkin/fnf/ModFolderProvider.java b/templates/android/template/app/src/main/java/me/funkin/fnf/ModFolderProvider.java new file mode 100644 index 000000000..e233b42ae --- /dev/null +++ b/templates/android/template/app/src/main/java/me/funkin/fnf/ModFolderProvider.java @@ -0,0 +1,293 @@ +package me.funkin.fnf; + +import android.annotation.SuppressLint; +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.DocumentsProvider; +import android.provider.DocumentsContract.Root; +import android.provider.DocumentsContract.Document; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Path; + +public class ModFolderProvider extends DocumentsProvider +{ + private static final String TAG = "ModFolderProvider"; + private File rootDir; + 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,}; + + private static final String ROOT = "root"; + + @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, ROOT); + row.add(Root.COLUMN_DOCUMENT_ID, getDocumentID(rootDir)); + row.add(Root.COLUMN_TITLE, "Friday Night Funkin'"); + row.add(Root.COLUMN_SUMMARY, "Mods folder"); + row.add(Root.COLUMN_FLAGS, Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_CREATE); + 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, getFileFromDocumentID(documentId)); + 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 = getFileFromDocumentID(parentDocumentId); + + for (File file : parent.listFiles()) + { + includeFile(result, file); + } + + return result; + } + + @Override + public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal) throws FileNotFoundException + { + return ParcelFileDescriptor.open(getFileFromDocumentID(documentId), ParcelFileDescriptor.parseMode(mode)); + } + + @SuppressLint({"SetWorldReadable", "SetWorldWritable"}) + @Override + public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException + { + File parentPath = getFileFromDocumentID(parentDocumentId); + try + { + File file = new File(parentPath, displayName); + if (mimeType.equals(Document.MIME_TYPE_DIR)) + assert file.mkdir(); + else + assert file.createNewFile(); + assert file.setReadable(true, false); + assert file.setWritable(true, false); + + return getDocumentID(file); + } + catch (IOException e) + { + throw new FileNotFoundException("Cannot create file with id " + parentDocumentId + "/" + displayName); + } + } + + @Override + public void deleteDocument(String documentId) throws FileNotFoundException + { + File file = getFileFromDocumentID(documentId); + + Boolean result = false; + + try + { + if (file.isDirectory()) + { + result = deleteFolderRecursively(file); + } + else + { + result = file.delete(); + } + } + catch (Exception e) + { + Log.e(TAG, e.toString()); + e.printStackTrace(); + } + + if (!result) + { + throw new FileNotFoundException("Failed to delete document with id " + documentId); + } + } + + public boolean deleteFolderRecursively(File folder) + { + boolean isSuccessful = true; + + final File[] files = folder.listFiles(); + + if (files == null || !folder.exists()) + return false; + + for (File item : files) + { + if (item.isDirectory()) + { + isSuccessful = isSuccessful && deleteFolderRecursively(item); + } + else + { + isSuccessful = isSuccessful && item.delete(); + } + } + + isSuccessful = isSuccessful && folder.delete(); + + return isSuccessful; + } + + @Override + public String renameDocument(String documentId, String displayName) throws FileNotFoundException + { + File origin = getFileFromDocumentID(documentId); + File dest = new File(origin.getParentFile(), displayName); + + if (!origin.renameTo(dest)) + { + throw new FileNotFoundException("Failed to rename document with id " + documentId + " to " + displayName); + } + + return getDocumentID(dest); + } + + @Override + public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException + { + final File file = getFileFromDocumentID(documentId); + final ParcelFileDescriptor parcelFileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY); + return new AssetFileDescriptor(parcelFileDescriptor, 0, AssetFileDescriptor.UNKNOWN_LENGTH); + } + + private String getDocumentID(File file) + { + Path filePath = file.toPath().normalize(); + Path rootPath = rootDir.toPath(); + + if (!filePath.startsWith(rootPath)) + return null; + + return ROOT + ":" + rootPath.relativize(filePath).toString(); + } + + private File getFileFromDocumentID(String id) + { + final String[] separatedPath = id.split(":", 2); + + if (separatedPath.length < 2) + return null; + + if (!separatedPath[0].equals(ROOT)) + return null; + + final String filePath = separatedPath[1]; + + if (filePath.isEmpty()) + return rootDir; + + final Path rootPath = rootDir.toPath().normalize().toAbsolutePath(); + final Path desiredPath = rootPath.resolve(filePath).normalize().toAbsolutePath(); + + if (!desiredPath.startsWith(rootPath)) + { + return null; + } + + return desiredPath.toFile(); + } + + private void includeFile(MatrixCursor result, File file) + { + if (file == null) + file = rootDir; + + int flags = 0; + final String mimeType = getTypeForFile(file); + + if (file.canWrite()) + { + flags |= Document.FLAG_SUPPORTS_WRITE | + Document.FLAG_SUPPORTS_DELETE | + Document.FLAG_SUPPORTS_RENAME; + + if (file.isDirectory()) + flags |= Document.FLAG_DIR_SUPPORTS_CREATE; + } + + if (mimeType.startsWith("image/")) + { + flags |= Document.FLAG_SUPPORTS_THUMBNAIL; + } + + final MatrixCursor.RowBuilder row = result.newRow(); + row.add(Document.COLUMN_DOCUMENT_ID, getDocumentID(file)); + 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); + } + + private static String getTypeForFile(File file) + { + if (file.isDirectory()) + { + return Document.MIME_TYPE_DIR; + } + else + { + return getTypeForName(file.getName()); + } + } + + private static String getTypeForName(String name) + { + final int lastDot = name.lastIndexOf('.'); + if (lastDot >= 0) + { + final String extension = name.substring(lastDot + 1); + + if (extension.equals("hxc") || extension.equals("hx")) + return "application/*"; + + final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + if (mime != null) + { + return mime; + } + } + return "application/octet-stream"; + } + + @Override + public boolean onCreate() + { + rootDir = getContext().getObbDir(); + return true; + } +}