diff options
-rw-r--r-- | app/src/main/AndroidManifest.xml | 14 | ||||
-rw-r--r-- | app/src/main/java/org/pacien/tincapp/storageprovider/FilesDocumentsProvider.kt | 222 | ||||
-rw-r--r-- | app/src/main/res/xml/file_paths.xml | 24 |
3 files changed, 260 insertions, 0 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e29bce6..e2ec0fc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml | |||
@@ -85,6 +85,20 @@ | |||
85 | </intent-filter> | 85 | </intent-filter> |
86 | </service> | 86 | </service> |
87 | 87 | ||
88 | <provider | ||
89 | android:name="org.pacien.tincapp.storageprovider.FilesDocumentsProvider" | ||
90 | android:authorities="org.pacien.tincapp.files" | ||
91 | android:exported="true" | ||
92 | android:grantUriPermissions="true" | ||
93 | android:permission="android.permission.MANAGE_DOCUMENTS"> | ||
94 | <intent-filter> | ||
95 | <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> | ||
96 | </intent-filter> | ||
97 | <meta-data | ||
98 | android:name="android.support.FILE_PROVIDER_PATHS" | ||
99 | android:resource="@xml/file_paths" /> | ||
100 | </provider> | ||
101 | |||
88 | </application> | 102 | </application> |
89 | 103 | ||
90 | </manifest> | 104 | </manifest> |
diff --git a/app/src/main/java/org/pacien/tincapp/storageprovider/FilesDocumentsProvider.kt b/app/src/main/java/org/pacien/tincapp/storageprovider/FilesDocumentsProvider.kt new file mode 100644 index 0000000..07ffaca --- /dev/null +++ b/app/src/main/java/org/pacien/tincapp/storageprovider/FilesDocumentsProvider.kt | |||
@@ -0,0 +1,222 @@ | |||
1 | /* | ||
2 | * Tinc Mesh VPN: Android client and user interface | ||
3 | * Copyright (C) 2017-2024 Euxane P. TRAN-GIRARD | ||
4 | * | ||
5 | * This program is free software: you can redistribute it and/or modify | ||
6 | * it under the terms of the GNU General Public License as published by | ||
7 | * the Free Software Foundation, either version 3 of the License, or | ||
8 | * (at your option) any later version. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | */ | ||
18 | |||
19 | package org.pacien.tincapp.storageprovider | ||
20 | |||
21 | import android.os.CancellationSignal | ||
22 | import android.os.ParcelFileDescriptor | ||
23 | import android.database.Cursor | ||
24 | import android.database.MatrixCursor | ||
25 | import android.os.Build | ||
26 | import android.provider.DocumentsContract | ||
27 | import android.provider.DocumentsContract.Document | ||
28 | import android.provider.DocumentsContract.Root | ||
29 | import android.provider.DocumentsProvider | ||
30 | import androidx.annotation.RequiresApi | ||
31 | import org.pacien.tincapp.R | ||
32 | import org.pacien.tincapp.context.AppPaths | ||
33 | import java.io.File | ||
34 | import java.io.FileNotFoundException | ||
35 | import kotlin.io.path.Path | ||
36 | import kotlin.io.path.name | ||
37 | import kotlin.io.path.relativeTo | ||
38 | |||
39 | class FilesDocumentsProvider : DocumentsProvider() { | ||
40 | companion object { | ||
41 | const val ROOT_ID = "" | ||
42 | const val ROOT_DOCUMENT_ID = "/" | ||
43 | const val VIRTUAL_ROOT_NETWORKS = "networks" | ||
44 | const val VIRTUAL_ROOT_LOG = "log" | ||
45 | |||
46 | private val DEFAULT_ROOT_PROJECTION = arrayOf( | ||
47 | Root.COLUMN_ROOT_ID, | ||
48 | Root.COLUMN_MIME_TYPES, | ||
49 | Root.COLUMN_FLAGS, | ||
50 | Root.COLUMN_ICON, | ||
51 | Root.COLUMN_TITLE, | ||
52 | Root.COLUMN_SUMMARY, | ||
53 | Root.COLUMN_DOCUMENT_ID, | ||
54 | Root.COLUMN_AVAILABLE_BYTES, | ||
55 | ) | ||
56 | |||
57 | private val DEFAULT_DOCUMENT_PROJECTION = arrayOf( | ||
58 | Document.COLUMN_DOCUMENT_ID, | ||
59 | Document.COLUMN_MIME_TYPE, | ||
60 | Document.COLUMN_DISPLAY_NAME, | ||
61 | Document.COLUMN_LAST_MODIFIED, | ||
62 | Document.COLUMN_FLAGS, | ||
63 | Document.COLUMN_SIZE, | ||
64 | ) | ||
65 | } | ||
66 | |||
67 | override fun onCreate(): Boolean = true | ||
68 | |||
69 | override fun queryRoots(projection: Array<out String>?): Cursor = | ||
70 | MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION).apply { | ||
71 | addRow( | ||
72 | Root.COLUMN_ROOT_ID to ROOT_ID, | ||
73 | Root.COLUMN_DOCUMENT_ID to ROOT_DOCUMENT_ID, | ||
74 | Root.COLUMN_FLAGS to (Root.FLAG_SUPPORTS_CREATE or Root.FLAG_SUPPORTS_IS_CHILD), | ||
75 | Root.COLUMN_MIME_TYPES to "*/*", | ||
76 | Root.COLUMN_TITLE to context!!.getString(R.string.app_name), | ||
77 | Root.COLUMN_ICON to R.mipmap.ic_launcher, | ||
78 | ) | ||
79 | } | ||
80 | |||
81 | override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor = | ||
82 | MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION).apply { | ||
83 | when (documentId) { | ||
84 | ROOT_DOCUMENT_ID -> addVirtualDirRow(ROOT_DOCUMENT_ID) | ||
85 | else -> addFileRow(documentId!!, fileForDocumentId(documentId)) | ||
86 | } | ||
87 | } | ||
88 | |||
89 | override fun queryChildDocuments( | ||
90 | parentDocumentId: String?, | ||
91 | projection: Array<out String>?, | ||
92 | sortOrder: String? | ||
93 | ): Cursor = | ||
94 | MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION).apply { | ||
95 | when (parentDocumentId) { | ||
96 | ROOT_DOCUMENT_ID -> { | ||
97 | addVirtualDirRow(VIRTUAL_ROOT_NETWORKS) | ||
98 | addVirtualDirRow(VIRTUAL_ROOT_LOG) | ||
99 | } | ||
100 | |||
101 | else -> fileForDocumentId(parentDocumentId!!).listFiles()?.forEach { | ||
102 | addFileRow(documentIdForFile(it), it) | ||
103 | } | ||
104 | } | ||
105 | } | ||
106 | |||
107 | @RequiresApi(Build.VERSION_CODES.O) | ||
108 | override fun findDocumentPath( | ||
109 | parentDocumentId: String?, | ||
110 | childDocumentId: String? | ||
111 | ): DocumentsContract.Path { | ||
112 | var childPath = Path(childDocumentId!!) | ||
113 | if (parentDocumentId != null) | ||
114 | childPath = childPath.relativeTo(Path(parentDocumentId)) | ||
115 | |||
116 | val components = childPath.asSequence().map { it.name }.toList() | ||
117 | return DocumentsContract.Path(ROOT_ID, listOf(ROOT_DOCUMENT_ID) + components) | ||
118 | } | ||
119 | |||
120 | override fun isChildDocument(parentDocumentId: String?, documentId: String?): Boolean = | ||
121 | fileForDocumentId(parentDocumentId!!).isParentOf(fileForDocumentId(documentId!!)) | ||
122 | |||
123 | override fun getDocumentType(documentId: String?): String = | ||
124 | fileForDocumentId(documentId!!).documentMimeType() | ||
125 | |||
126 | override fun deleteDocument(documentId: String?) { | ||
127 | fileForDocumentId(documentId!!).apply { | ||
128 | if (!deleteRecursively()) throw FileSystemException(this) | ||
129 | } | ||
130 | } | ||
131 | |||
132 | override fun openDocument( | ||
133 | documentId: String?, | ||
134 | mode: String?, | ||
135 | signal: CancellationSignal? | ||
136 | ): ParcelFileDescriptor = | ||
137 | ParcelFileDescriptor.open( | ||
138 | fileForDocumentId(documentId!!), | ||
139 | ParcelFileDescriptor.parseMode(mode), | ||
140 | ) | ||
141 | |||
142 | override fun createDocument( | ||
143 | parentDocumentId: String?, | ||
144 | mimeType: String?, | ||
145 | displayName: String? | ||
146 | ): String = | ||
147 | File(fileForDocumentId(parentDocumentId!!), displayName!!).apply { | ||
148 | val success = when (mimeType) { | ||
149 | Document.MIME_TYPE_DIR -> mkdir() | ||
150 | else -> createNewFile() | ||
151 | } | ||
152 | if (!success) throw FileSystemException(this) | ||
153 | }.let { | ||
154 | documentIdForFile(it) | ||
155 | } | ||
156 | |||
157 | private fun fileForDocumentId(documentId: String): File = | ||
158 | documentId.split(File.separatorChar, limit = 2).let { | ||
159 | val root = it[0] | ||
160 | val under = if (it.size >= 2) it[1] else "" | ||
161 | when (root) { | ||
162 | VIRTUAL_ROOT_NETWORKS -> File(AppPaths.confDir(), under) | ||
163 | VIRTUAL_ROOT_LOG -> File(AppPaths.logDir(), under) | ||
164 | else -> throw FileNotFoundException() | ||
165 | } | ||
166 | } | ||
167 | |||
168 | private fun documentIdForFile(file: File): String = | ||
169 | if (AppPaths.confDir().isParentOf(file)) { | ||
170 | File(VIRTUAL_ROOT_NETWORKS, file.pathUnder(AppPaths.confDir())).path | ||
171 | } else if (AppPaths.logDir().isParentOf(file)) { | ||
172 | File(VIRTUAL_ROOT_LOG, file.pathUnder(AppPaths.logDir())).path | ||
173 | } else { | ||
174 | throw IllegalArgumentException() | ||
175 | } | ||
176 | |||