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