RetroDocumentsProvider.java 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. // Contains GPLv3-licensed code from the Termux project.
  2. // https://github.com/termux/termux-app/blob/master/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java
  3. package com.retroarch.browser.provider;
  4. import android.annotation.TargetApi;
  5. import android.content.res.AssetFileDescriptor;
  6. import android.database.Cursor;
  7. import android.database.MatrixCursor;
  8. import android.graphics.Point;
  9. import android.os.Build;
  10. import android.os.CancellationSignal;
  11. import android.os.ParcelFileDescriptor;
  12. import android.provider.DocumentsContract.Document;
  13. import android.provider.DocumentsContract.Root;
  14. import android.provider.DocumentsProvider;
  15. import android.webkit.MimeTypeMap;
  16. import com.xugame.gameconsole.R;
  17. import java.io.File;
  18. import java.io.FileNotFoundException;
  19. import java.io.IOException;
  20. import java.util.Collections;
  21. import java.util.LinkedList;
  22. /**
  23. * A document provider for the Storage Access Framework which exposes the files in the
  24. * $HOME/ folder to other apps.
  25. * <p/>
  26. * Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
  27. * <p/>
  28. * "A document provider and ACTION_GET_CONTENT should be considered mutually exclusive. If you
  29. * support both of them simultaneously, your app will appear twice in the system picker UI,
  30. * offering two different ways of accessing your stored data. This would be confusing for users."
  31. * - http://developer.android.com/guide/topics/providers/document-provider.html#43
  32. */
  33. @TargetApi(Build.VERSION_CODES.KITKAT)
  34. public class RetroDocumentsProvider extends DocumentsProvider {
  35. private static final String ALL_MIME_TYPES = "*/*";
  36. // The default columns to return information about a root if no specific
  37. // columns are requested in a query.
  38. private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{
  39. Root.COLUMN_ROOT_ID,
  40. Root.COLUMN_MIME_TYPES,
  41. Root.COLUMN_FLAGS,
  42. Root.COLUMN_ICON,
  43. Root.COLUMN_TITLE,
  44. Root.COLUMN_SUMMARY,
  45. Root.COLUMN_DOCUMENT_ID,
  46. Root.COLUMN_AVAILABLE_BYTES
  47. };
  48. // The default columns to return information about a document if no specific
  49. // columns are requested in a query.
  50. private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{
  51. Document.COLUMN_DOCUMENT_ID,
  52. Document.COLUMN_MIME_TYPE,
  53. Document.COLUMN_DISPLAY_NAME,
  54. Document.COLUMN_LAST_MODIFIED,
  55. Document.COLUMN_FLAGS,
  56. Document.COLUMN_SIZE
  57. };
  58. @Override
  59. public Cursor queryRoots(String[] projection) throws FileNotFoundException {
  60. final File BASE_DIR = new File(getContext().getFilesDir().getParent());
  61. final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
  62. @SuppressWarnings("ConstantConditions") final String applicationName = getContext().getString(R.string.app_name);
  63. final MatrixCursor.RowBuilder row = result.newRow();
  64. row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
  65. row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
  66. row.add(Root.COLUMN_SUMMARY, null);
  67. row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD);
  68. row.add(Root.COLUMN_TITLE, applicationName);
  69. row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
  70. row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
  71. row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
  72. return result;
  73. }
  74. @Override
  75. public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
  76. final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
  77. includeFile(result, documentId, null);
  78. return result;
  79. }
  80. @Override
  81. public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder) throws FileNotFoundException {
  82. final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
  83. final File parent = getFileForDocId(parentDocumentId);
  84. for (File file : parent.listFiles()) {
  85. includeFile(result, null, file);
  86. }
  87. return result;
  88. }
  89. @Override
  90. public ParcelFileDescriptor openDocument(final String documentId, String mode, CancellationSignal signal) throws FileNotFoundException {
  91. final File file = getFileForDocId(documentId);
  92. final int accessMode = ParcelFileDescriptor.parseMode(mode);
  93. return ParcelFileDescriptor.open(file, accessMode);
  94. }
  95. @Override
  96. public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
  97. final File file = getFileForDocId(documentId);
  98. final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
  99. return new AssetFileDescriptor(pfd, 0, file.length());
  100. }
  101. @Override
  102. public boolean onCreate() {
  103. return true;
  104. }
  105. @Override
  106. public String createDocument(String parentDocumentId, String mimeType, String displayName) throws FileNotFoundException {
  107. File newFile = new File(parentDocumentId, displayName);
  108. int noConflictId = 2;
  109. while (newFile.exists()) {
  110. newFile = new File(parentDocumentId, displayName + " (" + noConflictId++ + ")");
  111. }
  112. try {
  113. boolean succeeded;
  114. if (Document.MIME_TYPE_DIR.equals(mimeType)) {
  115. succeeded = newFile.mkdir();
  116. } else {
  117. succeeded = newFile.createNewFile();
  118. }
  119. if (!succeeded) {
  120. throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
  121. }
  122. } catch (IOException e) {
  123. throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
  124. }
  125. return newFile.getPath();
  126. }
  127. @Override
  128. public void deleteDocument(String documentId) throws FileNotFoundException {
  129. File file = getFileForDocId(documentId);
  130. if (!file.delete()) {
  131. throw new FileNotFoundException("Failed to delete document with id " + documentId);
  132. }
  133. }
  134. @Override
  135. public String getDocumentType(String documentId) throws FileNotFoundException {
  136. File file = getFileForDocId(documentId);
  137. return getMimeType(file);
  138. }
  139. @Override
  140. public Cursor querySearchDocuments(String rootId, String query, String[] projection) throws FileNotFoundException {
  141. final MatrixCursor result = new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
  142. final File parent = getFileForDocId(rootId);
  143. // This example implementation searches file names for the query and doesn't rank search
  144. // results, so we can stop as soon as we find a sufficient number of matches. Other
  145. // implementations might rank results and use other data about files, rather than the file
  146. // name, to produce a match.
  147. final LinkedList<File> pending = new LinkedList<>();
  148. pending.add(parent);
  149. final int MAX_SEARCH_RESULTS = 50;
  150. while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
  151. final File file = pending.removeFirst();
  152. // Avoid folders outside the $HOME folders linked in to symlinks (to avoid e.g. search
  153. // through the whole SD card).
  154. boolean isInsideHome;
  155. try {
  156. isInsideHome = file.getCanonicalPath().startsWith(getContext().getFilesDir().getParent());
  157. } catch (IOException e) {
  158. isInsideHome = true;
  159. }
  160. if (isInsideHome) {
  161. if (file.isDirectory()) {
  162. Collections.addAll(pending, file.listFiles());
  163. } else {
  164. if (file.getName().toLowerCase().contains(query)) {
  165. includeFile(result, null, file);
  166. }
  167. }
  168. }
  169. }
  170. return result;
  171. }
  172. @Override
  173. public boolean isChildDocument(String parentDocumentId, String documentId) {
  174. return documentId.startsWith(parentDocumentId);
  175. }
  176. /**
  177. * Get the document id given a file. This document id must be consistent across time as other
  178. * applications may save the ID and use it to reference documents later.
  179. * <p/>
  180. * The reverse of @{link #getFileForDocId}.
  181. */
  182. private static String getDocIdForFile(File file) {
  183. return file.getAbsolutePath();
  184. }
  185. /**
  186. * Get the file given a document id (the reverse of {@link #getDocIdForFile(File)}).
  187. */
  188. private static File getFileForDocId(String docId) throws FileNotFoundException {
  189. final File f = new File(docId);
  190. if (!f.exists()) throw new FileNotFoundException(f.getAbsolutePath() + " not found");
  191. return f;
  192. }
  193. private static String getMimeType(File file) {
  194. if (file.isDirectory()) {
  195. return Document.MIME_TYPE_DIR;
  196. } else {
  197. final String name = file.getName();
  198. final int lastDot = name.lastIndexOf('.');
  199. if (lastDot >= 0) {
  200. final String extension = name.substring(lastDot + 1).toLowerCase();
  201. final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
  202. if (mime != null) return mime;
  203. }
  204. return "application/octet-stream";
  205. }
  206. }
  207. /**
  208. * Add a representation of a file to a cursor.
  209. *
  210. * @param result the cursor to modify
  211. * @param docId the document ID representing the desired file (may be null if given file)
  212. * @param file the File object representing the desired file (may be null if given docID)
  213. */
  214. private void includeFile(MatrixCursor result, String docId, File file)
  215. throws FileNotFoundException {
  216. if (docId == null) {
  217. docId = getDocIdForFile(file);
  218. } else {
  219. file = getFileForDocId(docId);
  220. }
  221. int flags = 0;
  222. if (file.isDirectory()) {
  223. if (file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
  224. } else if (file.canWrite()) {
  225. flags |= Document.FLAG_SUPPORTS_WRITE;
  226. }
  227. if (file.getParentFile().canWrite()) flags |= Document.FLAG_SUPPORTS_DELETE;
  228. final String displayName = file.getName();
  229. final String mimeType = getMimeType(file);
  230. if (mimeType.startsWith("image/")) flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
  231. final MatrixCursor.RowBuilder row = result.newRow();
  232. row.add(Document.COLUMN_DOCUMENT_ID, docId);
  233. row.add(Document.COLUMN_DISPLAY_NAME, displayName);
  234. row.add(Document.COLUMN_SIZE, file.length());
  235. row.add(Document.COLUMN_MIME_TYPE, mimeType);
  236. row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
  237. row.add(Document.COLUMN_FLAGS, flags);
  238. row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher);
  239. }
  240. }