android: Use Skyline's document provider
This commit is contained in:
parent
55e4c2d87b
commit
515f3deea1
|
@ -74,6 +74,18 @@
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/nnf_provider_paths" />
|
android:resource="@xml/nnf_provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name=".features.DocumentProvider"
|
||||||
|
android:authorities="${applicationId}.user"
|
||||||
|
android:grantUriPermissions="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||||
|
</intent-filter>
|
||||||
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -8,9 +8,12 @@ import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import org.yuzu.yuzu_emu.model.GameDatabase
|
import org.yuzu.yuzu_emu.model.GameDatabase
|
||||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization.start
|
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||||
import org.yuzu.yuzu_emu.utils.DocumentsTree
|
import org.yuzu.yuzu_emu.utils.DocumentsTree
|
||||||
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
fun Context.getPublicFilesDir() : File = getExternalFilesDir(null) ?: filesDir
|
||||||
|
|
||||||
class YuzuApplication : Application() {
|
class YuzuApplication : Application() {
|
||||||
private fun createNotificationChannel() {
|
private fun createNotificationChannel() {
|
||||||
|
@ -36,7 +39,7 @@ class YuzuApplication : Application() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
application = this
|
application = this
|
||||||
documentsTree = DocumentsTree()
|
documentsTree = DocumentsTree()
|
||||||
start(applicationContext)
|
DirectoryInitialization.start(applicationContext)
|
||||||
GpuDriverHelper.initializeDriverParameters(applicationContext)
|
GpuDriverHelper.initializeDriverParameters(applicationContext)
|
||||||
NativeLibrary.LogDeviceInfo()
|
NativeLibrary.LogDeviceInfo()
|
||||||
|
|
||||||
|
@ -50,10 +53,10 @@ class YuzuApplication : Application() {
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
var documentsTree: DocumentsTree? = null
|
var documentsTree: DocumentsTree? = null
|
||||||
private var application: YuzuApplication? = null
|
lateinit var application: YuzuApplication
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
val appContext: Context
|
val appContext: Context
|
||||||
get() = application!!.applicationContext
|
get() = application.applicationContext
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,300 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
// Copyright © 2023 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.features
|
||||||
|
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.database.MatrixCursor
|
||||||
|
import android.os.CancellationSignal
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import android.provider.DocumentsProvider
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
|
import org.yuzu.yuzu_emu.getPublicFilesDir
|
||||||
|
import java.io.*
|
||||||
|
|
||||||
|
class DocumentProvider : DocumentsProvider() {
|
||||||
|
private val baseDirectory: File
|
||||||
|
get() = File(YuzuApplication.application.getPublicFilesDir().canonicalPath)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
|
||||||
|
DocumentsContract.Root.COLUMN_ROOT_ID,
|
||||||
|
DocumentsContract.Root.COLUMN_MIME_TYPES,
|
||||||
|
DocumentsContract.Root.COLUMN_FLAGS,
|
||||||
|
DocumentsContract.Root.COLUMN_ICON,
|
||||||
|
DocumentsContract.Root.COLUMN_TITLE,
|
||||||
|
DocumentsContract.Root.COLUMN_SUMMARY,
|
||||||
|
DocumentsContract.Root.COLUMN_DOCUMENT_ID,
|
||||||
|
DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
const val ROOT_ID: String = "root"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The [File] that corresponds to the document ID supplied by [getDocumentId]
|
||||||
|
*/
|
||||||
|
private fun getFile(documentId: String): File {
|
||||||
|
if (documentId.startsWith(ROOT_ID)) {
|
||||||
|
val file = baseDirectory.resolve(documentId.drop(ROOT_ID.length + 1))
|
||||||
|
if (!file.exists()) throw FileNotFoundException("${file.absolutePath} ($documentId) not found")
|
||||||
|
return file
|
||||||
|
} else {
|
||||||
|
throw FileNotFoundException("'$documentId' is not in any known root")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A unique ID for the provided [File]
|
||||||
|
*/
|
||||||
|
private fun getDocumentId(file: File): String {
|
||||||
|
return "$ROOT_ID/${file.toRelativeString(baseDirectory)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun queryRoots(projection: Array<out String>?): Cursor {
|
||||||
|
val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION)
|
||||||
|
|
||||||
|
cursor.newRow().apply {
|
||||||
|
add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT_ID)
|
||||||
|
add(DocumentsContract.Root.COLUMN_SUMMARY, null)
|
||||||
|
add(
|
||||||
|
DocumentsContract.Root.COLUMN_FLAGS,
|
||||||
|
DocumentsContract.Root.FLAG_SUPPORTS_CREATE or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
|
||||||
|
)
|
||||||
|
add(DocumentsContract.Root.COLUMN_TITLE, context!!.getString(R.string.app_name))
|
||||||
|
add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocumentId(baseDirectory))
|
||||||
|
add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*")
|
||||||
|
add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDirectory.freeSpace)
|
||||||
|
add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_yuzu)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor {
|
||||||
|
val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
|
||||||
|
return includeFile(cursor, documentId, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isChildDocument(parentDocumentId: String?, documentId: String?): Boolean {
|
||||||
|
return documentId?.startsWith(parentDocumentId!!) ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A new [File] with a unique name based off the supplied [name], not conflicting with any existing file
|
||||||
|
*/
|
||||||
|
private fun File.resolveWithoutConflict(name: String): File {
|
||||||
|
var file = resolve(name)
|
||||||
|
if (file.exists()) {
|
||||||
|
var noConflictId =
|
||||||
|
1 // Makes sure two files don't have the same name by adding a number to the end
|
||||||
|
val extension = name.substringAfterLast('.')
|
||||||
|
val baseName = name.substringBeforeLast('.')
|
||||||
|
while (file.exists())
|
||||||
|
file = resolve("$baseName (${noConflictId++}).$extension")
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createDocument(
|
||||||
|
parentDocumentId: String?,
|
||||||
|
mimeType: String?,
|
||||||
|
displayName: String
|
||||||
|
): String {
|
||||||
|
val parentFile = getFile(parentDocumentId!!)
|
||||||
|
val newFile = parentFile.resolveWithoutConflict(displayName)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (DocumentsContract.Document.MIME_TYPE_DIR == mimeType) {
|
||||||
|
if (!newFile.mkdir())
|
||||||
|
throw IOException("Failed to create directory")
|
||||||
|
} else {
|
||||||
|
if (!newFile.createNewFile())
|
||||||
|
throw IOException("Failed to create file")
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw FileNotFoundException("Couldn't create document '${newFile.path}': ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDocumentId(newFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteDocument(documentId: String?) {
|
||||||
|
val file = getFile(documentId!!)
|
||||||
|
if (!file.delete())
|
||||||
|
throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun removeDocument(documentId: String, parentDocumentId: String?) {
|
||||||
|
val parent = getFile(parentDocumentId!!)
|
||||||
|
val file = getFile(documentId)
|
||||||
|
|
||||||
|
if (parent == file || file.parentFile == null || file.parentFile!! == parent) {
|
||||||
|
if (!file.delete())
|
||||||
|
throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
|
||||||
|
} else {
|
||||||
|
throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun renameDocument(documentId: String?, displayName: String?): String {
|
||||||
|
if (displayName == null)
|
||||||
|
throw FileNotFoundException("Couldn't rename document '$documentId' as the new name is null")
|
||||||
|
|
||||||
|
val sourceFile = getFile(documentId!!)
|
||||||
|
val sourceParentFile = sourceFile.parentFile
|
||||||
|
?: throw FileNotFoundException("Couldn't rename document '$documentId' as it has no parent")
|
||||||
|
val destFile = sourceParentFile.resolve(displayName)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!sourceFile.renameTo(destFile))
|
||||||
|
throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}'")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}': ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDocumentId(destFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyDocument(
|
||||||
|
sourceDocumentId: String, sourceParentDocumentId: String,
|
||||||
|
targetParentDocumentId: String?
|
||||||
|
): String {
|
||||||
|
if (!isChildDocument(sourceParentDocumentId, sourceDocumentId))
|
||||||
|
throw FileNotFoundException("Couldn't copy document '$sourceDocumentId' as its parent is not '$sourceParentDocumentId'")
|
||||||
|
|
||||||
|
return copyDocument(sourceDocumentId, targetParentDocumentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun copyDocument(sourceDocumentId: String, targetParentDocumentId: String?): String {
|
||||||
|
val parent = getFile(targetParentDocumentId!!)
|
||||||
|
val oldFile = getFile(sourceDocumentId)
|
||||||
|
val newFile = parent.resolveWithoutConflict(oldFile.name)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!(newFile.createNewFile() && newFile.setWritable(true) && newFile.setReadable(true)))
|
||||||
|
throw IOException("Couldn't create new file")
|
||||||
|
|
||||||
|
FileInputStream(oldFile).use { inStream ->
|
||||||
|
FileOutputStream(newFile).use { outStream ->
|
||||||
|
inStream.copyTo(outStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw FileNotFoundException("Couldn't copy document '$sourceDocumentId': ${e.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
return getDocumentId(newFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun moveDocument(
|
||||||
|
sourceDocumentId: String, sourceParentDocumentId: String?,
|
||||||
|
targetParentDocumentId: String?
|
||||||
|
): String {
|
||||||
|
try {
|
||||||
|
val newDocumentId = copyDocument(
|
||||||
|
sourceDocumentId, sourceParentDocumentId!!,
|
||||||
|
targetParentDocumentId
|
||||||
|
)
|
||||||
|
removeDocument(sourceDocumentId, sourceParentDocumentId)
|
||||||
|
return newDocumentId
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
throw FileNotFoundException("Couldn't move document '$sourceDocumentId'")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun includeFile(cursor: MatrixCursor, documentId: String?, file: File?): MatrixCursor {
|
||||||
|
val localDocumentId = documentId ?: file?.let { getDocumentId(it) }
|
||||||
|
val localFile = file ?: getFile(documentId!!)
|
||||||
|
|
||||||
|
var flags = 0
|
||||||
|
if (localFile.isDirectory && localFile.canWrite()) {
|
||||||
|
flags = DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE
|
||||||
|
} else if (localFile.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
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.newRow().apply {
|
||||||
|
add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, localDocumentId)
|
||||||
|
add(
|
||||||
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||||
|
if (localFile == baseDirectory) context!!.getString(R.string.app_name) else localFile.name
|
||||||
|
)
|
||||||
|
add(DocumentsContract.Document.COLUMN_SIZE, localFile.length())
|
||||||
|
add(DocumentsContract.Document.COLUMN_MIME_TYPE, getTypeForFile(localFile))
|
||||||
|
add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, localFile.lastModified())
|
||||||
|
add(DocumentsContract.Document.COLUMN_FLAGS, flags)
|
||||||
|
if (localFile == baseDirectory)
|
||||||
|
add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_yuzu)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTypeForFile(file: File): Any {
|
||||||
|
return if (file.isDirectory)
|
||||||
|
DocumentsContract.Document.MIME_TYPE_DIR
|
||||||
|
else
|
||||||
|
getTypeForName(file.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTypeForName(name: String): Any {
|
||||||
|
val lastDot = name.lastIndexOf('.')
|
||||||
|
if (lastDot >= 0) {
|
||||||
|
val extension = name.substring(lastDot + 1)
|
||||||
|
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||||
|
if (mime != null)
|
||||||
|
return mime
|
||||||
|
}
|
||||||
|
return "application/octect-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun queryChildDocuments(
|
||||||
|
parentDocumentId: String?,
|
||||||
|
projection: Array<out String>?,
|
||||||
|
sortOrder: String?
|
||||||
|
): Cursor {
|
||||||
|
var cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
|
||||||
|
|
||||||
|
val parent = getFile(parentDocumentId!!)
|
||||||
|
for (file in parent.listFiles()!!)
|
||||||
|
cursor = includeFile(cursor, null, file)
|
||||||
|
|
||||||
|
return cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openDocument(
|
||||||
|
documentId: String?,
|
||||||
|
mode: String?,
|
||||||
|
signal: CancellationSignal?
|
||||||
|
): ParcelFileDescriptor {
|
||||||
|
val file = documentId?.let { getFile(it) }
|
||||||
|
val accessMode = ParcelFileDescriptor.parseMode(mode)
|
||||||
|
return ParcelFileDescriptor.open(file, accessMode)
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue