mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-05-09 04:13:28 -05:00
Merge bf744cede4 into 30a20d75d2
This commit is contained in:
commit
27950ce0e3
|
|
@ -1,425 +0,0 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.dolphinemu.dolphinemu.utils;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.dolphinemu.dolphinemu.DolphinApplication;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/*
|
||||
We use a lot of "catch (Exception e)" in this class. This is for two reasons:
|
||||
|
||||
1. We don't want any exceptions to escape to native code, as this leads to nasty crashes
|
||||
that often don't have stack traces that make sense.
|
||||
|
||||
2. The sheer number of different exceptions, both documented and undocumented. These include:
|
||||
- FileNotFoundException when a file doesn't exist
|
||||
- FileNotFoundException when using an invalid open mode (according to the documentation)
|
||||
- IllegalArgumentException when using an invalid open mode (in practice with FileProvider)
|
||||
- IllegalArgumentException when providing a tree where a document was expected and vice versa
|
||||
- SecurityException when trying to access something the user hasn't granted us permission to
|
||||
- UnsupportedOperationException when a URI specifies a storage provider that doesn't exist
|
||||
*/
|
||||
|
||||
public class ContentHandler
|
||||
{
|
||||
public static boolean isContentUri(@NonNull String pathOrUri)
|
||||
{
|
||||
return pathOrUri.startsWith("content://");
|
||||
}
|
||||
|
||||
@Keep
|
||||
public static int openFd(@NonNull String uri, @NonNull String mode)
|
||||
{
|
||||
try
|
||||
{
|
||||
return getContentResolver().openFileDescriptor(unmangle(uri), mode).detachFd();
|
||||
}
|
||||
catch (SecurityException e)
|
||||
{
|
||||
Log.error("Tried to open " + uri + " without permission");
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Keep
|
||||
public static boolean delete(@NonNull String uri)
|
||||
{
|
||||
try
|
||||
{
|
||||
return DocumentsContract.deleteDocument(getContentResolver(), unmangle(uri));
|
||||
}
|
||||
catch (FileNotFoundException e)
|
||||
{
|
||||
// Return true because we care about the file not being there, not the actual delete.
|
||||
return true;
|
||||
}
|
||||
catch (SecurityException e)
|
||||
{
|
||||
Log.error("Tried to delete " + uri + " without permission");
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean exists(@NonNull String uri)
|
||||
{
|
||||
try
|
||||
{
|
||||
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))
|
||||
{
|
||||
return cursor != null && cursor.getCount() > 0;
|
||||
}
|
||||
}
|
||||
catch (SecurityException e)
|
||||
{
|
||||
Log.error("Tried to check if " + uri + " exists without permission");
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return -1 if not found, -2 if directory, file size otherwise
|
||||
*/
|
||||
@Keep
|
||||
public static long getSizeAndIsDirectory(@NonNull String uri)
|
||||
{
|
||||
try
|
||||
{
|
||||
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 (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 (Exception ignored)
|
||||
{
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Nullable @Keep
|
||||
public static String getDisplayName(@NonNull String uri)
|
||||
{
|
||||
try
|
||||
{
|
||||
return getDisplayName(unmangle(uri));
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getDisplayName(@NonNull Uri uri)
|
||||
{
|
||||
final String[] projection = new String[]{Document.COLUMN_DISPLAY_NAME};
|
||||
Uri documentUri = treeToDocument(uri);
|
||||
try (Cursor cursor = getContentResolver().query(documentUri, projection, null, null, null))
|
||||
{
|
||||
if (cursor != null && cursor.moveToFirst())
|
||||
{
|
||||
return cursor.getString(0);
|
||||
}
|
||||
}
|
||||
catch (SecurityException e)
|
||||
{
|
||||
Log.error("Tried to get display name of " + uri + " without permission");
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@NonNull @Keep
|
||||
public static String[] getChildNames(@NonNull String uri, boolean recursive)
|
||||
{
|
||||
try
|
||||
{
|
||||
return getChildNames(unmangle(uri), recursive);
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
}
|
||||
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String[] getChildNames(@NonNull Uri uri, boolean recursive)
|
||||
{
|
||||
ArrayList<String> result = new ArrayList<>();
|
||||
|
||||
ForEachChildCallback callback = new ForEachChildCallback()
|
||||
{
|
||||
@Override
|
||||
public void run(String displayName, String documentId, boolean isDirectory)
|
||||
{
|
||||
if (recursive && isDirectory)
|
||||
{
|
||||
forEachChild(uri, documentId, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.add(displayName);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
forEachChild(uri, DocumentsContract.getDocumentId(treeToDocument(uri)), callback);
|
||||
|
||||
return result.toArray(new String[0]);
|
||||
}
|
||||
|
||||
@NonNull @Keep
|
||||
public static String[] doFileSearch(@NonNull String directory, @NonNull String[] extensions,
|
||||
boolean recursive)
|
||||
{
|
||||
ArrayList<String> result = new ArrayList<>();
|
||||
|
||||
try
|
||||
{
|
||||
Uri uri = unmangle(directory);
|
||||
String documentId = DocumentsContract.getDocumentId(treeToDocument(uri));
|
||||
boolean acceptAll = extensions.length == 0;
|
||||
Predicate<String> extensionCheck = (displayName) ->
|
||||
{
|
||||
String extension = FileBrowserHelper.getExtension(displayName, true);
|
||||
return extension != null && Arrays.stream(extensions).anyMatch(extension::equalsIgnoreCase);
|
||||
};
|
||||
doFileSearch(uri, directory, documentId, recursive, result, acceptAll, extensionCheck);
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
}
|
||||
|
||||
return result.toArray(new String[0]);
|
||||
}
|
||||
|
||||
private static void doFileSearch(@NonNull Uri baseUri, @NonNull String path,
|
||||
@NonNull String documentId, boolean recursive, @NonNull List<String> resultOut,
|
||||
boolean acceptAll, @NonNull Predicate<String> extensionCheck)
|
||||
{
|
||||
forEachChild(baseUri, documentId, (displayName, childDocumentId, isDirectory) ->
|
||||
{
|
||||
String childPath = path + '/' + displayName;
|
||||
if (acceptAll || (!isDirectory && extensionCheck.test(displayName)))
|
||||
{
|
||||
resultOut.add(childPath);
|
||||
}
|
||||
if (recursive && isDirectory)
|
||||
{
|
||||
doFileSearch(baseUri, childPath, childDocumentId, recursive, resultOut, acceptAll,
|
||||
extensionCheck);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private interface ForEachChildCallback
|
||||
{
|
||||
void run(String displayName, String documentId, boolean isDirectory);
|
||||
}
|
||||
|
||||
private static void forEachChild(@NonNull Uri uri, @NonNull String documentId,
|
||||
@NonNull ForEachChildCallback callback)
|
||||
{
|
||||
try
|
||||
{
|
||||
Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, documentId);
|
||||
|
||||
final String[] projection = new String[]{Document.COLUMN_DISPLAY_NAME,
|
||||
Document.COLUMN_MIME_TYPE, Document.COLUMN_DOCUMENT_ID};
|
||||
try (Cursor cursor = getContentResolver().query(childrenUri, projection, null, null, null))
|
||||
{
|
||||
if (cursor != null)
|
||||
{
|
||||
while (cursor.moveToNext())
|
||||
{
|
||||
callback.run(cursor.getString(0), cursor.getString(2),
|
||||
Document.MIME_TYPE_DIR.equals(cursor.getString(1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SecurityException e)
|
||||
{
|
||||
Log.error("Tried to get children of " + uri + " without permission");
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@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");
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
}
|
||||
|
||||
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
|
||||
public 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,358 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.dolphinemu.dolphinemu.utils
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.DocumentsContract.Document
|
||||
import androidx.annotation.Keep
|
||||
import androidx.core.net.toUri
|
||||
import org.dolphinemu.dolphinemu.DolphinApplication
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
/*
|
||||
We use a lot of "catch (Exception e)" in this class. This is for two reasons:
|
||||
|
||||
1. We don't want any exceptions to escape to native code, as this leads to nasty crashes
|
||||
that often don't have stack traces that make sense.
|
||||
|
||||
2. The sheer number of different exceptions, both documented and undocumented. These include:
|
||||
- FileNotFoundException when a file doesn't exist
|
||||
- FileNotFoundException when using an invalid open mode (according to the documentation)
|
||||
- IllegalArgumentException when using an invalid open mode (in practice with FileProvider)
|
||||
- IllegalArgumentException when providing a tree where a document was expected and vice versa
|
||||
- SecurityException when trying to access something the user hasn't granted us permission to
|
||||
- UnsupportedOperationException when a URI specifies a storage provider that doesn't exist
|
||||
*/
|
||||
object ContentHandler {
|
||||
@JvmStatic
|
||||
fun isContentUri(pathOrUri: String): Boolean {
|
||||
return pathOrUri.startsWith("content://")
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun openFd(uri: String, mode: String): Int {
|
||||
try {
|
||||
return getContentResolver().openFileDescriptor(unmangle(uri), mode)!!.detachFd()
|
||||
} catch (_: SecurityException) {
|
||||
Log.error("Tried to open $uri without permission")
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun delete(uri: String): Boolean {
|
||||
try {
|
||||
return DocumentsContract.deleteDocument(getContentResolver(), unmangle(uri))
|
||||
} catch (_: FileNotFoundException) {
|
||||
// Return true because we care about the file not being there, not the actual delete.
|
||||
return true
|
||||
} catch (_: SecurityException) {
|
||||
Log.error("Tried to delete $uri without permission")
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun exists(uri: String): Boolean {
|
||||
try {
|
||||
val documentUri = treeToDocument(unmangle(uri))
|
||||
val projection = arrayOf(Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE)
|
||||
getContentResolver().query(documentUri, projection, null, null, null)?.use { cursor ->
|
||||
return cursor.count > 0
|
||||
}
|
||||
} catch (_: SecurityException) {
|
||||
Log.error("Tried to check if $uri exists without permission")
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @return -1 if not found, -2 if directory, file size otherwise
|
||||
*/
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun getSizeAndIsDirectory(uri: String): Long {
|
||||
try {
|
||||
val documentUri = treeToDocument(unmangle(uri))
|
||||
val projection = arrayOf(Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE)
|
||||
getContentResolver().query(documentUri, projection, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
return if (Document.MIME_TYPE_DIR == cursor.getString(0)) {
|
||||
-2
|
||||
} else {
|
||||
if (cursor.isNull(1)) 0 else cursor.getLong(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: SecurityException) {
|
||||
Log.error("Tried to get metadata for $uri without permission")
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun getDisplayName(uri: String): String? {
|
||||
try {
|
||||
return getDisplayName(unmangle(uri))
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDisplayName(uri: Uri): String? {
|
||||
val projection = arrayOf(Document.COLUMN_DISPLAY_NAME)
|
||||
val documentUri = treeToDocument(uri)
|
||||
try {
|
||||
getContentResolver().query(documentUri, projection, null, null, null)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
return cursor.getString(0)
|
||||
}
|
||||
}
|
||||
} catch (_: SecurityException) {
|
||||
Log.error("Tried to get display name of $uri without permission")
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun getChildNames(uri: String, recursive: Boolean): Array<String> {
|
||||
try {
|
||||
return getChildNames(unmangle(uri), recursive)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
return emptyArray()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getChildNames(uri: Uri, recursive: Boolean): Array<String> {
|
||||
val result = ArrayList<String>()
|
||||
|
||||
if (recursive) {
|
||||
fun addChildren(documentId: String) {
|
||||
forEachChild(uri, documentId) { displayName, childDocumentId, isDirectory ->
|
||||
if (isDirectory) {
|
||||
addChildren(childDocumentId)
|
||||
} else {
|
||||
result.add(displayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addChildren(DocumentsContract.getDocumentId(treeToDocument(uri)))
|
||||
} else {
|
||||
forEachChild(
|
||||
uri, DocumentsContract.getDocumentId(treeToDocument(uri))
|
||||
) { displayName, _, isDirectory ->
|
||||
if (!isDirectory) {
|
||||
result.add(displayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.toTypedArray()
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun doFileSearch(
|
||||
directory: String, extensions: Array<String>, recursive: Boolean
|
||||
): Array<String> {
|
||||
val result = ArrayList<String>()
|
||||
|
||||
try {
|
||||
val uri = unmangle(directory)
|
||||
val documentId = DocumentsContract.getDocumentId(treeToDocument(uri))
|
||||
val acceptAll = extensions.isEmpty()
|
||||
val extensionCheck: (String) -> Boolean = { displayName ->
|
||||
val extension = FileBrowserHelper.getExtension(displayName, true)
|
||||
extension != null && extensions.any { it.equals(extension, ignoreCase = true) }
|
||||
}
|
||||
if (recursive) {
|
||||
doRecursiveFileSearch(uri, directory, documentId, result, acceptAll, extensionCheck)
|
||||
} else {
|
||||
forEachChild(uri, documentId) { displayName, _, isDirectory ->
|
||||
val childPath = "$directory/$displayName"
|
||||
if (acceptAll || (!isDirectory && extensionCheck(displayName))) {
|
||||
result.add(childPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
return result.toTypedArray()
|
||||
}
|
||||
|
||||
private fun doRecursiveFileSearch(
|
||||
baseUri: Uri,
|
||||
path: String,
|
||||
documentId: String,
|
||||
resultOut: MutableList<String>,
|
||||
acceptAll: Boolean,
|
||||
extensionCheck: (String) -> Boolean
|
||||
) {
|
||||
forEachChild(baseUri, documentId) { displayName, childDocumentId, isDirectory ->
|
||||
val childPath = "$path/$displayName"
|
||||
if (acceptAll || (!isDirectory && extensionCheck(displayName))) {
|
||||
resultOut.add(childPath)
|
||||
}
|
||||
if (isDirectory) {
|
||||
doRecursiveFileSearch(
|
||||
baseUri, childPath, childDocumentId, resultOut, acceptAll, extensionCheck
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun forEachChild(
|
||||
uri: Uri, documentId: String, callback: (String, String, Boolean) -> Unit
|
||||
) {
|
||||
try {
|
||||
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, documentId)
|
||||
val projection = arrayOf(
|
||||
Document.COLUMN_DISPLAY_NAME, Document.COLUMN_MIME_TYPE, Document.COLUMN_DOCUMENT_ID
|
||||
)
|
||||
getContentResolver().query(childrenUri, projection, null, null, null)?.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
callback(
|
||||
cursor.getString(0),
|
||||
cursor.getString(2),
|
||||
Document.MIME_TYPE_DIR == cursor.getString(1)
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (_: SecurityException) {
|
||||
Log.error("Tried to get children of $uri without permission")
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(FileNotFoundException::class, SecurityException::class)
|
||||
private fun getChild(parentUri: Uri, childName: String): Uri {
|
||||
val parentId = DocumentsContract.getDocumentId(treeToDocument(parentUri))
|
||||
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(parentUri, parentId)
|
||||
|
||||
val projection = arrayOf(Document.COLUMN_DISPLAY_NAME, Document.COLUMN_DOCUMENT_ID)
|
||||
val selection = Document.COLUMN_DISPLAY_NAME + "=?"
|
||||
val selectionArgs = arrayOf(childName)
|
||||
try {
|
||||
getContentResolver().query(childrenUri, projection, selection, selectionArgs, null)
|
||||
?.use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
// FileProvider seemingly doesn't support selections, so we have to manually filter here
|
||||
if (childName == cursor.getString(0)) {
|
||||
return DocumentsContract.buildDocumentUriUsingTree(
|
||||
parentUri, cursor.getString(1)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: SecurityException) {
|
||||
Log.error("Tried to get child $childName of $parentUri without permission")
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
throw 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.
|
||||
*/
|
||||
@Throws(FileNotFoundException::class, SecurityException::class)
|
||||
@JvmStatic
|
||||
fun unmangle(uri: String): Uri {
|
||||
val lastComponentEnd = getLastComponentEnd(uri)
|
||||
val lastComponentStart = getLastComponentStart(uri, lastComponentEnd)
|
||||
|
||||
return if (lastComponentStart == 0) {
|
||||
uri.substring(0, lastComponentEnd).toUri()
|
||||
} else {
|
||||
val parentUri = unmangle(uri.substring(0, lastComponentStart))
|
||||
val childName = uri.substring(lastComponentStart, lastComponentEnd)
|
||||
getChild(parentUri, childName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last character which is not a slash.
|
||||
*/
|
||||
private fun getLastComponentEnd(uri: String): Int {
|
||||
var i = uri.length
|
||||
while (i > 0 && uri[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 fun getLastComponentStart(uri: String, lastComponentEnd: Int): Int {
|
||||
var i = lastComponentEnd
|
||||
while (i > 0 && uri[i - 1] != '/') {
|
||||
i--
|
||||
if (uri[i] == '%') {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
var j = i
|
||||
while (j > 0) {
|
||||
j--
|
||||
if (uri[j] == '%') {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun treeToDocument(uri: Uri): Uri {
|
||||
return if (isTreeUri(uri)) {
|
||||
val documentId = DocumentsContract.getTreeDocumentId(uri)
|
||||
DocumentsContract.buildDocumentUriUsingTree(uri, documentId)
|
||||
} else {
|
||||
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 fun isTreeUri(uri: Uri): Boolean {
|
||||
val pathSegments = uri.pathSegments
|
||||
return pathSegments.size == 2 && pathSegments[0] == "tree"
|
||||
}
|
||||
|
||||
private fun getContentResolver(): ContentResolver {
|
||||
return DolphinApplication.getAppContext().contentResolver
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user