diff --git a/FCL/src/main/AndroidManifest.xml b/FCL/src/main/AndroidManifest.xml
index 4ae00910..42d11fc7 100644
--- a/FCL/src/main/AndroidManifest.xml
+++ b/FCL/src/main/AndroidManifest.xml
@@ -117,13 +117,17 @@
android:process=":processService"
android:enabled="true"/>
+ android:exported="true"
+ android:grantUriPermissions="true"
+ android:permission="android.permission.MANAGE_DOCUMENTS">
+
+
+
diff --git a/FCL/src/main/java/com/tungsten/fcl/scoped/FolderProvider.java b/FCL/src/main/java/com/tungsten/fcl/scoped/FolderProvider.java
new file mode 100644
index 00000000..fff220fc
--- /dev/null
+++ b/FCL/src/main/java/com/tungsten/fcl/scoped/FolderProvider.java
@@ -0,0 +1,332 @@
+package com.tungsten.fcl.scoped;
+
+import android.annotation.TargetApi;
+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.DocumentsContract.Document;
+import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsProvider;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import androidx.annotation.Nullable;
+
+import com.tungsten.fcl.R;
+import com.tungsten.fclcore.util.io.FileUtils;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * 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."
+ * - ...
+ */
+public class FolderProvider extends DocumentsProvider {
+
+ private static final String ALL_MIME_TYPES = "*/*";
+
+ // 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_name);
+ final File BASE_DIR = getContext().getFilesDir().getParentFile();
+
+ 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);
+ final File[] children = parent.listFiles();
+ if(children == null) throw new FileNotFoundException("Unable to list files in "+parent.getAbsolutePath());
+ for (File file : children) {
+ 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 String renameDocument(String documentId, String displayName) throws FileNotFoundException {
+ File sourceFile = getFileForDocId(documentId);
+ File sourceParent = sourceFile.getParentFile();
+ if(sourceParent == null) throw new FileNotFoundException("Cannot rename root");
+ File targetFile = new File(getDocIdForFile(sourceParent) + "/" + displayName);
+ if(!sourceFile.renameTo(targetFile)){
+ throw new FileNotFoundException("Couldn't rename the document with id" + documentId);
+ }
+ return getDocIdForFile(targetFile);
+ }
+
+ @Override
+ public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId) throws FileNotFoundException {
+ File sourceFile = getFileForDocId(sourceParentDocumentId + sourceDocumentId);
+ File targetFile = new File(targetParentDocumentId + sourceDocumentId);
+ if(!sourceFile.renameTo(targetFile)){
+ throw new FileNotFoundException("Failed to move the document with id " + sourceFile.getPath());
+ }
+ return getDocIdForFile(targetFile);
+ }
+
+ @Override
+ public void removeDocument(String documentId, String parentDocumentId) throws FileNotFoundException {
+ deleteDocument(parentDocumentId + "/" + documentId);
+ }
+
+ @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 {
+ Log.i("FolderPRovider", "getDocumentType("+documentId+")");
+ 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 {
+ final File BASE_DIR = getContext().getFilesDir().getParentFile();
+ isInsideHome = file.getCanonicalPath().startsWith(BASE_DIR.getAbsolutePath());
+ } catch (IOException e) {
+ isInsideHome = true;
+ }
+ if (isInsideHome) {
+ if (file.isDirectory()) {
+ File[] listing = file.listFiles();
+ if(listing != null) Collections.addAll(pending, listing);
+ } 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;
+ }
+ File parent = file.getParentFile();
+ if(parent != null) { // Only fails in one case: when the parent is /, which you can't delete.
+ if(parent.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);
+ }
+
+ @Override
+ @TargetApi(26)
+ public DocumentsContract.Path findDocumentPath(@Nullable String parentDocumentId, String childDocumentId) throws FileNotFoundException {
+ File source = getContext().getFilesDir().getParentFile();;
+ if(parentDocumentId != null) source = getFileForDocId(parentDocumentId);
+ File destination = getFileForDocId(childDocumentId);
+ List pathIds = new ArrayList<>();
+ while(!source.equals(destination) && destination != null) {
+ pathIds.add(getDocIdForFile(destination));
+ destination = destination.getParentFile();
+ }
+ pathIds.add(getDocIdForFile(source));
+ Collections.reverse(pathIds);
+ Log.i("FolderProvider", pathIds.toString());
+ return new DocumentsContract.Path(getDocIdForFile(source), pathIds);
+ }
+}
\ No newline at end of file