package com.termux.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.Document; import android.provider.DocumentsContract.Root; import android.provider.DocumentsProvider; import android.webkit.MimeTypeMap; import com.termux.R; import com.termux.shared.termux.TermuxConstants; 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. *
* 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 */ public class TermuxDocumentsProvider extends DocumentsProvider { private static final String ALL_MIME_TYPES = "*/*"; private static final File BASE_DIR = TermuxConstants.TERMUX_HOME_DIR; // 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.application_name); 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); 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