From b33eee5c34c75688c96d6cd874d82e55dab30491 Mon Sep 17 00:00:00 2001
From: euxane
Date: Wed, 18 Sep 2024 23:41:13 +0200
Subject: storage/provider: expose config and log dirs through
DocumentsProvider
---
app/src/main/AndroidManifest.xml | 14 ++
.../storageprovider/FilesDocumentsProvider.kt | 222 +++++++++++++++++++++
app/src/main/res/xml/file_paths.xml | 24 +++
3 files changed, 260 insertions(+)
create mode 100644 app/src/main/java/org/pacien/tincapp/storageprovider/FilesDocumentsProvider.kt
create mode 100644 app/src/main/res/xml/file_paths.xml
(limited to 'app/src/main')
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 @@
+
+
+
+
+
+
+
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 @@
+/*
+ * Tinc Mesh VPN: Android client and user interface
+ * Copyright (C) 2017-2024 Euxane P. TRAN-GIRARD
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.pacien.tincapp.storageprovider
+
+import android.os.CancellationSignal
+import android.os.ParcelFileDescriptor
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.os.Build
+import android.provider.DocumentsContract
+import android.provider.DocumentsContract.Document
+import android.provider.DocumentsContract.Root
+import android.provider.DocumentsProvider
+import androidx.annotation.RequiresApi
+import org.pacien.tincapp.R
+import org.pacien.tincapp.context.AppPaths
+import java.io.File
+import java.io.FileNotFoundException
+import kotlin.io.path.Path
+import kotlin.io.path.name
+import kotlin.io.path.relativeTo
+
+class FilesDocumentsProvider : DocumentsProvider() {
+ companion object {
+ const val ROOT_ID = ""
+ const val ROOT_DOCUMENT_ID = "/"
+ const val VIRTUAL_ROOT_NETWORKS = "networks"
+ const val VIRTUAL_ROOT_LOG = "log"
+
+ private val DEFAULT_ROOT_PROJECTION = arrayOf(
+ Root.COLUMN_ROOT_ID,
+ Root.COLUMN_MIME_TYPES,
+ Root.COLUMN_FLAGS,
+ Root.COLUMN_ICON,
+ Root.COLUMN_TITLE,
+ Root.COLUMN_SUMMARY,
+ Root.COLUMN_DOCUMENT_ID,
+ Root.COLUMN_AVAILABLE_BYTES,
+ )
+
+ private val DEFAULT_DOCUMENT_PROJECTION = arrayOf(
+ Document.COLUMN_DOCUMENT_ID,
+ Document.COLUMN_MIME_TYPE,
+ Document.COLUMN_DISPLAY_NAME,
+ Document.COLUMN_LAST_MODIFIED,
+ Document.COLUMN_FLAGS,
+ Document.COLUMN_SIZE,
+ )
+ }
+
+ override fun onCreate(): Boolean = true
+
+ override fun queryRoots(projection: Array?): Cursor =
+ MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION).apply {
+ addRow(
+ Root.COLUMN_ROOT_ID to ROOT_ID,
+ Root.COLUMN_DOCUMENT_ID to ROOT_DOCUMENT_ID,
+ Root.COLUMN_FLAGS to (Root.FLAG_SUPPORTS_CREATE or Root.FLAG_SUPPORTS_IS_CHILD),
+ Root.COLUMN_MIME_TYPES to "*/*",
+ Root.COLUMN_TITLE to context!!.getString(R.string.app_name),
+ Root.COLUMN_ICON to R.mipmap.ic_launcher,
+ )
+ }
+
+ override fun queryDocument(documentId: String?, projection: Array?): Cursor =
+ MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION).apply {
+ when (documentId) {
+ ROOT_DOCUMENT_ID -> addVirtualDirRow(ROOT_DOCUMENT_ID)
+ else -> addFileRow(documentId!!, fileForDocumentId(documentId))
+ }
+ }
+
+ override fun queryChildDocuments(
+ parentDocumentId: String?,
+ projection: Array?,
+ sortOrder: String?
+ ): Cursor =
+ MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION).apply {
+ when (parentDocumentId) {
+ ROOT_DOCUMENT_ID -> {
+ addVirtualDirRow(VIRTUAL_ROOT_NETWORKS)
+ addVirtualDirRow(VIRTUAL_ROOT_LOG)
+ }
+
+ else -> fileForDocumentId(parentDocumentId!!).listFiles()?.forEach {
+ addFileRow(documentIdForFile(it), it)
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ override fun findDocumentPath(
+ parentDocumentId: String?,
+ childDocumentId: String?
+ ): DocumentsContract.Path {
+ var childPath = Path(childDocumentId!!)
+ if (parentDocumentId != null)
+ childPath = childPath.relativeTo(Path(parentDocumentId))
+
+ val components = childPath.asSequence().map { it.name }.toList()
+ return DocumentsContract.Path(ROOT_ID, listOf(ROOT_DOCUMENT_ID) + components)
+ }
+
+ override fun isChildDocument(parentDocumentId: String?, documentId: String?): Boolean =
+ fileForDocumentId(parentDocumentId!!).isParentOf(fileForDocumentId(documentId!!))
+
+ override fun getDocumentType(documentId: String?): String =
+ fileForDocumentId(documentId!!).documentMimeType()
+
+ override fun deleteDocument(documentId: String?) {
+ fileForDocumentId(documentId!!).apply {
+ if (!deleteRecursively()) throw FileSystemException(this)
+ }
+ }
+
+ override fun openDocument(
+ documentId: String?,
+ mode: String?,
+ signal: CancellationSignal?
+ ): ParcelFileDescriptor =
+ ParcelFileDescriptor.open(
+ fileForDocumentId(documentId!!),
+ ParcelFileDescriptor.parseMode(mode),
+ )
+
+ override fun createDocument(
+ parentDocumentId: String?,
+ mimeType: String?,
+ displayName: String?
+ ): String =
+ File(fileForDocumentId(parentDocumentId!!), displayName!!).apply {
+ val success = when (mimeType) {
+ Document.MIME_TYPE_DIR -> mkdir()
+ else -> createNewFile()
+ }
+ if (!success) throw FileSystemException(this)
+ }.let {
+ documentIdForFile(it)
+ }
+
+ private fun fileForDocumentId(documentId: String): File =
+ documentId.split(File.separatorChar, limit = 2).let {
+ val root = it[0]
+ val under = if (it.size >= 2) it[1] else ""
+ when (root) {
+ VIRTUAL_ROOT_NETWORKS -> File(AppPaths.confDir(), under)
+ VIRTUAL_ROOT_LOG -> File(AppPaths.logDir(), under)
+ else -> throw FileNotFoundException()
+ }
+ }
+
+ private fun documentIdForFile(file: File): String =
+ if (AppPaths.confDir().isParentOf(file)) {
+ File(VIRTUAL_ROOT_NETWORKS, file.pathUnder(AppPaths.confDir())).path
+ } else if (AppPaths.logDir().isParentOf(file)) {
+ File(VIRTUAL_ROOT_LOG, file.pathUnder(AppPaths.logDir())).path
+ } else {
+ throw IllegalArgumentException()
+ }
+
+ private fun File.pathUnder(parent: File): String =
+ canonicalPath.removePrefix(parent.canonicalPath)
+
+ private fun File.isParentOf(childCandidate: File): Boolean {
+ var parentOfChild = childCandidate.canonicalFile.parentFile
+ while (parentOfChild != null) {
+ if (parentOfChild.equals(canonicalFile)) return true
+ parentOfChild = parentOfChild.parentFile
+ }
+ return false
+ }
+
+ private fun File.documentMimeType() =
+ if (isDirectory) Document.MIME_TYPE_DIR
+ else "text/plain"
+
+ private fun File.documentPermFlags() =
+ (if (isDirectory && canWrite()) Document.FLAG_DIR_SUPPORTS_CREATE else 0) or
+ (if (isFile && canWrite()) Document.FLAG_SUPPORTS_WRITE else 0) or
+ (if (parentFile?.canWrite() == true) Document.FLAG_SUPPORTS_DELETE else 0)
+
+ private fun MatrixCursor.addFileRow(documentId: String, file: File) {
+ addRow(
+ Document.COLUMN_DOCUMENT_ID to documentId,
+ Document.COLUMN_DISPLAY_NAME to file.name,
+ Document.COLUMN_SIZE to file.length(),
+ Document.COLUMN_LAST_MODIFIED to file.lastModified(),
+ Document.COLUMN_MIME_TYPE to file.documentMimeType(),
+ Document.COLUMN_FLAGS to file.documentPermFlags(),
+ )
+ }
+
+ private fun MatrixCursor.addVirtualDirRow(documentId: String) {
+ addRow(
+ Document.COLUMN_DOCUMENT_ID to documentId,
+ Document.COLUMN_DISPLAY_NAME to documentId,
+ Document.COLUMN_MIME_TYPE to Document.MIME_TYPE_DIR,
+ Document.COLUMN_FLAGS to Document.FLAG_DIR_SUPPORTS_CREATE,
+ )
+ }
+
+ private fun MatrixCursor.addRow(vararg pairs: Pair) {
+ val row = newRow()
+ pairs.forEach { row.add(it.first, it.second) }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..98a441b
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
--
cgit v1.2.3