summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/android/app/src/main/AndroidManifest.xml12
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt11
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/DocumentProvider.kt300
3 files changed, 319 insertions, 4 deletions
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index 7d3eccc5c..c37559b47 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -74,6 +74,18 @@
74 android:name="android.support.FILE_PROVIDER_PATHS" 74 android:name="android.support.FILE_PROVIDER_PATHS"
75 android:resource="@xml/nnf_provider_paths" /> 75 android:resource="@xml/nnf_provider_paths" />
76 </provider> 76 </provider>
77
78 <provider
79 android:name=".features.DocumentProvider"
80 android:authorities="${applicationId}.user"
81 android:grantUriPermissions="true"
82 android:exported="true"
83 android:permission="android.permission.MANAGE_DOCUMENTS">
84 <intent-filter>
85 <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
86 </intent-filter>
87 </provider>
88
77 </application> 89 </application>
78 90
79</manifest> 91</manifest>
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
index 273d4951a..a0c5c5c25 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
@@ -8,9 +8,12 @@ import android.app.NotificationChannel
8import android.app.NotificationManager 8import android.app.NotificationManager
9import android.content.Context 9import android.content.Context
10import org.yuzu.yuzu_emu.model.GameDatabase 10import org.yuzu.yuzu_emu.model.GameDatabase
11import org.yuzu.yuzu_emu.utils.DirectoryInitialization.start 11import org.yuzu.yuzu_emu.utils.DirectoryInitialization
12import org.yuzu.yuzu_emu.utils.DocumentsTree 12import org.yuzu.yuzu_emu.utils.DocumentsTree
13import org.yuzu.yuzu_emu.utils.GpuDriverHelper 13import org.yuzu.yuzu_emu.utils.GpuDriverHelper
14import java.io.File
15
16fun Context.getPublicFilesDir() : File = getExternalFilesDir(null) ?: filesDir
14 17
15class YuzuApplication : Application() { 18class YuzuApplication : Application() {
16 private fun createNotificationChannel() { 19 private fun createNotificationChannel() {
@@ -36,7 +39,7 @@ class YuzuApplication : Application() {
36 super.onCreate() 39 super.onCreate()
37 application = this 40 application = this
38 documentsTree = DocumentsTree() 41 documentsTree = DocumentsTree()
39 start(applicationContext) 42 DirectoryInitialization.start(applicationContext)
40 GpuDriverHelper.initializeDriverParameters(applicationContext) 43 GpuDriverHelper.initializeDriverParameters(applicationContext)
41 NativeLibrary.LogDeviceInfo() 44 NativeLibrary.LogDeviceInfo()
42 45
@@ -50,10 +53,10 @@ class YuzuApplication : Application() {
50 53
51 @JvmField 54 @JvmField
52 var documentsTree: DocumentsTree? = null 55 var documentsTree: DocumentsTree? = null
53 private var application: YuzuApplication? = null 56 lateinit var application: YuzuApplication
54 57
55 @JvmStatic 58 @JvmStatic
56 val appContext: Context 59 val appContext: Context
57 get() = application!!.applicationContext 60 get() = application.applicationContext
58 } 61 }
59} 62}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/DocumentProvider.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/DocumentProvider.kt
new file mode 100644
index 000000000..e6e9a6fe8
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/DocumentProvider.kt
@@ -0,0 +1,300 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4// SPDX-License-Identifier: MPL-2.0
5// Copyright © 2023 Skyline Team and Contributors (https://github.com/skyline-emu/)
6
7package org.yuzu.yuzu_emu.features
8
9import android.database.Cursor
10import android.database.MatrixCursor
11import android.os.CancellationSignal
12import android.os.ParcelFileDescriptor
13import android.provider.DocumentsContract
14import android.provider.DocumentsProvider
15import android.webkit.MimeTypeMap
16import org.yuzu.yuzu_emu.R
17import org.yuzu.yuzu_emu.YuzuApplication
18import org.yuzu.yuzu_emu.getPublicFilesDir
19import java.io.*
20
21class DocumentProvider : DocumentsProvider() {
22 private val baseDirectory: File
23 get() = File(YuzuApplication.application.getPublicFilesDir().canonicalPath)
24
25 companion object {
26 private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
27 DocumentsContract.Root.COLUMN_ROOT_ID,
28 DocumentsContract.Root.COLUMN_MIME_TYPES,
29 DocumentsContract.Root.COLUMN_FLAGS,
30 DocumentsContract.Root.COLUMN_ICON,
31 DocumentsContract.Root.COLUMN_TITLE,
32 DocumentsContract.Root.COLUMN_SUMMARY,
33 DocumentsContract.Root.COLUMN_DOCUMENT_ID,
34 DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
35 )
36
37 private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf(
38 DocumentsContract.Document.COLUMN_DOCUMENT_ID,
39 DocumentsContract.Document.COLUMN_MIME_TYPE,
40 DocumentsContract.Document.COLUMN_DISPLAY_NAME,
41 DocumentsContract.Document.COLUMN_LAST_MODIFIED,
42 DocumentsContract.Document.COLUMN_FLAGS,
43 DocumentsContract.Document.COLUMN_SIZE
44 )
45
46 const val ROOT_ID: String = "root"
47 }
48
49 override fun onCreate(): Boolean {
50 return true
51 }
52
53 /**
54 * @return The [File] that corresponds to the document ID supplied by [getDocumentId]
55 */
56 private fun getFile(documentId: String): File {
57 if (documentId.startsWith(ROOT_ID)) {
58 val file = baseDirectory.resolve(documentId.drop(ROOT_ID.length + 1))
59 if (!file.exists()) throw FileNotFoundException("${file.absolutePath} ($documentId) not found")
60 return file
61 } else {
62 throw FileNotFoundException("'$documentId' is not in any known root")
63 }
64 }
65
66 /**
67 * @return A unique ID for the provided [File]
68 */
69 private fun getDocumentId(file: File): String {
70 return "$ROOT_ID/${file.toRelativeString(baseDirectory)}"
71 }
72
73 override fun queryRoots(projection: Array<out String>?): Cursor {
74 val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION)
75
76 cursor.newRow().apply {
77 add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT_ID)
78 add(DocumentsContract.Root.COLUMN_SUMMARY, null)
79 add(
80 DocumentsContract.Root.COLUMN_FLAGS,
81 DocumentsContract.Root.FLAG_SUPPORTS_CREATE or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
82 )
83 add(DocumentsContract.Root.COLUMN_TITLE, context!!.getString(R.string.app_name))
84 add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocumentId(baseDirectory))
85 add(DocumentsContract.Root.COLUMN_MIME_TYPES, "*/*")
86 add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDirectory.freeSpace)
87 add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_yuzu)
88 }
89
90 return cursor
91 }
92
93 override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor {
94 val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
95 return includeFile(cursor, documentId, null)
96 }
97
98 override fun isChildDocument(parentDocumentId: String?, documentId: String?): Boolean {
99 return documentId?.startsWith(parentDocumentId!!) ?: false
100 }
101
102 /**
103 * @return A new [File] with a unique name based off the supplied [name], not conflicting with any existing file
104 */
105 private fun File.resolveWithoutConflict(name: String): File {
106 var file = resolve(name)
107 if (file.exists()) {
108 var noConflictId =
109 1 // Makes sure two files don't have the same name by adding a number to the end
110 val extension = name.substringAfterLast('.')
111 val baseName = name.substringBeforeLast('.')
112 while (file.exists())
113 file = resolve("$baseName (${noConflictId++}).$extension")
114 }
115 return file
116 }
117
118 override fun createDocument(
119 parentDocumentId: String?,
120 mimeType: String?,
121 displayName: String
122 ): String {
123 val parentFile = getFile(parentDocumentId!!)
124 val newFile = parentFile.resolveWithoutConflict(displayName)
125
126 try {
127 if (DocumentsContract.Document.MIME_TYPE_DIR == mimeType) {
128 if (!newFile.mkdir())
129 throw IOException("Failed to create directory")
130 } else {
131 if (!newFile.createNewFile())
132 throw IOException("Failed to create file")
133 }
134 } catch (e: IOException) {
135 throw FileNotFoundException("Couldn't create document '${newFile.path}': ${e.message}")
136 }
137
138 return getDocumentId(newFile)
139 }
140
141 override fun deleteDocument(documentId: String?) {
142 val file = getFile(documentId!!)
143 if (!file.delete())
144 throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
145 }
146
147 override fun removeDocument(documentId: String, parentDocumentId: String?) {
148 val parent = getFile(parentDocumentId!!)
149 val file = getFile(documentId)
150
151 if (parent == file || file.parentFile == null || file.parentFile!! == parent) {
152 if (!file.delete())
153 throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
154 } else {
155 throw FileNotFoundException("Couldn't delete document with ID '$documentId'")
156 }
157 }
158
159 override fun renameDocument(documentId: String?, displayName: String?): String {
160 if (displayName == null)
161 throw FileNotFoundException("Couldn't rename document '$documentId' as the new name is null")
162
163 val sourceFile = getFile(documentId!!)
164 val sourceParentFile = sourceFile.parentFile
165 ?: throw FileNotFoundException("Couldn't rename document '$documentId' as it has no parent")
166 val destFile = sourceParentFile.resolve(displayName)
167
168 try {
169 if (!sourceFile.renameTo(destFile))
170 throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}'")
171 } catch (e: Exception) {
172 throw FileNotFoundException("Couldn't rename document from '${sourceFile.name}' to '${destFile.name}': ${e.message}")
173 }
174
175 return getDocumentId(destFile)
176 }
177
178 private fun copyDocument(
179 sourceDocumentId: String, sourceParentDocumentId: String,
180 targetParentDocumentId: String?
181 ): String {
182 if (!isChildDocument(sourceParentDocumentId, sourceDocumentId))
183 throw FileNotFoundException("Couldn't copy document '$sourceDocumentId' as its parent is not '$sourceParentDocumentId'")
184
185 return copyDocument(sourceDocumentId, targetParentDocumentId)
186 }
187
188 override fun copyDocument(sourceDocumentId: String, targetParentDocumentId: String?): String {
189 val parent = getFile(targetParentDocumentId!!)
190 val oldFile = getFile(sourceDocumentId)
191 val newFile = parent.resolveWithoutConflict(oldFile.name)
192
193 try {
194 if (!(newFile.createNewFile() && newFile.setWritable(true) && newFile.setReadable(true)))
195 throw IOException("Couldn't create new file")
196
197 FileInputStream(oldFile).use { inStream ->
198 FileOutputStream(newFile).use { outStream ->
199 inStream.copyTo(outStream)
200 }
201 }
202 } catch (e: IOException) {
203 throw FileNotFoundException("Couldn't copy document '$sourceDocumentId': ${e.message}")
204 }
205
206 return getDocumentId(newFile)
207 }
208
209 override fun moveDocument(
210 sourceDocumentId: String, sourceParentDocumentId: String?,
211 targetParentDocumentId: String?
212 ): String {
213 try {
214 val newDocumentId = copyDocument(
215 sourceDocumentId, sourceParentDocumentId!!,
216 targetParentDocumentId
217 )
218 removeDocument(sourceDocumentId, sourceParentDocumentId)
219 return newDocumentId
220 } catch (e: FileNotFoundException) {
221 throw FileNotFoundException("Couldn't move document '$sourceDocumentId'")
222 }
223 }
224
225 private fun includeFile(cursor: MatrixCursor, documentId: String?, file: File?): MatrixCursor {
226 val localDocumentId = documentId ?: file?.let { getDocumentId(it) }
227 val localFile = file ?: getFile(documentId!!)
228
229 var flags = 0
230 if (localFile.isDirectory && localFile.canWrite()) {
231 flags = DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE
232 } else if (localFile.canWrite()) {
233 flags = DocumentsContract.Document.FLAG_SUPPORTS_WRITE
234 flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE
235
236 flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_REMOVE
237 flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_MOVE
238 flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_COPY
239 flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_RENAME
240 }
241
242 cursor.newRow().apply {
243 add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, localDocumentId)
244 add(
245 DocumentsContract.Document.COLUMN_DISPLAY_NAME,
246 if (localFile == baseDirectory) context!!.getString(R.string.app_name) else localFile.name
247 )
248 add(DocumentsContract.Document.COLUMN_SIZE, localFile.length())
249 add(DocumentsContract.Document.COLUMN_MIME_TYPE, getTypeForFile(localFile))
250 add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, localFile.lastModified())
251 add(DocumentsContract.Document.COLUMN_FLAGS, flags)
252 if (localFile == baseDirectory)
253 add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_yuzu)
254 }
255
256 return cursor
257 }
258
259 private fun getTypeForFile(file: File): Any {
260 return if (file.isDirectory)
261 DocumentsContract.Document.MIME_TYPE_DIR
262 else
263 getTypeForName(file.name)
264 }
265
266 private fun getTypeForName(name: String): Any {
267 val lastDot = name.lastIndexOf('.')
268 if (lastDot >= 0) {
269 val extension = name.substring(lastDot + 1)
270 val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
271 if (mime != null)
272 return mime
273 }
274 return "application/octect-stream"
275 }
276
277 override fun queryChildDocuments(
278 parentDocumentId: String?,
279 projection: Array<out String>?,
280 sortOrder: String?
281 ): Cursor {
282 var cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
283
284 val parent = getFile(parentDocumentId!!)
285 for (file in parent.listFiles()!!)
286 cursor = includeFile(cursor, null, file)
287
288 return cursor
289 }
290
291 override fun openDocument(
292 documentId: String?,
293 mode: String?,
294 signal: CancellationSignal?
295 ): ParcelFileDescriptor {
296 val file = documentId?.let { getFile(it) }
297 val accessMode = ParcelFileDescriptor.parseMode(mode)
298 return ParcelFileDescriptor.open(file, accessMode)
299 }
300}