mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-02-15 16:59:18 +01:00
Android: Add content provider support to File::ScanDirectoryTree
This commit is contained in:
parent
525268f043
commit
2126f62111
@ -14,15 +14,16 @@ import androidx.annotation.Keep;
|
|||||||
import org.dolphinemu.dolphinemu.DolphinApplication;
|
import org.dolphinemu.dolphinemu.DolphinApplication;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class ContentHandler
|
public class ContentHandler
|
||||||
{
|
{
|
||||||
@Keep
|
@Keep
|
||||||
public static int openFd(String uri, String mode)
|
public static int openFd(@NonNull String uri, @NonNull String mode)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return getContentResolver().openFileDescriptor(Uri.parse(uri), mode).detachFd();
|
return getContentResolver().openFileDescriptor(unmangle(uri), mode).detachFd();
|
||||||
}
|
}
|
||||||
catch (SecurityException e)
|
catch (SecurityException e)
|
||||||
{
|
{
|
||||||
@ -38,11 +39,11 @@ public class ContentHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
public static boolean delete(String uri)
|
public static boolean delete(@NonNull String uri)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return DocumentsContract.deleteDocument(getContentResolver(), Uri.parse(uri));
|
return DocumentsContract.deleteDocument(getContentResolver(), unmangle(uri));
|
||||||
}
|
}
|
||||||
catch (SecurityException e)
|
catch (SecurityException e)
|
||||||
{
|
{
|
||||||
@ -60,8 +61,9 @@ public class ContentHandler
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
Uri documentUri = treeToDocument(unmangle(uri));
|
||||||
final String[] projection = new String[]{Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE};
|
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;
|
return cursor != null && cursor.getCount() > 0;
|
||||||
}
|
}
|
||||||
@ -70,6 +72,9 @@ public class ContentHandler
|
|||||||
{
|
{
|
||||||
Log.error("Tried to check if " + uri + " exists without permission");
|
Log.error("Tried to check if " + uri + " exists without permission");
|
||||||
}
|
}
|
||||||
|
catch (FileNotFoundException ignored)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -78,38 +83,53 @@ public class ContentHandler
|
|||||||
* @return -1 if not found, -2 if directory, file size otherwise
|
* @return -1 if not found, -2 if directory, file size otherwise
|
||||||
*/
|
*/
|
||||||
@Keep
|
@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
|
||||||
try (Cursor cursor = getContentResolver().query(Uri.parse(uri), projection, null, null, null))
|
|
||||||
{
|
{
|
||||||
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)))
|
if (cursor != null && cursor.moveToFirst())
|
||||||
return -2;
|
{
|
||||||
else
|
if (Document.MIME_TYPE_DIR.equals(cursor.getString(0)))
|
||||||
return cursor.isNull(1) ? 0 : cursor.getLong(1);
|
return -2;
|
||||||
|
else
|
||||||
|
return cursor.isNull(1) ? 0 : cursor.getLong(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (SecurityException e)
|
catch (SecurityException e)
|
||||||
{
|
{
|
||||||
Log.error("Tried to get metadata for " + uri + " without permission");
|
Log.error("Tried to get metadata for " + uri + " without permission");
|
||||||
}
|
}
|
||||||
|
catch (FileNotFoundException ignored)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable @Keep
|
@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
|
@Nullable
|
||||||
public static String getDisplayName(@NonNull Uri uri)
|
public static String getDisplayName(@NonNull Uri uri)
|
||||||
{
|
{
|
||||||
final String[] projection = new String[]{Document.COLUMN_DISPLAY_NAME};
|
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())
|
if (cursor != null && cursor.moveToFirst())
|
||||||
{
|
{
|
||||||
@ -124,6 +144,163 @@ public class ContentHandler
|
|||||||
return null;
|
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()
|
private static ContentResolver getContentResolver()
|
||||||
{
|
{
|
||||||
return DolphinApplication.getAppContext().getContentResolver();
|
return DolphinApplication.getAppContext().getContentResolver();
|
||||||
|
@ -121,6 +121,15 @@ std::string GetAndroidContentDisplayName(const std::string& uri)
|
|||||||
return display_name ? GetJString(env, reinterpret_cast<jstring>(display_name)) : "";
|
return display_name ? GetJString(env, reinterpret_cast<jstring>(display_name)) : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> GetAndroidContentChildNames(const std::string& uri)
|
||||||
|
{
|
||||||
|
JNIEnv* env = IDCache::GetEnvForThread();
|
||||||
|
jobject children =
|
||||||
|
env->CallStaticObjectMethod(IDCache::GetContentHandlerClass(),
|
||||||
|
IDCache::GetContentHandlerGetChildNames(), ToJString(env, uri));
|
||||||
|
return JStringArrayToVector(env, reinterpret_cast<jobjectArray>(children));
|
||||||
|
}
|
||||||
|
|
||||||
int GetNetworkIpAddress()
|
int GetNetworkIpAddress()
|
||||||
{
|
{
|
||||||
JNIEnv* env = IDCache::GetEnvForThread();
|
JNIEnv* env = IDCache::GetEnvForThread();
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include <ios>
|
#include <ios>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
|
|
||||||
@ -34,6 +35,9 @@ jlong GetAndroidContentSizeAndIsDirectory(const std::string& uri);
|
|||||||
// An empty string will be returned for files which do not exist.
|
// An empty string will be returned for files which do not exist.
|
||||||
std::string GetAndroidContentDisplayName(const std::string& uri);
|
std::string GetAndroidContentDisplayName(const std::string& uri);
|
||||||
|
|
||||||
|
// Returns the display names of all children of a directory.
|
||||||
|
std::vector<std::string> GetAndroidContentChildNames(const std::string& uri);
|
||||||
|
|
||||||
int GetNetworkIpAddress();
|
int GetNetworkIpAddress();
|
||||||
int GetNetworkPrefixLength();
|
int GetNetworkPrefixLength();
|
||||||
int GetNetworkGateway();
|
int GetNetworkGateway();
|
||||||
|
@ -46,6 +46,7 @@ static jmethodID s_content_handler_open_fd;
|
|||||||
static jmethodID s_content_handler_delete;
|
static jmethodID s_content_handler_delete;
|
||||||
static jmethodID s_content_handler_get_size_and_is_directory;
|
static jmethodID s_content_handler_get_size_and_is_directory;
|
||||||
static jmethodID s_content_handler_get_display_name;
|
static jmethodID s_content_handler_get_display_name;
|
||||||
|
static jmethodID s_content_handler_get_child_names;
|
||||||
|
|
||||||
static jclass s_network_helper_class;
|
static jclass s_network_helper_class;
|
||||||
static jmethodID s_network_helper_get_network_ip_address;
|
static jmethodID s_network_helper_get_network_ip_address;
|
||||||
@ -222,6 +223,11 @@ jmethodID GetContentHandlerGetDisplayName()
|
|||||||
return s_content_handler_get_display_name;
|
return s_content_handler_get_display_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jmethodID GetContentHandlerGetChildNames()
|
||||||
|
{
|
||||||
|
return s_content_handler_get_child_names;
|
||||||
|
}
|
||||||
|
|
||||||
jclass GetNetworkHelperClass()
|
jclass GetNetworkHelperClass()
|
||||||
{
|
{
|
||||||
return s_network_helper_class;
|
return s_network_helper_class;
|
||||||
@ -323,6 +329,8 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved)
|
|||||||
s_content_handler_class, "getSizeAndIsDirectory", "(Ljava/lang/String;)J");
|
s_content_handler_class, "getSizeAndIsDirectory", "(Ljava/lang/String;)J");
|
||||||
s_content_handler_get_display_name = env->GetStaticMethodID(
|
s_content_handler_get_display_name = env->GetStaticMethodID(
|
||||||
s_content_handler_class, "getDisplayName", "(Ljava/lang/String;)Ljava/lang/String;");
|
s_content_handler_class, "getDisplayName", "(Ljava/lang/String;)Ljava/lang/String;");
|
||||||
|
s_content_handler_get_child_names = env->GetStaticMethodID(
|
||||||
|
s_content_handler_class, "getChildNames", "(Ljava/lang/String;)[Ljava/lang/String;");
|
||||||
|
|
||||||
const jclass network_helper_class =
|
const jclass network_helper_class =
|
||||||
env->FindClass("org/dolphinemu/dolphinemu/utils/NetworkHelper");
|
env->FindClass("org/dolphinemu/dolphinemu/utils/NetworkHelper");
|
||||||
|
@ -46,6 +46,7 @@ jmethodID GetContentHandlerOpenFd();
|
|||||||
jmethodID GetContentHandlerDelete();
|
jmethodID GetContentHandlerDelete();
|
||||||
jmethodID GetContentHandlerGetSizeAndIsDirectory();
|
jmethodID GetContentHandlerGetSizeAndIsDirectory();
|
||||||
jmethodID GetContentHandlerGetDisplayName();
|
jmethodID GetContentHandlerGetDisplayName();
|
||||||
|
jmethodID GetContentHandlerGetChildNames();
|
||||||
|
|
||||||
jclass GetNetworkHelperClass();
|
jclass GetNetworkHelperClass();
|
||||||
jmethodID GetNetworkHelperGetNetworkIpAddress();
|
jmethodID GetNetworkHelperGetNetworkIpAddress();
|
||||||
|
@ -497,14 +497,47 @@ FSTEntry ScanDirectoryTree(const std::string& directory, bool recursive)
|
|||||||
{
|
{
|
||||||
const std::string virtual_name(TStrToUTF8(ffd.cFileName));
|
const std::string virtual_name(TStrToUTF8(ffd.cFileName));
|
||||||
#else
|
#else
|
||||||
DIR* dirp = opendir(directory.c_str());
|
DIR* dirp = nullptr;
|
||||||
if (!dirp)
|
|
||||||
return parent_entry;
|
#ifdef ANDROID
|
||||||
|
std::vector<std::string> child_names;
|
||||||
|
if (IsPathAndroidContent(directory))
|
||||||
|
{
|
||||||
|
child_names = GetAndroidContentChildNames(directory);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
dirp = opendir(directory.c_str());
|
||||||
|
if (!dirp)
|
||||||
|
return parent_entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef ANDROID
|
||||||
|
auto it = child_names.cbegin();
|
||||||
|
#endif
|
||||||
|
|
||||||
// non Windows loop
|
// non Windows loop
|
||||||
while (dirent* result = readdir(dirp))
|
while (true)
|
||||||
{
|
{
|
||||||
const std::string virtual_name(result->d_name);
|
std::string virtual_name;
|
||||||
|
|
||||||
|
#ifdef ANDROID
|
||||||
|
if (!dirp)
|
||||||
|
{
|
||||||
|
if (it == child_names.cend())
|
||||||
|
break;
|
||||||
|
virtual_name = *it;
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
dirent* result = readdir(dirp);
|
||||||
|
if (!result)
|
||||||
|
break;
|
||||||
|
virtual_name = result->d_name;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
if (virtual_name == "." || virtual_name == "..")
|
if (virtual_name == "." || virtual_name == "..")
|
||||||
continue;
|
continue;
|
||||||
@ -535,7 +568,8 @@ FSTEntry ScanDirectoryTree(const std::string& directory, bool recursive)
|
|||||||
FindClose(hFind);
|
FindClose(hFind);
|
||||||
#else
|
#else
|
||||||
}
|
}
|
||||||
closedir(dirp);
|
if (dirp)
|
||||||
|
closedir(dirp);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return parent_entry;
|
return parent_entry;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user