aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/org/pacien/tincapp/storageprovider/FilesDocumentsProvider.kt222
1 files changed, 222 insertions, 0 deletions
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
19package org.pacien.tincapp.storageprovider
20
21import android.os.CancellationSignal
22import android.os.ParcelFileDescriptor
23import android.database.Cursor
24import android.database.MatrixCursor
25import android.os.Build
26import android.provider.DocumentsContract
27import android.provider.DocumentsContract.Document
28import android.provider.DocumentsContract.Root
29import android.provider.DocumentsProvider
30import androidx.annotation.RequiresApi
31import org.pacien.tincapp.R
32import org.pacien.tincapp.context.AppPaths
33import java.io.File
34import java.io.FileNotFoundException
35import kotlin.io.path.Path
36import kotlin.io.path.name
37import kotlin.io.path.relativeTo
38
39class 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
177 private fun File.pathUnder(parent: File): String =
178 canonicalPath.removePrefix(parent.canonicalPath)
179
180 private fun File.isParentOf(childCandidate: File): Boolean {
181 var parentOfChild = childCandidate.canonicalFile.parentFile
182 while (parentOfChild != null) {
183 if (parentOfChild.equals(canonicalFile)) return true
184 parentOfChild = parentOfChild.parentFile
185 }
186 return false
187 }
188
189 private fun File.documentMimeType() =
190 if (isDirectory) Document.MIME_TYPE_DIR
191 else "text/plain"
192
193 private fun File.documentPermFlags() =
194 (if (isDirectory && canWrite()) Document.FLAG_DIR_SUPPORTS_CREATE else 0) or
195 (if (isFile && canWrite()) Document.FLAG_SUPPORTS_WRITE else 0) or
196 (if (parentFile?.canWrite() == true) Document.FLAG_SUPPORTS_DELETE else 0)
197
198 private fun MatrixCursor.addFileRow(documentId: String, file: File) {
199 addRow(
200 Document.COLUMN_DOCUMENT_ID to documentId,
201 Document.COLUMN_DISPLAY_NAME to file.name,