Fixed NPE in DocumentsProvider; Renamed DocumentsProvider to IDEDocumentsProvider

This commit is contained in:
Akash Yadav
2022-04-01 19:47:02 +05:30
parent 9f3ee3d558
commit 5e3291ef0d
7 changed files with 354 additions and 263 deletions
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\itsaky01\.android\avd\Pixel_4_API_31.avd" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-04-01T11:43:50.979181300Z" />
</component>
</project>
+5 -2
View File
@@ -6,6 +6,9 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
@@ -66,8 +69,8 @@
android:windowSoftInputMode="adjustResize" />
<provider
android:name=".filepicker.DocumentsProvider"
android:authorities="com.itsaky.androidide.root"
android:name=".filepicker.IDEDocumentsProvider"
android:authorities="com.itsaky.androidide.documents"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
@@ -1,248 +0,0 @@
@file:Suppress("KDocUnresolvedReference")
package com.itsaky.androidide.filepicker
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
import android.provider.DocumentsProvider
import android.webkit.MimeTypeMap
import com.itsaky.androidide.R
import com.itsaky.androidide.utils.Environment
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.util.*
/**
* 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
*
* @see [TermuxDocumentsProvider](https://github.com/termux/termux-app/blob/master/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java)
*/
class DocumentsProvider : DocumentsProvider() {
override fun queryRoots(projection: Array<String>): Cursor {
val result = MatrixCursor(projection)
val applicationName = context!!.getString(R.string.app_name)
val row = result.newRow()
row.add(DocumentsContract.Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR))
row.add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR))
row.add(DocumentsContract.Root.COLUMN_SUMMARY, null)
row.add(
DocumentsContract.Root.COLUMN_FLAGS,
DocumentsContract.Root.FLAG_SUPPORTS_CREATE
or DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD)
row.add(DocumentsContract.Root.COLUMN_TITLE, applicationName)
row.add(DocumentsContract.Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES)
row.add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.freeSpace)
row.add(DocumentsContract.Root.COLUMN_ICON, R.mipmap.ic_launcher)
return result
}
@Throws(FileNotFoundException::class)
override fun queryDocument(documentId: String, projection: Array<String>): Cursor {
val result = MatrixCursor(projection)
includeFile(result, documentId, null)
return result
}
@Throws(FileNotFoundException::class)
override fun queryChildDocuments(
parentDocumentId: String, projection: Array<String>, sortOrder: String): Cursor {
val result = MatrixCursor(projection)
val parent = getFileForDocId(parentDocumentId)
for (file in parent.listFiles()) {
includeFile(result, null, file)
}
return result
}
@Throws(FileNotFoundException::class)
override fun openDocument(
documentId: String, mode: String, signal: CancellationSignal?): ParcelFileDescriptor {
val file = getFileForDocId(documentId)
val accessMode = ParcelFileDescriptor.parseMode(mode)
return ParcelFileDescriptor.open(file, accessMode)
}
@Throws(FileNotFoundException::class)
override fun openDocumentThumbnail(
documentId: String, sizeHint: Point, signal: CancellationSignal): AssetFileDescriptor {
val file = getFileForDocId(documentId)
val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
return AssetFileDescriptor(pfd, 0, file.length())
}
override fun onCreate(): Boolean {
return true
}
@Throws(FileNotFoundException::class)
override fun createDocument(parentDocumentId: String, mimeType: String, displayName: String): String {
var newFile = File(parentDocumentId, displayName)
var noConflictId = 2
while (newFile.exists()) {
newFile = File(parentDocumentId, displayName + " (" + noConflictId++ + ")")
}
try {
val succeeded: Boolean = if (DocumentsContract.Document.MIME_TYPE_DIR == mimeType) {
newFile.mkdir()
} else {
newFile.createNewFile()
}
if (!succeeded) {
throw FileNotFoundException(
"Failed to create document with id " + newFile.path)
}
} catch (e: IOException) {
throw FileNotFoundException(
"Failed to create document with id " + newFile.path)
}
return newFile.path
}
@Throws(FileNotFoundException::class)
override fun deleteDocument(documentId: String) {
val file = getFileForDocId(documentId)
if (!file.delete()) {
throw FileNotFoundException("Failed to delete document with id $documentId")
}
}
@Throws(FileNotFoundException::class)
override fun getDocumentType(documentId: String): String {
val file = getFileForDocId(documentId)
return getMimeType(file)
}
@Throws(FileNotFoundException::class)
override fun querySearchDocuments(rootId: String, query: String, projection: Array<String>): Cursor {
val result = MatrixCursor(projection)
val 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.
val pending = LinkedList<File>()
pending.add(parent)
val maxSearchResults = 50
while (!pending.isEmpty() && result.count < maxSearchResults) {
val file = pending.removeFirst()
// Avoid directories outside the $HOME directory linked with symlinks (to avoid e.g.
// search
// through the whole SD card).
val isInsideHome: Boolean = try {
file.canonicalPath.startsWith(Environment.ROOT.absolutePath)
} catch (e: IOException) {
true
}
if (isInsideHome) {
if (file.isDirectory) {
Collections.addAll(pending, *file.listFiles())
} else {
if (file.name.lowercase(Locale.ROOT).contains(query)) {
includeFile(result, null, file)
}
}
}
}
return result
}
override fun isChildDocument(parentDocumentId: String, documentId: String): Boolean {
return documentId.startsWith(parentDocumentId)
}
/**
* 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)
*/
@Suppress("NAME_SHADOWING")
@Throws(FileNotFoundException::class)
private fun includeFile(result: MatrixCursor, docId: String?, file: File?) {
var docId = docId
var file = file
if (docId == null) {
docId = getDocIdForFile(file)
} else {
file = getFileForDocId(docId)
}
var flags = 0
if (file!!.isDirectory) {
if (file.canWrite()) flags = flags or DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE
} else if (file.canWrite()) {
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_WRITE
}
if (file.parentFile.canWrite()) flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE
val displayName = file.name
val mimeType = getMimeType(file)
if (mimeType.startsWith("image/")) flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL
val row = result.newRow()
row.add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, docId)
row.add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, displayName)
row.add(DocumentsContract.Document.COLUMN_SIZE, file.length())
row.add(DocumentsContract.Document.COLUMN_MIME_TYPE, mimeType)
row.add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, file.lastModified())
row.add(DocumentsContract.Document.COLUMN_FLAGS, flags)
row.add(DocumentsContract.Document.COLUMN_ICON, R.mipmap.ic_launcher)
}
companion object {
private const val ALL_MIME_TYPES = "*/*"
private val BASE_DIR = Environment.ROOT
/**
* 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 fun getDocIdForFile(file: File?): String {
return file!!.absolutePath
}
/** Get the file given a document id (the reverse of [.getDocIdForFile]). */
@Throws(FileNotFoundException::class)
private fun getFileForDocId(docId: String): File {
val f = File(docId)
if (!f.exists()) throw FileNotFoundException(f.absolutePath + " not found")
return f
}
private fun getMimeType(file: File?): String {
return if (file!!.isDirectory) {
DocumentsContract.Document.MIME_TYPE_DIR
} else {
val name = file.name
val lastDot = name.lastIndexOf('.')
if (lastDot >= 0) {
val extension = name.substring(lastDot + 1).lowercase(Locale.ROOT)
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
if (mime != null) return mime
}
"application/octet-stream"
}
}
}
}
@@ -0,0 +1,326 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.filepicker;
import static com.itsaky.androidide.utils.Logger.instance;
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.NonNull;
import com.itsaky.androidide.R;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.Logger;
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.
*
* <p>Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
*
* <p>"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
*
* @see <a
* href="https://github.com/termux/termux-app/blob/master/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java">TermuxDocumentsProvider</a>
*/
public class IDEDocumentsProvider extends DocumentsProvider {
private static final String ALL_MIME_TYPES = "*/*";
private static final Logger LOG = instance("IDEDocumentsProvider");
private static final File BASE_DIR = getBaseDir();
// 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
};
@NonNull
private static File getBaseDir() {
if (Environment.HOME != null) {
return Environment.HOME;
}
return new File(Environment.DEFAULT_HOME);
}
/**
* 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.
*
* <p>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";
}
}
@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_name);
final MatrixCursor.RowBuilder row = result.newRow();
LOG.debug("queryRoots() before all add");
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
LOG.debug("queryRoots() before all add, 1");
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
LOG.debug("queryRoots() before all add, 2");
row.add(Root.COLUMN_SUMMARY, null);
LOG.debug("queryRoots() before all add, 3");
row.add(
Root.COLUMN_FLAGS,
Root.FLAG_SUPPORTS_CREATE
| Root.FLAG_SUPPORTS_SEARCH
| Root.FLAG_SUPPORTS_IS_CHILD);
LOG.debug("queryRoots() before all add, 4");
row.add(Root.COLUMN_TITLE, applicationName);
LOG.debug("queryRoots() before all add, 5");
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
LOG.debug("queryRoots() before all add, 6");
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
LOG.debug("queryRoots() before all add, 7");
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
LOG.debug("queryRoots() before all add");
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.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<File> 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(Environment.HOME.getAbsolutePath());
} 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);
}
/**
* 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);
}
}
@@ -24,13 +24,11 @@ import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.ChecksSdkIntAtLeast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationManagerCompat;
import androidx.multidex.MultiDexApplication;
import com.blankj.utilcode.util.ThrowableUtils;
import com.itsaky.androidide.common.R;
import com.itsaky.androidide.managers.PreferenceManager;
@@ -41,11 +39,9 @@ import com.itsaky.androidide.utils.FileUtil;
import com.itsaky.androidide.utils.JavaCharacter;
import com.itsaky.androidide.utils.StudioUtils;
import com.itsaky.toaster.Toaster;
import org.jetbrains.annotations.Contract;
import java.io.File;
import java.util.Arrays;
import org.jetbrains.annotations.Contract;
public abstract class BaseApplication extends MultiDexApplication {
@@ -18,7 +18,6 @@
package com.itsaky.androidide.managers;
import androidx.annotation.NonNull;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.FileUtils;
import com.blankj.utilcode.util.ResourceUtils;
@@ -26,11 +25,9 @@ import com.blankj.utilcode.util.ZipUtils;
import com.itsaky.androidide.app.BaseApplication;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.Logger;
import org.jetbrains.annotations.Contract;
import java.io.File;
import java.io.IOException;
import org.jetbrains.annotations.Contract;
public class ToolsManager {
@@ -36,10 +36,10 @@ public final class Environment {
public static final Map<String, String> IDE_PROPS = new HashMap<>();
public static final Map<String, String> ENV_VARS = new HashMap<>();
public static final String PROJECTS_FOLDER = "AndroidIDEProjects";
private static final String DEFAULT_JAVA_HOME =
"/data/data/com.itsaky.androidide/files/framework/jdk";
private static final String DEFAULT_ANDROID_HOME =
"/data/data/com.itsaky.androidide/files/framework/android-sdk";
public static final String DEFAULT_ROOT = "/data/data/com.itsaky.androidide/files";
public static final String DEFAULT_HOME = DEFAULT_ROOT + "/framework";
private static final String DEFAULT_JAVA_HOME = DEFAULT_HOME + "/jdk";
private static final String DEFAULT_ANDROID_HOME = DEFAULT_HOME + "/android-sdk";
private static final Logger LOG = Logger.instance("Environment");
private static final List<String> blacklist = new ArrayList<>();
public static File ROOT;