Android: Add content provider support to File::ScanDirectoryTree

This commit is contained in:
JosJuice
2020-11-08 16:57:49 +01:00
parent 525268f043
commit 2126f62111
6 changed files with 255 additions and 22 deletions

View File

@ -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();