Android: Implement DocumentProvider

This allows users to access the Dolphin user directory.
This commit is contained in:
Robin Kertels 2023-02-01 03:09:22 +01:00
parent f1e4b6a141
commit 13ed46a488
No known key found for this signature in database
GPG Key ID: 3824904F14D40757
5 changed files with 263 additions and 3 deletions

View File

@ -155,6 +155,18 @@
android:resource="@xml/nnf_provider_paths"/>
</provider>
<provider
android:name=".features.DocumentProvider"
android:authorities="org.dolphinemu.dolphinemu.user"
android:grantUriPermissions="true"
android:exported="true"
android:permission="android.permission.MANAGE_DOCUMENTS"
android:enabled="@bool/enableDocumentProvider">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
</application>
</manifest>

View File

@ -0,0 +1,238 @@
// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
// Partially based on:
// Skyline
// SPDX-License-Identifier: MPL-2.0
// Copyright © 2022 Skyline Team and Contributors (https://github.com/skyline-emu/)
package org.dolphinemu.dolphinemu.features
import android.annotation.TargetApi
import android.database.Cursor
import android.database.MatrixCursor
import android.os.Build
import android.os.CancellationSignal
import android.os.ParcelFileDescriptor
import android.provider.DocumentsContract
import android.provider.DocumentsProvider
import android.webkit.MimeTypeMap
import org.dolphinemu.dolphinemu.R
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization
import java.io.File
import java.io.FileNotFoundException
@TargetApi(Build.VERSION_CODES.N)
class DocumentProvider : DocumentsProvider() {
private var rootDirectory: File? = null
companion object {
private const val ROOT_ID = "root"
private val DEFAULT_ROOT_PROJECTION = arrayOf(
DocumentsContract.Root.COLUMN_ROOT_ID,
DocumentsContract.Root.COLUMN_FLAGS,
DocumentsContract.Root.COLUMN_ICON,
DocumentsContract.Root.COLUMN_TITLE,
DocumentsContract.Root.COLUMN_DOCUMENT_ID
)
private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf(
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_MIME_TYPE,
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
DocumentsContract.Document.COLUMN_FLAGS,
DocumentsContract.Document.COLUMN_SIZE
)
}
override fun onCreate(): Boolean {
rootDirectory = DirectoryInitialization.getUserDirectoryPath(context)
return true
}
override fun queryRoots(projection: Array<String>?): Cursor {
val result = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION)
rootDirectory = rootDirectory ?: DirectoryInitialization.getUserDirectoryPath(context)
rootDirectory ?: return result
result.newRow().apply {
add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT_ID)
add(DocumentsContract.Root.COLUMN_TITLE, context!!.getString(R.string.app_name))
add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_dolphin)
add(
DocumentsContract.Root.COLUMN_FLAGS,
DocumentsContract.Root.FLAG_SUPPORTS_CREATE or DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
)
add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, ROOT_ID)
}
return result
}
override fun queryDocument(documentId: String, projection: Array<String>?): Cursor {
val result = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
rootDirectory = rootDirectory ?: DirectoryInitialization.getUserDirectoryPath(context)
rootDirectory ?: return result
val file = documentIdToPath(documentId)
appendDocument(file, result)
return result
}
override fun queryChildDocuments(
parentDocumentId: String,
projection: Array<String>?,
queryArgs: String?
): Cursor {
val result = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
rootDirectory = rootDirectory ?: DirectoryInitialization.getUserDirectoryPath(context)
rootDirectory ?: return result
val folder = documentIdToPath(parentDocumentId)
val files = folder.listFiles()
if (files != null) {
for (file in files) {
appendDocument(file, result)
}
}
return result
}
override fun openDocument(
documentId: String,
mode: String,
signal: CancellationSignal?
): ParcelFileDescriptor {
val file = documentIdToPath(documentId)
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode(mode))
}
override fun createDocument(
parentDocumentId: String,
mimeType: String,
displayName: String
): String {
val folder = documentIdToPath(parentDocumentId)
val file = findFileNameForNewFile(File(folder, displayName))
if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
file.mkdirs()
} else {
file.createNewFile()
}
return pathToDocumentId(file)
}
override fun copyDocument(sourceDocumentId: String, targetParentDocumentId: String): String {
val file = documentIdToPath(sourceDocumentId)
val target = documentIdToPath(targetParentDocumentId)
val copy = copyRecursively(file, File(target, file.name))
return pathToDocumentId(copy)
}
override fun removeDocument(documentId: String, parentDocumentId: String) {
val file = documentIdToPath(documentId)
file.deleteRecursively()
}
override fun moveDocument(
sourceDocumentId: String,
sourceParentDocumentId: String,
targetParentDocumentId: String
): String {
val copy = copyDocument(sourceDocumentId, targetParentDocumentId)
val file = documentIdToPath(sourceDocumentId)
file.delete()
return copy
}
override fun renameDocument(documentId: String, displayName: String): String {
val file = documentIdToPath(documentId)
file.renameTo(findFileNameForNewFile(File(file.parentFile, displayName)))
return pathToDocumentId(file)
}
override fun isChildDocument(parentDocumentId: String, documentId: String): Boolean {
val file = documentIdToPath(documentId)
val folder = documentIdToPath(parentDocumentId)
return file.relativeToOrNull(folder) != null
}
private fun appendDocument(file: File, cursor: MatrixCursor) {
var flags = 0
if (file.isDirectory && file.canWrite()) {
flags = DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE
} else if (file.canWrite()) {
flags = DocumentsContract.Document.FLAG_SUPPORTS_WRITE
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_REMOVE
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_MOVE
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_COPY
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_RENAME
}
val name = if (file == rootDirectory) {
context!!.getString(R.string.app_name)
} else {
file.name
}
cursor.newRow().apply {
add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, pathToDocumentId(file))
add(DocumentsContract.Document.COLUMN_MIME_TYPE, getTypeForFile(file))
add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, name)
add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, file.lastModified())
add(DocumentsContract.Document.COLUMN_FLAGS, flags)
add(DocumentsContract.Document.COLUMN_SIZE, file.length())
if (file == rootDirectory) {
add(DocumentsContract.Document.COLUMN_ICON, R.drawable.ic_dolphin)
}
}
}
private fun pathToDocumentId(path: File): String {
return "$ROOT_ID/${path.toRelativeString(rootDirectory!!)}"
}
private fun documentIdToPath(documentId: String): File {
val file = File(rootDirectory, documentId.substring("$ROOT_ID/".lastIndex))
if (!file.exists()) {
throw FileNotFoundException("File $documentId does not exist.")
}
return file
}
private fun getTypeForFile(file: File): String {
return if (file.isDirectory)
DocumentsContract.Document.MIME_TYPE_DIR
else
MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension)
?: "application/octet-stream"
}
private fun findFileNameForNewFile(file: File): File {
var unusedFile = file
var i = 1
while (unusedFile.exists()) {
val pathWithoutExtension = unusedFile.absolutePath.substringBeforeLast('.')
val extension = unusedFile.absolutePath.substringAfterLast('.')
unusedFile = File("$pathWithoutExtension.$i.$extension")
i++
}
return file
}
private fun copyRecursively(src: File, dst: File): File {
val actualDst = findFileNameForNewFile(dst)
if (src.isDirectory) {
actualDst.mkdirs()
val children = src.listFiles()
if (children !== null) {
for (file in children) {
copyRecursively(file, File(actualDst, file.name))
}
}
} else {
src.copyTo(actualDst)
}
return actualDst
}
}

View File

@ -102,17 +102,22 @@ public final class DirectoryInitialization
return new File(externalPath, "dolphin-emu");
}
private static boolean setDolphinUserDirectory(Context context)
@Nullable
public static File getUserDirectoryPath(Context context)
{
if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()))
return false;
return null;
isUsingLegacyUserDirectory =
preferLegacyUserDirectory(context) && PermissionsHandler.hasWriteAccess(context);
File path = isUsingLegacyUserDirectory ?
return isUsingLegacyUserDirectory ?
getLegacyUserDirectoryPath() : context.getExternalFilesDir(null);
}
private static boolean setDolphinUserDirectory(Context context)
{
File path = DirectoryInitialization.getUserDirectoryPath(context);
if (path == null)
return false;

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="enableDocumentProvider">true</bool>
</resources>

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="hasTouch">true</bool>
<bool name="enableDocumentProvider">false</bool>
</resources>