diff options
| author | JosJuice <josjuice@gmail.com> | 2020-11-08 16:57:49 +0100 |
|---|---|---|
| committer | JosJuice <josjuice@gmail.com> | 2020-12-20 13:24:54 +0100 |
| commit | 2126f62111a95e3a2386467747e5bf94b9943e1d (patch) | |
| tree | 450f264e9068e665fbe5107981e1a39eb5b3b528 /Source/Android/app/src/main/java | |
| parent | 525268f043a25116dbb5e362d9e3e84b3f4ca5e5 (diff) | |
Android: Add content provider support to File::ScanDirectoryTree
Diffstat (limited to 'Source/Android/app/src/main/java')
| -rw-r--r-- | Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java | 209 |
1 files changed, 193 insertions, 16 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java index 53e950ac41..5233c9dc59 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java @@ -14,15 +14,16 @@ import androidx.annotation.Keep; import org.dolphinemu.dolphinemu.DolphinApplication; import java.io.FileNotFoundException; +import java.util.List; public class ContentHandler { @Keep - public static int openFd(String uri, String mode) + public static int openFd(@NonNull String uri, @NonNull String mode) { try { - return getContentResolver().openFileDescriptor(Uri.parse(uri), mode).detachFd(); + return getContentResolver().openFileDescriptor(unmangle(uri), mode).detachFd(); } catch (SecurityException e) { @@ -38,11 +39,11 @@ public class ContentHandler } @Keep - public static boolean delete(String uri) + public static boolean delete(@NonNull String uri) { try { - return DocumentsContract.deleteDocument(getContentResolver(), Uri.parse(uri)); + return DocumentsContract.deleteDocument(getContentResolver(), unmangle(uri)); } catch (SecurityException e) { @@ -60,8 +61,9 @@ public class ContentHandler { try { + Uri documentUri = treeToDocument(unmangle(uri)); final String[] projection = new String[]{Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE}; - try (Cursor cursor = getContentResolver().query(Uri.parse(uri), projection, null, null, null)) + try (Cursor cursor = getContentResolver().query(documentUri, projection, null, null, null)) { return cursor != null && cursor.getCount() > 0; } @@ -70,6 +72,9 @@ public class ContentHandler { Log.error("Tried to check if " + uri + " exists without permission"); } + catch (FileNotFoundException ignored) + { + } return false; } @@ -78,38 +83,53 @@ public class ContentHandler * @return -1 if not found, -2 if directory, file size otherwise */ @Keep - public static long getSizeAndIsDirectory(String uri) + public static long getSizeAndIsDirectory(@NonNull String uri) { - final String[] projection = new String[]{Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE}; - try (Cursor cursor = getContentResolver().query(Uri.parse(uri), projection, null, null, null)) + try { - if (cursor != null && cursor.moveToFirst()) + Uri documentUri = treeToDocument(unmangle(uri)); + final String[] projection = new String[]{Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE}; + try (Cursor cursor = getContentResolver().query(documentUri, projection, null, null, null)) { - if (Document.MIME_TYPE_DIR.equals(cursor.getString(0))) - return -2; - else - return cursor.isNull(1) ? 0 : cursor.getLong(1); + if (cursor != null && cursor.moveToFirst()) + { + if (Document.MIME_TYPE_DIR.equals(cursor.getString(0))) + return -2; + else + return cursor.isNull(1) ? 0 : cursor.getLong(1); + } } } catch (SecurityException e) { Log.error("Tried to get metadata for " + uri + " without permission"); } + catch (FileNotFoundException ignored) + { + } return -1; } @Nullable @Keep - public static String getDisplayName(String uri) + public static String getDisplayName(@NonNull String uri) { - return getDisplayName(Uri.parse(uri)); + try + { + return getDisplayName(unmangle(uri)); + } + catch (FileNotFoundException e) + { + return null; + } } @Nullable public static String getDisplayName(@NonNull Uri uri) { final String[] projection = new String[]{Document.COLUMN_DISPLAY_NAME}; - try (Cursor cursor = getContentResolver().query(uri, projection, null, null, null)) + Uri documentUri = treeToDocument(uri); + try (Cursor cursor = getContentResolver().query(documentUri, projection, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { @@ -124,6 +144,163 @@ public class ContentHandler return null; } + @NonNull @Keep + public static String[] getChildNames(@NonNull String uri) + { + try + { + Uri unmangledUri = unmangle(uri); + String documentId = DocumentsContract.getDocumentId(treeToDocument(unmangledUri)); + Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(unmangledUri, documentId); + + final String[] projection = new String[]{Document.COLUMN_DISPLAY_NAME}; + try (Cursor cursor = getContentResolver().query(childrenUri, projection, null, null, null)) + { + if (cursor != null) + { + String[] result = new String[cursor.getCount()]; + for (int i = 0; i < result.length; i++) + { + cursor.moveToNext(); + result[i] = cursor.getString(0); + } + return result; + } + } + } + catch (SecurityException e) + { + Log.error("Tried to get children of " + uri + " without permission"); + } + catch (FileNotFoundException ignored) + { + } + + return new String[0]; + } + + @NonNull + private static Uri getChild(@NonNull Uri parentUri, @NonNull String childName) + throws FileNotFoundException, SecurityException + { + String parentId = DocumentsContract.getDocumentId(treeToDocument(parentUri)); + Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(parentUri, parentId); + + final String[] projection = new String[]{Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_DOCUMENT_ID}; + final String selection = Document.COLUMN_DISPLAY_NAME + "=?"; + final String[] selectionArgs = new String[]{childName}; + try (Cursor cursor = getContentResolver().query(childrenUri, projection, selection, + selectionArgs, null)) + { + if (cursor != null) + { + while (cursor.moveToNext()) + { + // FileProvider seemingly doesn't support selections, so we have to manually filter here + if (childName.equals(cursor.getString(0))) + { + return DocumentsContract.buildDocumentUriUsingTree(parentUri, cursor.getString(1)); + } + } + } + } + catch (SecurityException e) + { + Log.error("Tried to get child " + childName + " of " + parentUri + " without permission"); + } + + throw new FileNotFoundException(parentUri + "/" + childName); + } + + /** + * Since our C++ code was written under the assumption that it would be running under a filesystem + * which supports normal paths, it appends a slash followed by a file name when it wants to access + * a file in a directory. This function translates that into the type of URI that SAF requires. + * + * In order to detect whether a URI is mangled or not, we make the assumption that an + * unmangled URI contains at least one % and does not contain any slashes after the last %. + * This seems to hold for all common storage providers, but it is theoretically for a storage + * provider to use URIs without any % characters. + */ + @NonNull + private static Uri unmangle(@NonNull String uri) throws FileNotFoundException, SecurityException + { + int lastComponentEnd = getLastComponentEnd(uri); + int lastComponentStart = getLastComponentStart(uri, lastComponentEnd); + + if (lastComponentStart == 0) + { + return Uri.parse(uri.substring(0, lastComponentEnd)); + } + else + { + Uri parentUri = unmangle(uri.substring(0, lastComponentStart)); + String childName = uri.substring(lastComponentStart, lastComponentEnd); + return getChild(parentUri, childName); + } + } + + /** + * Returns the last character which is not a slash. + */ + private static int getLastComponentEnd(@NonNull String uri) + { + int i = uri.length(); + while (i > 0 && uri.charAt(i - 1) == '/') + i--; + return i; + } + + /** + * Scans backwards starting from lastComponentEnd and returns the index after the first slash + * it finds, but only if there is a % before that slash and there is no % after it. + */ + private static int getLastComponentStart(@NonNull String uri, int lastComponentEnd) + { + int i = lastComponentEnd; + while (i > 0 && uri.charAt(i - 1) != '/') + { + i--; + if (uri.charAt(i) == '%') + return 0; + } + + int j = i; + while (j > 0) + { + j--; + if (uri.charAt(j) == '%') + return i; + } + + return 0; + } + + @NonNull + private static Uri treeToDocument(@NonNull Uri uri) + { + if (isTreeUri(uri)) + { + String documentId = DocumentsContract.getTreeDocumentId(uri); + return DocumentsContract.buildDocumentUriUsingTree(uri, documentId); + } + else + { + return uri; + } + } + + /** + * This is like DocumentsContract.isTreeUri, except it doesn't return true for URIs like + * content://com.example/tree/12/document/24/. We want to treat those as documents, not trees. + */ + private static boolean isTreeUri(@NonNull Uri uri) + { + final List<String> pathSegments = uri.getPathSegments(); + return pathSegments.size() == 2 && "tree".equals(pathSegments.get(0)); + } + private static ContentResolver getContentResolver() { return DolphinApplication.getAppContext().getContentResolver(); |
