summaryrefslogtreecommitdiff
path: root/Source/Android/app/src/main/java
diff options
context:
space:
mode:
authorJosJuice <josjuice@gmail.com>2020-11-08 16:57:49 +0100
committerJosJuice <josjuice@gmail.com>2020-12-20 13:24:54 +0100
commit2126f62111a95e3a2386467747e5bf94b9943e1d (patch)
tree450f264e9068e665fbe5107981e1a39eb5b3b528 /Source/Android/app/src/main/java
parent525268f043a25116dbb5e362d9e3e84b3f4ca5e5 (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.java209
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();