diff --git a/termux-app/src/main/java/com/termux/app/TermuxDocumentsProvider.java b/termux-app/src/main/java/com/termux/app/TermuxDocumentsProvider.java
index 299a080b..5efb2a26 100644
--- a/termux-app/src/main/java/com/termux/app/TermuxDocumentsProvider.java
+++ b/termux-app/src/main/java/com/termux/app/TermuxDocumentsProvider.java
@@ -6,9 +6,11 @@
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 com.termux.R;
@@ -16,9 +18,14 @@
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.Collections;
+import java.util.Comparator;
import java.util.LinkedList;
+import java.util.List;
import java.util.Locale;
+import java.util.stream.Collectors;
/**
* A document provider for the Storage Access Framework which exposes the files in the
@@ -29,7 +36,7 @@
* "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
+ * - Source
*/
public class TermuxDocumentsProvider extends DocumentsProvider {
@@ -62,36 +69,74 @@ public class TermuxDocumentsProvider extends DocumentsProvider {
Document.COLUMN_SIZE
};
+ private void setNotificationUri(Cursor cursor) {
+ var context = getContext();
+ if (context != null) {
+ var baseUri = DocumentsContract.buildChildDocumentsUri(TermuxContentProvider.URI_AUTHORITY, BASE_DIR.getAbsolutePath());
+ cursor.setNotificationUri(context.getContentResolver(), baseUri);
+ }
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
@Override
public Cursor queryRoots(String[] projection) {
- final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
- final String applicationName = getContext().getString(R.string.application_name);
+ var result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
+ var applicationName = getContext().getString(R.string.application_name);
- final MatrixCursor.RowBuilder row = result.newRow();
+ var row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
+ row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
+ row.add(Root.COLUMN_TITLE, applicationName);
+ row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_RECENTS);
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);
+ var result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
+ setNotificationUri(result);
includeFile(result, documentId, null);
return result;
}
+ @Override
+ public Cursor queryRecentDocuments(String rootId, String[] projection) throws FileNotFoundException {
+ var dir = getFileForDocId(rootId);
+ try (var walk = Files.walk(dir.toPath())) {
+ var iterator = walk
+ .filter(f -> f.toFile().isFile())
+ .sorted(Comparator.comparingLong((Path a) -> a.toFile().lastModified()).reversed())
+ .limit(64)
+ .iterator();
+
+ var result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
+ setNotificationUri(result);
+ while (iterator.hasNext()) {
+ includeFile(result, null, iterator.next().toFile());
+ }
+ return result;
+ } catch (IOException e) {
+ throw handleIOExceptionFromFilesWalk(e, dir);
+ }
+ }
+
@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);
+ var result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
+ setNotificationUri(result);
+ var files = getFileForDocId(parentDocumentId).listFiles();
+ if (files != null) {
+ for (var file : files) {
+ includeFile(result, null, file);
+ }
}
return result;
}
@@ -110,11 +155,6 @@ public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHi
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);
@@ -135,14 +175,42 @@ public String createDocument(String parentDocumentId, String mimeType, String di
} catch (IOException e) {
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
+ notifyFileChange();
return newFile.getPath();
}
+ @Override
+ public String renameDocument(String documentId, String displayName) throws FileNotFoundException {
+ var oldFile = getFileForDocId(documentId);
+ var newFile = new File(oldFile.getParent(), displayName);
+ if (newFile.exists()) {
+ throw new FileNotFoundException("File already exists: " + displayName);
+ }
+ var pathsToInvalidate = findAllPathsIn(oldFile);
+ if (!oldFile.renameTo(newFile)) {
+ throw new FileNotFoundException("Unable to rename " + documentId);
+ }
+ revokeDocumentsPermission(pathsToInvalidate);
+ return getDocIdForFile(newFile);
+ }
+
@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);
+ var file = getFileForDocId(documentId);
+ try (var walk = Files.walk(file.toPath())) {
+ var iterator = walk.sorted(Comparator.reverseOrder())
+ .map(Path::toFile)
+ .iterator();
+ while (iterator.hasNext()) {
+ var f = iterator.next();
+ if (!f.delete()) {
+ throw new FileNotFoundException("Cannot delete: " + f.getAbsolutePath());
+ }
+ revokeDocumentPermission(getDocIdForFile(f));
+ }
+ notifyFileChange();
+ } catch (IOException e) {
+ throw handleIOExceptionFromFilesWalk(e, file);
}
}
@@ -152,10 +220,27 @@ public String getDocumentType(String documentId) throws FileNotFoundException {
return getMimeType(file);
}
+ @Override
+ public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId) throws FileNotFoundException {
+ var srcFile = getFileForDocId(sourceDocumentId);
+ var destFile = new File(getFileForDocId(targetParentDocumentId), srcFile.getName());
+ if (destFile.exists()) {
+ throw new FileNotFoundException("File already exists: " + destFile);
+ }
+ var pathsToInvalidate = findAllPathsIn(srcFile);
+ if (!srcFile.renameTo(destFile)) {
+ throw new FileNotFoundException("Cannot rename " + srcFile.getAbsolutePath() + " to " + destFile.getAbsolutePath());
+ }
+ revokeDocumentsPermission(pathsToInvalidate);
+ return getDocIdForFile(destFile);
+ }
+
+
@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);
+ var result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
+ setNotificationUri(result);
+ var 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
@@ -255,7 +340,7 @@ private void includeFile(MatrixCursor result, String docId, File file)
final String mimeType = getMimeType(file);
if (mimeType.startsWith("image/")) flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
- final MatrixCursor.RowBuilder row = result.newRow();
+ var row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
row.add(Document.COLUMN_SIZE, file.length());
@@ -265,4 +350,35 @@ private void includeFile(MatrixCursor result, String docId, File file)
row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher);
}
+ private static List findAllPathsIn(File fileOrDirectory) throws FileNotFoundException {
+ try (var walk = Files.walk(fileOrDirectory.toPath())) {
+ return walk.map(f -> f.toAbsolutePath().toString()).collect(Collectors.toList());
+ } catch (IOException e) {
+ throw handleIOExceptionFromFilesWalk(e, fileOrDirectory);
+ }
+ }
+
+ private static FileNotFoundException handleIOExceptionFromFilesWalk(IOException e, File walked) {
+ var errorMessage = "Error walking: " + walked.getAbsolutePath();
+ Log.e(TermuxConstants.LOG_TAG, errorMessage, e);
+ return new FileNotFoundException(errorMessage);
+ }
+
+ private void revokeDocumentsPermission(List paths) {
+ for (var path : paths) {
+ Log.e(TermuxConstants.LOG_TAG, "Revoking: " + path);
+
+ revokeDocumentPermission(path);
+ }
+ notifyFileChange();
+ }
+
+ private void notifyFileChange() {
+ var updatedUri = DocumentsContract.buildChildDocumentsUri(TermuxContentProvider.URI_AUTHORITY, BASE_DIR.getAbsolutePath());
+ var context = getContext();
+ if (context != null) {
+ context.getContentResolver().notifyChange(updatedUri, null);
+ }
+ }
+
}