LoveDocumentsProvider.java 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. /*
  2. * Copyright (C) 2013 The Android Open Source Project
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. /*
  17. * Modified for use with LOVE by the LOVE Development Team
  18. * Copyright (C) 2022 LOVE Development Team
  19. */
  20. package org.love2d.android;
  21. import static android.os.Build.VERSION.SDK_INT;
  22. import android.content.res.AssetFileDescriptor;
  23. import android.database.Cursor;
  24. import android.database.MatrixCursor;
  25. import android.graphics.Point;
  26. import android.os.Build;
  27. import android.os.CancellationSignal;
  28. import android.os.Handler;
  29. import android.os.ParcelFileDescriptor;
  30. import android.provider.DocumentsContract.Document;
  31. import android.provider.DocumentsContract.Root;
  32. import android.provider.DocumentsProvider;
  33. import android.util.Log;
  34. import android.webkit.MimeTypeMap;
  35. import java.io.File;
  36. import java.io.FileInputStream;
  37. import java.io.FileNotFoundException;
  38. import java.io.FileOutputStream;
  39. import java.io.IOException;
  40. import java.io.InputStream;
  41. import java.io.OutputStream;
  42. import java.util.Collections;
  43. import java.util.Comparator;
  44. import java.util.HashSet;
  45. import java.util.LinkedList;
  46. import java.util.PriorityQueue;
  47. import java.util.Set;
  48. /**
  49. * Manages documents and exposes them to the Android system for sharing.
  50. */
  51. public class LoveDocumentsProvider extends DocumentsProvider {
  52. private static final String TAG = "LoveDocumentsProvider";
  53. // Use these as the default columns to return information about a root if no specific
  54. // columns are requested in a query.
  55. private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{
  56. Root.COLUMN_ROOT_ID,
  57. Root.COLUMN_MIME_TYPES,
  58. Root.COLUMN_FLAGS,
  59. Root.COLUMN_ICON,
  60. Root.COLUMN_TITLE,
  61. Root.COLUMN_SUMMARY,
  62. Root.COLUMN_DOCUMENT_ID,
  63. Root.COLUMN_AVAILABLE_BYTES
  64. };
  65. // Use these as the default columns to return information about a document if no specific
  66. // columns are requested in a query.
  67. private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{
  68. Document.COLUMN_DOCUMENT_ID,
  69. Document.COLUMN_MIME_TYPE,
  70. Document.COLUMN_DISPLAY_NAME,
  71. Document.COLUMN_LAST_MODIFIED,
  72. Document.COLUMN_FLAGS,
  73. Document.COLUMN_SIZE
  74. };
  75. // No official policy on how many to return, but make sure you do limit the number of recent
  76. // and search results.
  77. private static final int MAX_SEARCH_RESULTS = 20;
  78. private static final int MAX_LAST_MODIFIED = 5;
  79. private static final String ROOT = "root";
  80. // This file is the base of our root
  81. private File mBaseDir;
  82. @Override
  83. public boolean onCreate() {
  84. Log.v(TAG, "onCreate");
  85. mBaseDir = getContext().getExternalFilesDir(null);
  86. return true;
  87. }
  88. @Override
  89. public Cursor queryRoots(String[] projection) throws FileNotFoundException {
  90. Log.v(TAG, "queryRoots");
  91. // Create a cursor with either the requested fields, or the default projection. This
  92. // cursor is returned to the Android system picker UI and used to display all roots from
  93. // this provider.
  94. final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
  95. // It's possible to have multiple roots
  96. final MatrixCursor.RowBuilder row = result.newRow();
  97. row.add(Root.COLUMN_ROOT_ID, ROOT);
  98. row.add(Root.COLUMN_SUMMARY, null);
  99. // FLAG_SUPPORTS_CREATE means at least one directory under the root supports creating
  100. // documents. FLAG_SUPPORTS_RECENTS means your application's most recently used
  101. // documents will show up in the "Recents" category. FLAG_SUPPORTS_SEARCH allows users
  102. // to search all documents the application shares. FLAG_SUPPORTS_IS_CHILD allows
  103. // testing parent child relationships, available after SDK 21 (Lollipop).
  104. if (SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
  105. row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
  106. Root.FLAG_SUPPORTS_RECENTS |
  107. Root.FLAG_SUPPORTS_SEARCH);
  108. } else {
  109. row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
  110. Root.FLAG_SUPPORTS_RECENTS |
  111. Root.FLAG_SUPPORTS_SEARCH |
  112. Root.FLAG_SUPPORTS_IS_CHILD);
  113. }
  114. // COLUMN_TITLE is the root title (e.g. what will be displayed to identify your provider).
  115. row.add(Root.COLUMN_TITLE, getContext().getApplicationInfo().loadLabel(getContext().getPackageManager()).toString());
  116. // This document id must be unique within this provider and consistent across time. The
  117. // system picker UI may save it and refer to it later.
  118. row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir));
  119. // The child MIME types are used to filter the roots and only present to the user roots
  120. // that contain the desired type somewhere in their file hierarchy.
  121. row.add(Root.COLUMN_MIME_TYPES, "*/*");
  122. row.add(Root.COLUMN_AVAILABLE_BYTES, mBaseDir.getFreeSpace());
  123. row.add(Root.COLUMN_ICON, R.drawable.love);
  124. return result;
  125. }
  126. @Override
  127. public Cursor queryRecentDocuments(String rootId, String[] projection)
  128. throws FileNotFoundException {
  129. Log.v(TAG, "queryRecentDocuments");
  130. // This implementation walks a local file structure to find the most recently
  131. // modified files.
  132. // Create a cursor with the requested projection, or the default projection.
  133. final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
  134. final File parent = getFileForDocId(rootId);
  135. // Create a queue to store the most recent documents, which orders by last modified.
  136. PriorityQueue<File> lastModifiedFiles = new PriorityQueue<File>(5, new Comparator<File>() {
  137. public int compare(File i, File j) {
  138. return Long.compare(i.lastModified(), j.lastModified());
  139. }
  140. });
  141. // Iterate through all files and directories in the file structure under the root. If
  142. // the file is more recent than the least recently modified, add it to the queue,
  143. // limiting the number of results.
  144. final LinkedList<File> pending = new LinkedList<File>();
  145. // Start by adding the parent to the list of files to be processed
  146. pending.add(parent);
  147. // Do while we still have unexamined files
  148. while (!pending.isEmpty()) {
  149. // Take a file from the list of unprocessed files
  150. final File file = pending.removeFirst();
  151. if (file.isDirectory()) {
  152. // If it's a directory, add all its children to the unprocessed list
  153. Collections.addAll(pending, file.listFiles());
  154. } else {
  155. // If it's a file, add it to the ordered queue.
  156. lastModifiedFiles.add(file);
  157. }
  158. }
  159. // Add the most recent files to the cursor, not exceeding the max number of results.
  160. int includedCount = 0;
  161. while (includedCount < MAX_LAST_MODIFIED + 1 && !lastModifiedFiles.isEmpty()) {
  162. final File file = lastModifiedFiles.remove();
  163. includeFile(result, null, file);
  164. includedCount++;
  165. }
  166. return result;
  167. }
  168. @Override
  169. public Cursor querySearchDocuments(String rootId, String query, String[] projection)
  170. throws FileNotFoundException {
  171. Log.v(TAG, "querySearchDocuments");
  172. // Create a cursor with the requested projection, or the default projection.
  173. final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
  174. final File parent = getFileForDocId(rootId);
  175. // This implementation searches file names for the query and doesn't rank search
  176. // results, so we can stop as soon as we find a sufficient number of matches. Other
  177. // implementations might use other data about files, rather than the file name, to
  178. // produce a match;
  179. // Iterate through all files in the file structure under the root until we reach the
  180. // desired number of matches.
  181. final LinkedList<File> pending = new LinkedList<File>();
  182. // Start by adding the parent to the list of files to be processed
  183. pending.add(parent);
  184. // Do while we still have unexamined files, and fewer than the max search results
  185. while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
  186. // Take a file from the list of unprocessed files
  187. final File file = pending.removeFirst();
  188. if (file.isDirectory()) {
  189. // If it's a directory, add all its children to the unprocessed list
  190. Collections.addAll(pending, file.listFiles());
  191. } else {
  192. // If it's a file and it matches, add it to the result cursor.
  193. if (file.getName().toLowerCase().contains(query)) {
  194. includeFile(result, null, file);
  195. }
  196. }
  197. }
  198. return result;
  199. }
  200. @Override
  201. public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint,
  202. CancellationSignal signal)
  203. throws FileNotFoundException {
  204. Log.v(TAG, "openDocumentThumbnail");
  205. final File file = getFileForDocId(documentId);
  206. final ParcelFileDescriptor pfd =
  207. ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
  208. return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
  209. }
  210. @Override
  211. public Cursor queryDocument(String documentId, String[] projection)
  212. throws FileNotFoundException {
  213. Log.v(TAG, "queryDocument");
  214. // Create a cursor with the requested projection, or the default projection.
  215. final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
  216. includeFile(result, documentId, null);
  217. return result;
  218. }
  219. @Override
  220. public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
  221. String sortOrder) throws FileNotFoundException {
  222. Log.v(TAG, "queryChildDocuments, parentDocumentId: " +
  223. parentDocumentId +
  224. " sortOrder: " +
  225. sortOrder);
  226. final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
  227. final File parent = getFileForDocId(parentDocumentId);
  228. for (File file : parent.listFiles()) {
  229. includeFile(result, null, file);
  230. }
  231. return result;
  232. }
  233. @Override
  234. public ParcelFileDescriptor openDocument(final String documentId, final String mode, CancellationSignal signal) throws FileNotFoundException {
  235. Log.v(TAG, "openDocument, mode: " + mode);
  236. final File file = getFileForDocId(documentId);
  237. final int accessMode = ParcelFileDescriptor.parseMode(mode);
  238. return ParcelFileDescriptor.open(file, accessMode);
  239. }
  240. public boolean isChildFile(File parentFile, File childFile) {
  241. File realFileParent = childFile.getParentFile();
  242. return realFileParent == null || realFileParent.equals(parentFile);
  243. }
  244. @Override
  245. public boolean isChildDocument(String parentDocumentId, String documentId) {
  246. Log.v(TAG, "isChildDocument");
  247. if(documentId.startsWith(parentDocumentId)) {
  248. return true;
  249. } else {
  250. return false;
  251. }
  252. }
  253. @Override
  254. public String createDocument(String documentId, String mimeType, String displayName)
  255. throws FileNotFoundException {
  256. Log.v(TAG, "createDocument");
  257. File parent = getFileForDocId(documentId);
  258. File file = new File(parent.getPath(), displayName);
  259. int conflictId = 1;
  260. while(file.exists()) {
  261. file = new File(parent.getPath(), displayName + "(" + conflictId++ + ")");
  262. }
  263. try {
  264. // Create the new File to copy into
  265. boolean wasNewFileCreated = false;
  266. if (Document.MIME_TYPE_DIR.equals(mimeType)) {
  267. wasNewFileCreated = file.mkdir();
  268. } else {
  269. wasNewFileCreated = file.createNewFile();
  270. }
  271. if (!wasNewFileCreated) {
  272. throw new FileNotFoundException("Failed to create document with name " +
  273. displayName + " and documentId " + documentId);
  274. }
  275. } catch (IOException e) {
  276. throw new FileNotFoundException("Failed to create document with name " +
  277. displayName + " and documentId " + documentId);
  278. }
  279. return getDocIdForFile(file);
  280. }
  281. @Override
  282. public String renameDocument(String documentId, String displayName)
  283. throws FileNotFoundException {
  284. Log.v(TAG, "renameDocument");
  285. if (displayName == null) {
  286. throw new FileNotFoundException("Failed to rename document, new name is null");
  287. }
  288. // Create the destination file in the same directory as the source file
  289. File sourceFile = getFileForDocId(documentId);
  290. File sourceParentFile = sourceFile.getParentFile();
  291. if (sourceParentFile == null) {
  292. throw new FileNotFoundException("Failed to rename document. File has no parent.");
  293. }
  294. File destFile = new File(sourceParentFile.getPath(), displayName);
  295. // Try to do the rename
  296. try {
  297. boolean renameSucceeded = sourceFile.renameTo(destFile);
  298. if (!renameSucceeded) {
  299. throw new FileNotFoundException("Failed to rename document. Renamed failed.");
  300. }
  301. } catch (Exception e) {
  302. Log.w(TAG, "Rename exception : " + e.getLocalizedMessage() + e.getCause());
  303. throw new FileNotFoundException("Failed to rename document. Error: " + e.getMessage());
  304. }
  305. return getDocIdForFile(destFile);
  306. }
  307. @Override
  308. public void deleteDocument(String documentId) throws FileNotFoundException {
  309. Log.v(TAG, "deleteDocument");
  310. File file = getFileForDocId(documentId);
  311. if (file.delete()) {
  312. Log.i(TAG, "Deleted file with id " + documentId);
  313. } else {
  314. throw new FileNotFoundException("Failed to delete document with id " + documentId);
  315. }
  316. }
  317. @Override
  318. public void removeDocument(String documentId, String parentDocumentId)
  319. throws FileNotFoundException {
  320. Log.v(TAG, "removeDocument");
  321. File parent = getFileForDocId(parentDocumentId);
  322. File file = getFileForDocId(documentId);
  323. if (file == null) {
  324. throw new FileNotFoundException("Failed to delete document with id " + documentId);
  325. }
  326. // removeDocument is the same as deleteDocument but allows the parent to be specified
  327. // Check here if the specified parentDocumentId matches the true parent of documentId
  328. boolean doesFileParentMatch = false;
  329. File fileParent = file.getParentFile();
  330. if (fileParent == null || fileParent.equals(parent)) {
  331. doesFileParentMatch = true;
  332. }
  333. // Remove the file if parent matches or file and parent are equal
  334. if (parent.equals(file) || doesFileParentMatch) {
  335. if (file.delete()) {
  336. Log.i(TAG, "Deleted file with id " + documentId);
  337. } else {
  338. throw new FileNotFoundException("Failed to delete document with id " + documentId);
  339. }
  340. } else {
  341. throw new FileNotFoundException("Failed to delete document with id " + documentId);
  342. }
  343. }
  344. /**
  345. * overload copyDocument to insist that the parent matches
  346. */
  347. public String copyDocument(String sourceDocumentId, String sourceParentDocumentId,
  348. String targetParentDocumentId) throws FileNotFoundException {
  349. Log.v(TAG, "copyDocument with document parent");
  350. if (!isChildDocument(sourceParentDocumentId, sourceDocumentId)) {
  351. throw new FileNotFoundException("Failed to copy document with id " +
  352. sourceDocumentId + ". Parent is not: " + sourceParentDocumentId);
  353. }
  354. return copyDocument(sourceDocumentId, targetParentDocumentId);
  355. }
  356. @Override
  357. public String copyDocument(String sourceDocumentId, String targetParentDocumentId)
  358. throws FileNotFoundException {
  359. Log.v(TAG, "copyDocument");
  360. File parent = getFileForDocId(targetParentDocumentId);
  361. File oldFile = getFileForDocId(sourceDocumentId);
  362. File newFile = new File(parent.getPath(), oldFile.getName());
  363. try {
  364. int conflictId = 1;
  365. while(newFile.exists()) {
  366. newFile = new File(parent.getPath(), oldFile.getName() + "(" + conflictId++ + ")");
  367. }
  368. // Create the new File to copy into
  369. boolean wasNewFileCreated = false;
  370. boolean isDirectory = Document.MIME_TYPE_DIR.equals(getTypeForFile(oldFile));
  371. if(isDirectory) {
  372. if (newFile.mkdir()) {
  373. if (newFile.setWritable(true) && newFile.setReadable(true)) {
  374. wasNewFileCreated = true;
  375. }
  376. }
  377. } else {
  378. if (newFile.createNewFile()) {
  379. if (newFile.setWritable(true) && newFile.setReadable(true)) {
  380. wasNewFileCreated = true;
  381. }
  382. }
  383. }
  384. if (!wasNewFileCreated) {
  385. throw new FileNotFoundException("Failed to copy document " + sourceDocumentId +
  386. ". Could not create new file.");
  387. }
  388. if(!isDirectory) {
  389. // Copy the bytes into the new file
  390. try (InputStream inStream = new FileInputStream(oldFile)) {
  391. try (OutputStream outStream = new FileOutputStream(newFile)) {
  392. // Transfer bytes from in to out
  393. byte[] buf = new byte[16384]; // disk: 8-64k
  394. int len;
  395. while ((len = inStream.read(buf)) > 0) {
  396. outStream.write(buf, 0, len);
  397. }
  398. }
  399. }
  400. }
  401. } catch (IOException e) {
  402. throw new FileNotFoundException("Failed to copy document: " + sourceDocumentId +
  403. ". " + e.getMessage());
  404. }
  405. return getDocIdForFile(newFile);
  406. }
  407. @Override
  408. public String moveDocument(String sourceDocumentId, String sourceParentDocumentId,
  409. String targetParentDocumentId) throws FileNotFoundException {
  410. Log.v(TAG, "moveDocument");
  411. try {
  412. // Copy document, insisting that the parent is correct
  413. String newDocumentId = copyDocument(sourceDocumentId, sourceParentDocumentId,
  414. targetParentDocumentId);
  415. // Remove old document
  416. removeDocument(sourceDocumentId, sourceParentDocumentId);
  417. return newDocumentId;
  418. } catch (FileNotFoundException e) {
  419. throw new FileNotFoundException("Failed to move document " + sourceDocumentId);
  420. }
  421. }
  422. @Override
  423. public String getDocumentType(String documentId) throws FileNotFoundException {
  424. File file = getFileForDocId(documentId);
  425. return getTypeForFile(file);
  426. }
  427. /**
  428. * @param projection the requested root column projection
  429. * @return either the requested root column projection, or the default projection if the
  430. * requested projection is null.
  431. */
  432. private static String[] resolveRootProjection(String[] projection) {
  433. return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
  434. }
  435. private static String[] resolveDocumentProjection(String[] projection) {
  436. return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
  437. }
  438. /**
  439. * Get a file's MIME type
  440. *
  441. * @param file the File object whose type we want
  442. * @return the MIME type of the file
  443. */
  444. private static String getTypeForFile(File file) {
  445. if (file.isDirectory()) {
  446. return Document.MIME_TYPE_DIR;
  447. } else {
  448. return getTypeForName(file.getName());
  449. }
  450. }
  451. /**
  452. * Get the MIME data type of a document, given its filename.
  453. *
  454. * @param name the filename of the document
  455. * @return the MIME data type of a document
  456. */
  457. private static String getTypeForName(String name) {
  458. final int lastDot = name.lastIndexOf('.');
  459. if (lastDot >= 0) {
  460. final String extension = name.substring(lastDot + 1);
  461. if(extension == "love") {
  462. return "application/x-love-game";
  463. }
  464. final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
  465. if (mime != null) {
  466. return mime;
  467. }
  468. }
  469. return "application/octet-stream";
  470. }
  471. /**
  472. * Get the document ID given a File. The document id must be consistent across time. Other
  473. * applications may save the ID and use it to reference documents later.
  474. * <p/>
  475. * This implementation is specific to this demo. It assumes only one root and is built
  476. * directly from the file structure. However, it is possible for a document to be a child of
  477. * multiple directories (for example "android" and "images"), in which case the file must have
  478. * the same consistent, unique document ID in both cases.
  479. *
  480. * @param file the File whose document ID you want
  481. * @return the corresponding document ID
  482. */
  483. private String getDocIdForFile(File file) {
  484. String path = file.getAbsolutePath();
  485. // Start at first char of path under root
  486. final String rootPath = mBaseDir.getPath();
  487. if (rootPath.equals(path)) {
  488. path = "";
  489. } else if (rootPath.endsWith("/")) {
  490. path = path.substring(rootPath.length());
  491. } else {
  492. path = path.substring(rootPath.length() + 1);
  493. }
  494. return ROOT + ':' + path;
  495. }
  496. /**
  497. * Add a representation of a file to a cursor.
  498. *
  499. * @param result the cursor to modify
  500. * @param docId the document ID representing the desired file (may be null if given file)
  501. * @param file the File object representing the desired file (may be null if given docID)
  502. * @throws FileNotFoundException
  503. */
  504. private void includeFile(MatrixCursor result, String docId, File file)
  505. throws FileNotFoundException {
  506. if (docId == null) {
  507. docId = getDocIdForFile(file);
  508. } else {
  509. file = getFileForDocId(docId);
  510. }
  511. int flags = 0;
  512. if (file.isDirectory()) {
  513. // Add FLAG_DIR_SUPPORTS_CREATE if the file is a writable directory.
  514. if (file.isDirectory() && file.canWrite()) {
  515. flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
  516. }
  517. } else if (file.canWrite()) {
  518. // If the file is writable set FLAG_SUPPORTS_WRITE and
  519. // FLAG_SUPPORTS_DELETE
  520. flags |= Document.FLAG_SUPPORTS_WRITE;
  521. flags |= Document.FLAG_SUPPORTS_DELETE;
  522. // Add SDK specific flags if appropriate
  523. if (SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
  524. flags |= Document.FLAG_SUPPORTS_RENAME;
  525. }
  526. if (SDK_INT >= Build.VERSION_CODES.N) {
  527. flags |= Document.FLAG_SUPPORTS_REMOVE;
  528. flags |= Document.FLAG_SUPPORTS_MOVE;
  529. flags |= Document.FLAG_SUPPORTS_COPY;
  530. }
  531. }
  532. final String displayName = file.getName();
  533. final String mimeType = getTypeForFile(file);
  534. if (mimeType.startsWith("image/")) {
  535. // Allow the image to be represented by a thumbnail rather than an icon
  536. flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
  537. }
  538. final MatrixCursor.RowBuilder row = result.newRow();
  539. row.add(Document.COLUMN_DOCUMENT_ID, docId);
  540. if(file.getAbsolutePath() == mBaseDir.getAbsolutePath()) {
  541. row.add(Document.COLUMN_DISPLAY_NAME, getContext().getApplicationInfo().loadLabel(getContext().getPackageManager()).toString());
  542. } else {
  543. row.add(Document.COLUMN_DISPLAY_NAME, displayName);
  544. }
  545. row.add(Document.COLUMN_SIZE, file.length());
  546. row.add(Document.COLUMN_MIME_TYPE, mimeType);
  547. row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
  548. row.add(Document.COLUMN_FLAGS, flags);
  549. // Add a custom icon
  550. if(mimeType == "application/x-love-game") {
  551. row.add(Document.COLUMN_ICON, R.drawable.love);
  552. } else {
  553. row.add(Document.COLUMN_ICON, null);
  554. }
  555. }
  556. /**
  557. * Translate your custom URI scheme into a File object.
  558. *
  559. * @param docId the document ID representing the desired file
  560. * @return a File represented by the given document ID
  561. * @throws java.io.FileNotFoundException
  562. */
  563. private File getFileForDocId(String docId) throws FileNotFoundException {
  564. File target = mBaseDir;
  565. if (docId.equals(ROOT)) {
  566. return target;
  567. }
  568. final int splitIndex = docId.indexOf(':', 1);
  569. if (splitIndex < 0) {
  570. throw new FileNotFoundException("Missing root for " + docId);
  571. } else {
  572. final String path = docId.substring(splitIndex + 1);
  573. target = new File(target, path);
  574. if (!target.exists()) {
  575. throw new FileNotFoundException("Missing file for " + docId + " at " + target);
  576. }
  577. return target;
  578. }
  579. }
  580. }