diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
index 2d67425..189d9c1 100644
--- a/app/CMakeLists.txt
+++ b/app/CMakeLists.txt
@@ -5,7 +5,8 @@ project(DroidFS)
option(GOCRYPTFS "build libgocryptfs" ON)
option(CRYFS "build libcryfs" ON)
-add_library(memfile SHARED src/main/native/memfile.cpp)
+add_library(memfile SHARED src/main/native/memfile.c)
+target_link_libraries(memfile log)
if (GOCRYPTFS)
add_library(gocryptfs SHARED IMPORTED)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c4ed4c4..46b3699 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,10 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto">
-
-
@@ -68,16 +64,21 @@
+ android:name=".content_providers.TemporaryFileProvider"
+ android:authorities="${applicationId}.temporary_provider"
+ android:exported="true"/>
+ android:grantUriPermissions="true"
+ android:permission="android.permission.MANAGE_DOCUMENTS">
+
+
+
+
+
diff --git a/app/src/main/java/sushi/hardcore/droidfs/EncryptedFileProvider.kt b/app/src/main/java/sushi/hardcore/droidfs/EncryptedFileProvider.kt
new file mode 100644
index 0000000..5a667b6
--- /dev/null
+++ b/app/src/main/java/sushi/hardcore/droidfs/EncryptedFileProvider.kt
@@ -0,0 +1,225 @@
+package sushi.hardcore.droidfs
+
+import android.app.ActivityManager
+import android.content.Context
+import android.os.Build
+import android.os.Handler
+import android.os.ParcelFileDescriptor
+import android.system.Os
+import android.util.Log
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import sushi.hardcore.droidfs.filesystems.EncryptedVolume
+import sushi.hardcore.droidfs.util.Compat
+import sushi.hardcore.droidfs.util.Wiper
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.io.InputStream
+import java.util.UUID
+
+class EncryptedFileProvider(context: Context) {
+ companion object {
+ private const val TAG = "EncryptedFileProvider"
+ fun getTmpFilesDir(context: Context) = File(context.cacheDir, "tmp")
+ }
+
+ private val memoryInfo = ActivityManager.MemoryInfo()
+ private val isMemFileSupported = Compat.isMemFileSupported()
+ private val tmpFilesDir by lazy { getTmpFilesDir(context) }
+ private val handler by lazy { Handler(context.mainLooper) }
+
+ init {
+ (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(
+ memoryInfo
+ )
+ }
+
+ class ExportedDiskFile private constructor(
+ path: String,
+ private val file: File,
+ private val handler: Handler
+ ) : ExportedFile(path) {
+ companion object {
+ fun create(path: String, tmpFilesDir: File, handler: Handler): ExportedDiskFile? {
+ val uuid = UUID.randomUUID().toString()
+ val file = File(tmpFilesDir, uuid)
+ return if (file.createNewFile()) {
+ ExportedDiskFile(path, file, handler)
+ } else {
+ null
+ }
+ }
+ }
+
+ override fun open(mode: Int, furtive: Boolean): ParcelFileDescriptor {
+ return if (furtive) {
+ ParcelFileDescriptor.open(file, mode, handler) {
+ free()
+ }
+ } else {
+ ParcelFileDescriptor.open(file, mode)
+ }
+ }
+
+ override fun free() {
+ Wiper.wipe(file)
+ }
+ }
+
+ class ExportedMemFile private constructor(path: String, private val file: MemFile) :
+ ExportedFile(path) {
+ companion object {
+ fun create(path: String, size: Long): ExportedMemFile? {
+ val uuid = UUID.randomUUID().toString()
+ MemFile.create(uuid, size)?.let {
+ return ExportedMemFile(path, it)
+ }
+ return null
+ }
+ }
+
+ override fun open(mode: Int, furtive: Boolean): ParcelFileDescriptor {
+ val fd = if (furtive) {
+ file.toParcelFileDescriptor()
+ } else {
+ file.dup()
+ }
+ if (mode and ParcelFileDescriptor.MODE_TRUNCATE != 0) {
+ Os.ftruncate(fd.fileDescriptor, 0)
+ } else {
+ FileInputStream(fd.fileDescriptor).apply {
+ channel.position(0)
+ close()
+ }
+ }
+ return fd
+ }
+
+ override fun free() = file.close()
+ }
+
+ abstract class ExportedFile(val path: String) {
+ var isValid = true
+ private set
+
+ fun invalidate() {
+ isValid = false
+ }
+
+ /**
+ * @param furtive If set to true, the file will be deleted when closed
+ */
+ abstract fun open(mode: Int, furtive: Boolean): ParcelFileDescriptor
+ abstract fun free()
+ }
+
+ fun createFile(
+ path: String,
+ size: Long,
+ ): ExportedFile? {
+ return if (size > memoryInfo.availMem * 0.8) {
+ ExportedDiskFile.create(
+ path,
+ tmpFilesDir,
+ handler,
+ )
+ } else if (isMemFileSupported) {
+ ExportedMemFile.create(path, size) as ExportedFile
+ } else {
+ null
+ }
+ }
+
+ fun exportFile(
+ exportedFile: ExportedFile,
+ encryptedVolume: EncryptedVolume,
+ ): Boolean {
+ val fd = exportedFile.open(ParcelFileDescriptor.MODE_WRITE_ONLY, false).fileDescriptor
+ return encryptedVolume.exportFile(exportedFile.path, FileOutputStream(fd))
+ }
+
+ enum class Error {
+ SUCCESS,
+ INVALID_STATE,
+ WRITE_ACCESS_DENIED,
+ UNSUPPORTED_APPEND,
+ UNSUPPORTED_RW,
+ ;
+
+ fun log() {
+ Log.e(
+ TAG, when (this) {
+ SUCCESS -> "No error"
+ INVALID_STATE -> "Read after write is not supported"
+ WRITE_ACCESS_DENIED -> "Write access unauthorized"
+ UNSUPPORTED_APPEND -> "Appending is not supported"
+ UNSUPPORTED_RW -> "Read-write access requires Android 11 or later"
+ }
+ )
+ }
+ }
+
+ /**
+ * @param furtive If set to true, the file will be deleted when closed
+ */
+ fun openFile(
+ file: ExportedFile,
+ mode: String,
+ encryptedVolume: EncryptedVolume,
+ volumeScope: CoroutineScope,
+ furtive: Boolean,
+ allowWrites: Boolean,
+ ): Pair {
+ val mode = ParcelFileDescriptor.parseMode(mode)
+ return if (mode and ParcelFileDescriptor.MODE_READ_ONLY != 0) {
+ if (!file.isValid) return Pair(null, Error.INVALID_STATE)
+ Pair(file.open(mode, furtive), Error.SUCCESS)
+ } else {
+ if (!allowWrites) {
+ return Pair(null, Error.WRITE_ACCESS_DENIED)
+ }
+
+ fun import(input: InputStream): Boolean {
+ return if (encryptedVolume.importFile(input, file.path)) {
+ true
+ } else {
+ Log.e(TAG, "Failed to import file")
+ false
+ }
+ }
+
+ if (mode and ParcelFileDescriptor.MODE_WRITE_ONLY != 0) {
+ if (mode and ParcelFileDescriptor.MODE_APPEND != 0) {
+ return Pair(null, Error.UNSUPPORTED_APPEND)
+ }
+ if (mode and ParcelFileDescriptor.MODE_TRUNCATE == 0) {
+ Log.w(TAG, "Truncating file despite not being requested")
+ }
+ val pipe = ParcelFileDescriptor.createReliablePipe()
+ val input = FileInputStream(pipe[0].fileDescriptor)
+ volumeScope.launch {
+ if (import(input)) {
+ file.invalidate()
+ }
+ }
+ Pair(pipe[1], Error.SUCCESS)
+ } else { // read-write
+ if (!file.isValid) return Pair(null, Error.INVALID_STATE)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ val fd = file.open(mode, false)
+ Pair(ParcelFileDescriptor.wrap(fd, handler) { e ->
+ if (e == null) {
+ import(FileInputStream(fd.fileDescriptor))
+ if (furtive) {
+ file.free()
+ }
+ }
+ }, Error.SUCCESS)
+ } else {
+ Pair(null, Error.UNSUPPORTED_RW)
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/sushi/hardcore/droidfs/FileShare.kt b/app/src/main/java/sushi/hardcore/droidfs/FileShare.kt
index 14b5e2b..f6190ad 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/FileShare.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/FileShare.kt
@@ -4,66 +4,48 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.webkit.MimeTypeMap
-import sushi.hardcore.droidfs.content_providers.DiskFileProvider
-import sushi.hardcore.droidfs.content_providers.MemoryFileProvider
+import androidx.preference.PreferenceManager
import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
-import sushi.hardcore.droidfs.filesystems.EncryptedVolume
-import sushi.hardcore.droidfs.util.Version
import java.io.File
-class FileShare(private val encryptedVolume: EncryptedVolume, private val context: Context) {
-
+class FileShare(context: Context) {
companion object {
- private const val content_type_all = "*/*"
- fun getContentType(filename: String, previousContentType: String?): String {
- if (content_type_all != previousContentType) {
+ private const val CONTENT_TYPE_ANY = "*/*"
+ private fun getContentType(filename: String, previousContentType: String?): String {
+ if (CONTENT_TYPE_ANY != previousContentType) {
var contentType = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(File(filename).extension)
if (contentType == null) {
- contentType = content_type_all
+ contentType = CONTENT_TYPE_ANY
}
if (previousContentType == null) {
return contentType
} else if (previousContentType != contentType) {
- return content_type_all
+ return CONTENT_TYPE_ANY
}
}
return previousContentType
}
}
- private val fileProvider: TemporaryFileProvider<*>
+ private val usfSafWrite = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("usf_saf_write", false)
- init {
- var provider: MemoryFileProvider? = null
- System.getProperty("os.version")?.let {
- if (Version(it) >= Version("3.17")) {
- provider = MemoryFileProvider()
- }
- }
- fileProvider = provider ?: DiskFileProvider()
+ private fun exportFile(exportedFile: EncryptedFileProvider.ExportedFile, size: Long, volumeId: Int, previousContentType: String? = null): Pair? {
+ val uri = TemporaryFileProvider.instance.exportFile(exportedFile, size, volumeId) ?: return null
+ return Pair(uri, getContentType(File(exportedFile.path).name, previousContentType))
}
- private fun exportFile(path: String, size: Long, previousContentType: String? = null): Pair {
- val fileName = File(path).name
- val uri = fileProvider.newFile(fileName, size)
- if (uri != null) {
- if (encryptedVolume.exportFile(context, path, uri)) {
- return Pair(uri, getContentType(fileName, previousContentType))
- }
- }
- return Pair(null, null)
- }
-
- fun share(files: List>): Pair {
+ fun share(files: List>, volumeId: Int): Pair {
var contentType: String? = null
val uris = ArrayList(files.size)
for ((path, size) in files) {
- val result = exportFile(path, size, contentType)
- contentType = if (result.first == null) {
- return Pair(null, path)
+ val exportedFile = TemporaryFileProvider.instance.encryptedFileProvider.createFile(path, size)
+ ?: return Pair(null, R.string.export_failed_create)
+ val result = exportFile(exportedFile, size, volumeId, contentType)
+ contentType = if (result == null) {
+ return Pair(null, R.string.export_failed_export)
} else {
- uris.add(result.first!!)
+ uris.add(result.first)
result.second
}
}
@@ -79,15 +61,18 @@ class FileShare(private val encryptedVolume: EncryptedVolume, private val contex
}, null)
}
- fun openWith(path: String, size: Long): Intent? {
- val result = exportFile(path, size)
- return if (result.first != null) {
- Intent(Intent.ACTION_VIEW).apply {
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- setDataAndType(result.first, result.second)
- }
+ fun openWith(exportedFile: EncryptedFileProvider.ExportedFile, size: Long, volumeId: Int): Pair {
+ val result = exportFile(exportedFile, size, volumeId)
+ return if (result == null) {
+ Pair(null, R.string.export_failed_export)
} else {
- null
+ Pair(Intent(Intent.ACTION_VIEW).apply {
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ if (usfSafWrite) {
+ addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+ }
+ setDataAndType(result.first, result.second)
+ }, null)
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/MemFile.kt b/app/src/main/java/sushi/hardcore/droidfs/MemFile.kt
index d8b3fcb..0c76ad5 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/MemFile.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/MemFile.kt
@@ -1,6 +1,7 @@
package sushi.hardcore.droidfs
import android.os.ParcelFileDescriptor
+import android.system.Os
class MemFile private constructor(private val fd: Int) {
companion object {
@@ -15,8 +16,7 @@ class MemFile private constructor(private val fd: Int) {
}
}
- private external fun close(fd: Int)
-
- fun getParcelFileDescriptor(): ParcelFileDescriptor = ParcelFileDescriptor.fromFd(fd)
- fun close() = close(fd)
+ fun dup(): ParcelFileDescriptor = ParcelFileDescriptor.fromFd(fd)
+ fun toParcelFileDescriptor(): ParcelFileDescriptor = ParcelFileDescriptor.adoptFd(fd)
+ fun close() = Os.close(ParcelFileDescriptor.adoptFd(fd).fileDescriptor)
}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt
index 27840ff..c88a84f 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt
@@ -7,8 +7,16 @@ import android.os.Bundle
import android.text.InputType
import android.view.MenuItem
import android.widget.Toast
-import androidx.preference.*
+import androidx.preference.ListPreference
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceManager
+import androidx.preference.SwitchPreference
+import androidx.preference.SwitchPreferenceCompat
+import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
+import sushi.hardcore.droidfs.content_providers.VolumeProvider
import sushi.hardcore.droidfs.databinding.ActivitySettingsBinding
+import sushi.hardcore.droidfs.util.Compat
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog
@@ -150,6 +158,47 @@ class SettingsActivity : BaseActivity() {
true
}
}
+ val switchKeepOpen = findPreference("usf_keep_open")!!
+ val switchExternalOpen = findPreference("usf_open")!!
+ val switchExpose = findPreference("usf_expose")!!
+ val switchSafWrite = findPreference("usf_saf_write")!!
+
+ fun updateView(usfOpen: Boolean? = null, usfKeepOpen: Boolean? = null, usfExpose: Boolean? = null) {
+ val usfKeepOpen = usfKeepOpen ?: switchKeepOpen.isChecked
+ switchExpose.isEnabled = usfKeepOpen
+ switchSafWrite.isEnabled = usfOpen ?: switchExternalOpen.isChecked || (usfKeepOpen && usfExpose ?: switchExpose.isChecked)
+ }
+
+ updateView()
+ switchKeepOpen.setOnPreferenceChangeListener { _, checked ->
+ updateView(usfKeepOpen = checked as Boolean)
+ true
+ }
+ switchExternalOpen.setOnPreferenceChangeListener { _, checked ->
+ updateView(usfOpen = checked as Boolean)
+ true
+ }
+ switchExpose.setOnPreferenceChangeListener { _, checked ->
+ if (checked as Boolean) {
+ if (!Compat.isMemFileSupported()) {
+ CustomAlertDialogBuilder(requireContext(), (requireActivity() as BaseActivity).theme)
+ .setTitle(R.string.error)
+ .setMessage("Your current kernel does not support memfd_create(). This feature requires a minimum kernel version of ${Compat.MEMFD_CREATE_MINIMUM_KERNEL_VERSION}.")
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ return@setOnPreferenceChangeListener false
+ }
+ }
+ VolumeProvider.usfExpose = checked
+ updateView(usfExpose = checked)
+ VolumeProvider.notifyRootChanged(requireContext())
+ true
+ }
+ switchSafWrite.setOnPreferenceChangeListener { _, checked ->
+ VolumeProvider.usfSafWrite = checked as Boolean
+ TemporaryFileProvider.usfSafWrite = checked
+ true
+ }
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/VolumeManager.kt b/app/src/main/java/sushi/hardcore/droidfs/VolumeManager.kt
index 4bb26dc..224f0a1 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/VolumeManager.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/VolumeManager.kt
@@ -1,12 +1,15 @@
package sushi.hardcore.droidfs
+import android.content.Context
+import android.provider.DocumentsContract
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
+import sushi.hardcore.droidfs.content_providers.VolumeProvider
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
-class VolumeManager {
+class VolumeManager(private val context: Context) {
private var id = 0
private val volumes = HashMap()
private val volumesData = HashMap()
@@ -15,6 +18,7 @@ class VolumeManager {
fun insert(volume: EncryptedVolume, data: VolumeData): Int {
volumes[id] = volume
volumesData[data] = id
+ VolumeProvider.notifyRootChanged(context)
return id++
}
@@ -30,6 +34,10 @@ class VolumeManager {
return volumes[id]
}
+ fun listVolumes(): List> {
+ return volumesData.map { (data, id) -> Pair(id, data) }
+ }
+
fun getCoroutineScope(volumeId: Int): CoroutineScope {
return scopes[volumeId] ?: CoroutineScope(SupervisorJob() + Dispatchers.IO).also { scopes[volumeId] = it }
}
@@ -41,6 +49,7 @@ class VolumeManager {
volumesData.filter { it.value == id }.forEach {
volumesData.remove(it.key)
}
+ VolumeProvider.notifyRootChanged(context)
}
}
@@ -51,5 +60,6 @@ class VolumeManager {
}
volumes.clear()
volumesData.clear()
+ VolumeProvider.notifyRootChanged(context)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/VolumeManagerApp.kt b/app/src/main/java/sushi/hardcore/droidfs/VolumeManagerApp.kt
index 94849fc..6e49652 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/VolumeManagerApp.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/VolumeManagerApp.kt
@@ -6,8 +6,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.preference.PreferenceManager
-import sushi.hardcore.droidfs.content_providers.MemoryFileProvider
-import sushi.hardcore.droidfs.content_providers.DiskFileProvider
+import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
class VolumeManagerApp : Application(), DefaultLifecycleObserver {
companion object {
@@ -21,8 +20,9 @@ class VolumeManagerApp : Application(), DefaultLifecycleObserver {
}
}
private var usfKeepOpen = false
+ var isExporting = false
var isStartingExternalApp = false
- val volumeManager = VolumeManager()
+ val volumeManager = VolumeManager(this)
override fun onCreate() {
super.onCreate()
@@ -46,8 +46,9 @@ class VolumeManagerApp : Application(), DefaultLifecycleObserver {
if (!usfKeepOpen) {
volumeManager.closeAll()
}
- DiskFileProvider.wipe()
- MemoryFileProvider.wipe()
+ if (!usfKeepOpen || !isExporting) {
+ TemporaryFileProvider.instance.wipe()
+ }
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/content_providers/DiskFileProvider.kt b/app/src/main/java/sushi/hardcore/droidfs/content_providers/DiskFileProvider.kt
deleted file mode 100644
index 87d0903..0000000
--- a/app/src/main/java/sushi/hardcore/droidfs/content_providers/DiskFileProvider.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package sushi.hardcore.droidfs.content_providers
-
-import android.net.Uri
-import android.os.ParcelFileDescriptor
-import sushi.hardcore.droidfs.BuildConfig
-import sushi.hardcore.droidfs.util.Wiper
-import java.io.File
-import java.util.UUID
-
-class DiskFileProvider: TemporaryFileProvider() {
- companion object {
- private const val AUTHORITY = BuildConfig.APPLICATION_ID + ".disk_provider"
- private val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY")
- const val TEMPORARY_FILES_DIR_NAME = "temp"
-
- private lateinit var tempFilesDir: File
-
- private var files = HashMap.SharedFile>()
-
- fun wipe() {
- for (i in files.values) {
- Wiper.wipe(i.file)
- }
- files.clear()
- tempFilesDir.listFiles()?.let {
- for (file in it) {
- Wiper.wipe(file)
- }
- }
- }
- }
-
- override fun onCreate(): Boolean {
- context?.let {
- tempFilesDir = File(it.cacheDir, TEMPORARY_FILES_DIR_NAME)
- return tempFilesDir.mkdirs()
- }
- return false
- }
-
- override fun getFile(uri: Uri): SharedFile? = files[uri]
-
- override fun newFile(name: String, size: Long): Uri? {
- val uuid = UUID.randomUUID().toString()
- val file = File(tempFilesDir, uuid)
- return if (file.createNewFile()) {
- Uri.withAppendedPath(CONTENT_URI, uuid).also {
- files[it] = SharedFile(name, size, file)
- }
- } else {
- null
- }
- }
-
- override fun delete(uri: Uri, givenSelection: String?, givenSelectionArgs: Array?): Int {
- return if (files.remove(uri)?.file?.also { Wiper.wipe(it) } == null) 0 else 1
- }
-
- override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
- return if (("w" in mode && callingPackage == BuildConfig.APPLICATION_ID) || "w" !in mode) {
- files[uri]?.file?.let {
- return ParcelFileDescriptor.open(it, ParcelFileDescriptor.parseMode(mode))
- }
- } else {
- throw SecurityException("Read-only access")
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/content_providers/MemoryFileProvider.kt b/app/src/main/java/sushi/hardcore/droidfs/content_providers/MemoryFileProvider.kt
deleted file mode 100644
index f6bf759..0000000
--- a/app/src/main/java/sushi/hardcore/droidfs/content_providers/MemoryFileProvider.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package sushi.hardcore.droidfs.content_providers
-
-import android.net.Uri
-import android.os.ParcelFileDescriptor
-import sushi.hardcore.droidfs.BuildConfig
-import sushi.hardcore.droidfs.MemFile
-import java.io.FileInputStream
-import java.util.UUID
-
-class MemoryFileProvider: TemporaryFileProvider() {
- companion object {
- private const val AUTHORITY = BuildConfig.APPLICATION_ID + ".memory_provider"
- private val BASE_URI: Uri = Uri.parse("content://$AUTHORITY")
-
- private var files = HashMap.SharedFile>()
-
- fun wipe() {
- for (i in files.values) {
- i.file.close()
- }
- files.clear()
- }
- }
-
- override fun onCreate(): Boolean = true
-
- override fun getFile(uri: Uri): SharedFile? = files[uri]
-
- override fun newFile(name: String, size: Long): Uri? {
- val uuid = UUID.randomUUID().toString()
- return Uri.withAppendedPath(BASE_URI, uuid).also {
- files[it] = SharedFile(name, size, MemFile.create(uuid, size) ?: return null)
- }
- }
-
- override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int {
- return if (files.remove(uri)?.file?.close() == null) 0 else 1
- }
-
- override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
- return files[uri]?.file?.getParcelFileDescriptor()?.also {
- FileInputStream(it.fileDescriptor).apply {
- channel.position(0)
- close()
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/content_providers/TemporaryFileProvider.kt b/app/src/main/java/sushi/hardcore/droidfs/content_providers/TemporaryFileProvider.kt
index 52658bb..faa8c2c 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/content_providers/TemporaryFileProvider.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/content_providers/TemporaryFileProvider.kt
@@ -5,20 +5,83 @@ import android.content.ContentValues
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
+import android.os.ParcelFileDescriptor
import android.provider.OpenableColumns
+import android.util.Log
import android.webkit.MimeTypeMap
+import androidx.preference.PreferenceManager
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import sushi.hardcore.droidfs.BuildConfig
+import sushi.hardcore.droidfs.EncryptedFileProvider
+import sushi.hardcore.droidfs.VolumeManager
+import sushi.hardcore.droidfs.VolumeManagerApp
+import sushi.hardcore.droidfs.util.Wiper
import java.io.File
+import java.util.UUID
-abstract class TemporaryFileProvider: ContentProvider() {
- protected inner class SharedFile(val name: String, val size: Long, val file: T)
+class TemporaryFileProvider : ContentProvider() {
+ private inner class ProvidedFile(
+ val file: EncryptedFileProvider.ExportedFile,
+ val size: Long,
+ val volumeId: Int
+ )
- protected abstract fun getFile(uri: Uri): SharedFile?
- abstract fun newFile(name: String, size: Long): Uri?
+ companion object {
+ private const val TAG = "TemporaryFileProvider"
+ private const val AUTHORITY = BuildConfig.APPLICATION_ID + ".temporary_provider"
+ private val BASE_URI: Uri = Uri.parse("content://$AUTHORITY")
- override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? {
- val file = getFile(uri) ?: return null
+ lateinit var instance: TemporaryFileProvider
+ private set
+ var usfSafWrite = false
+ }
+
+ private lateinit var volumeManager: VolumeManager
+ lateinit var encryptedFileProvider: EncryptedFileProvider
+ private val files = HashMap()
+
+ override fun onCreate(): Boolean {
+ return context?.let {
+ volumeManager = (it.applicationContext as VolumeManagerApp).volumeManager
+ usfSafWrite =
+ PreferenceManager.getDefaultSharedPreferences(it).getBoolean("usf_saf_write", false)
+ encryptedFileProvider = EncryptedFileProvider(it)
+ instance = this
+ val tmpFilesDir = EncryptedFileProvider.getTmpFilesDir(it)
+ val success = tmpFilesDir.mkdirs()
+ // wipe any additional files not previously deleted
+ GlobalScope.launch(Dispatchers.IO) {
+ tmpFilesDir.listFiles()?.onEach { f -> Wiper.wipe(f) }
+ }
+ success
+ } ?: false
+ }
+
+ fun exportFile(
+ exportedFile: EncryptedFileProvider.ExportedFile,
+ size: Long,
+ volumeId: Int
+ ): Uri? {
+ if (!encryptedFileProvider.exportFile(exportedFile, volumeManager.getVolume(volumeId)!!)) {
+ return null
+ }
+ return Uri.withAppendedPath(BASE_URI, UUID.randomUUID().toString()).also {
+ files[it] = ProvidedFile(exportedFile, size, volumeId)
+ }
+ }
+
+ override fun query(
+ uri: Uri,
+ projection: Array?,
+ selection: String?,
+ selectionArgs: Array?,
+ sortOrder: String?
+ ): Cursor? {
+ val file = files[uri] ?: return null
return MatrixCursor(arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE), 1).apply {
- addRow(arrayOf(file.name, file.size))
+ addRow(arrayOf(File(file.file.path).name, file.size))
}
}
@@ -26,11 +89,57 @@ abstract class TemporaryFileProvider: ContentProvider() {
throw UnsupportedOperationException("Operation not supported")
}
- override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int {
+ override fun update(
+ uri: Uri,
+ values: ContentValues?,
+ selection: String?,
+ selectionArgs: Array?
+ ): Int {
throw UnsupportedOperationException("Operation not supported")
}
- override fun getType(uri: Uri): String = getFile(uri)?.name?.let {
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int {
+ return if (files.remove(uri)?.file?.also { it.free() } == null) 0 else 1
+ }
+
+ override fun getType(uri: Uri): String = files[uri]?.file?.path?.let {
MimeTypeMap.getSingleton().getMimeTypeFromExtension(File(it).extension)
} ?: "application/octet-stream"
+
+ override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
+ files[uri]?.let { file ->
+ val encryptedVolume = volumeManager.getVolume(file.volumeId) ?: run {
+ Log.e(TAG, "Volume closed for $uri")
+ return null
+ }
+ val result = encryptedFileProvider.openFile(
+ file.file,
+ mode,
+ encryptedVolume,
+ volumeManager.getCoroutineScope(file.volumeId),
+ false,
+ usfSafWrite,
+ )
+ when (result.second) {
+ EncryptedFileProvider.Error.SUCCESS -> return result.first!!
+ EncryptedFileProvider.Error.WRITE_ACCESS_DENIED -> Log.e(
+ TAG,
+ "Unauthorized write access requested from $callingPackage to $uri"
+ )
+
+ else -> result.second.log()
+ }
+ }
+ return null
+ }
+
+ // this must not be cancelled
+ fun wipe() = GlobalScope.launch(Dispatchers.IO) {
+ synchronized(this@TemporaryFileProvider) {
+ for (i in files.values) {
+ i.file.free()
+ }
+ files.clear()
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/content_providers/VolumeProvider.kt b/app/src/main/java/sushi/hardcore/droidfs/content_providers/VolumeProvider.kt
new file mode 100644
index 0000000..a1423ad
--- /dev/null
+++ b/app/src/main/java/sushi/hardcore/droidfs/content_providers/VolumeProvider.kt
@@ -0,0 +1,280 @@
+package sushi.hardcore.droidfs.content_providers
+
+import android.content.Context
+import android.content.Intent
+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.util.Log
+import android.webkit.MimeTypeMap
+import androidx.preference.PreferenceManager
+import sushi.hardcore.droidfs.BuildConfig
+import sushi.hardcore.droidfs.EncryptedFileProvider
+import sushi.hardcore.droidfs.R
+import sushi.hardcore.droidfs.VolumeData
+import sushi.hardcore.droidfs.VolumeManager
+import sushi.hardcore.droidfs.VolumeManagerApp
+import sushi.hardcore.droidfs.filesystems.EncryptedVolume
+import sushi.hardcore.droidfs.filesystems.Stat
+import sushi.hardcore.droidfs.util.PathUtils
+import java.io.File
+
+class VolumeProvider: DocumentsProvider() {
+ companion object {
+ private const val TAG = "DocumentsProvider"
+ private const val AUTHORITY = BuildConfig.APPLICATION_ID + ".volume_provider"
+ private val DEFAULT_ROOT_PROJECTION = arrayOf(
+ DocumentsContract.Root.COLUMN_ROOT_ID,
+ DocumentsContract.Root.COLUMN_FLAGS,
+ DocumentsContract.Root.COLUMN_ICON,
+ DocumentsContract.Root.COLUMN_TITLE,
+ DocumentsContract.Root.COLUMN_DOCUMENT_ID,
+ )
+ private val DEFAULT_DOCUMENT_PROJECTION = arrayOf(
+ DocumentsContract.Document.COLUMN_DOCUMENT_ID,
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+ DocumentsContract.Document.COLUMN_MIME_TYPE,
+ DocumentsContract.Document.COLUMN_FLAGS,
+ DocumentsContract.Document.COLUMN_SIZE,
+ DocumentsContract.Document.COLUMN_LAST_MODIFIED,
+ )
+ var usfExpose = false
+ var usfSafWrite = false
+
+ fun notifyRootChanged(context: Context) {
+ context.contentResolver.notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null)
+ }
+ }
+
+ private lateinit var volumeManager: VolumeManager
+ private val volumes = HashMap>()
+ private lateinit var encryptedFileProvider: EncryptedFileProvider
+
+ override fun onCreate(): Boolean {
+ val context = (context ?: return false)
+ val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
+ usfExpose = sharedPreferences.getBoolean("usf_expose", false)
+ usfSafWrite = sharedPreferences.getBoolean("usf_saf_write", false)
+ volumeManager = (context.applicationContext as VolumeManagerApp).volumeManager
+ encryptedFileProvider = EncryptedFileProvider(context)
+ return true
+ }
+
+ override fun queryRoots(projection: Array?): Cursor {
+ val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION)
+ if (!usfExpose) return cursor
+ val previousVolumeIds = volumes.keys.toSet()
+ volumes.clear()
+ for (volume in volumeManager.listVolumes()) {
+ var flags = DocumentsContract.Root.FLAG_LOCAL_ONLY or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
+ if (usfSafWrite && volume.second.canWrite(context!!.filesDir.path)) {
+ flags = flags or DocumentsContract.Root.FLAG_SUPPORTS_CREATE
+ }
+ cursor.newRow().apply {
+ add(DocumentsContract.Root.COLUMN_ROOT_ID, volume.second.name)
+ add(DocumentsContract.Root.COLUMN_FLAGS, flags)
+ add(DocumentsContract.Root.COLUMN_ICON, R.drawable.icon_document_provider)
+ add(DocumentsContract.Root.COLUMN_TITLE, volume.second.name)
+ add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, volume.first)
+ }
+ volumes[volume.first.toString()] = volume
+ }
+ for (i in previousVolumeIds) {
+ if (!volumes.containsKey(i)) {
+ val uri = DocumentsContract.buildRootUri(AUTHORITY, i)
+ context?.revokeUriPermission(
+ uri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ )
+ }
+ }
+ return cursor
+ }
+
+ internal data class DocumentData(
+ val rootId: String,
+ val volumeId: Int,
+ val volumeData: VolumeData,
+ val encryptedVolume: EncryptedVolume,
+ val path: String
+ ) {
+ fun child(childPath: String) = DocumentData(rootId, volumeId, volumeData, encryptedVolume, childPath)
+ }
+
+ private fun parseDocumentId(documentId: String): DocumentData? {
+ val splits = documentId.split("/", limit = 2)
+ if (splits.size > 2) {
+ return null
+ } else {
+ volumes[splits[0]]?.let {
+ val encryptedVolume = volumeManager.getVolume(it.first) ?: return null
+ val path = "/"+if (splits.size == 2) {
+ splits[1]
+ } else {
+ ""
+ }
+ return DocumentData(splits[0], it.first, it.second, encryptedVolume, path)
+ }
+ }
+ return null
+ }
+
+ override fun isChildDocument(parentDocumentId: String, documentId: String): Boolean {
+ if (!usfExpose) return false
+ val parent = parseDocumentId(parentDocumentId) ?: return false
+ val child = parseDocumentId(documentId) ?: return false
+ return parent.rootId == child.rootId && PathUtils.isChildOf(child.path, parent.path)
+ }
+
+ private fun addDocumentRow(cursor: MatrixCursor, volumeData: VolumeData, documentId: String, name: String, stat: Stat) {
+ val isDirectory = stat.type == Stat.S_IFDIR
+ var flags = 0
+ if (usfSafWrite && volumeData.canWrite(context!!.filesDir.path)) {
+ flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE or DocumentsContract.Document.FLAG_SUPPORTS_RENAME
+ if (isDirectory) {
+ flags = flags or DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE
+ } else if (stat.type == Stat.S_IFREG) {
+ flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_WRITE
+ }
+ }
+ val mimeType = if (isDirectory) {
+ DocumentsContract.Document.MIME_TYPE_DIR
+ } else {
+ MimeTypeMap.getSingleton().getMimeTypeFromExtension(File(name).extension)
+ ?: "application/octet-stream"
+ }
+ cursor.newRow().apply {
+ add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, documentId)
+ add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, name)
+ add(DocumentsContract.Document.COLUMN_MIME_TYPE, mimeType)
+ add(DocumentsContract.Document.COLUMN_FLAGS, flags)
+ add(DocumentsContract.Document.COLUMN_SIZE, stat.size)
+ add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, stat.mTime)
+ }
+ }
+
+ override fun queryDocument(documentId: String, projection: Array?): Cursor {
+ val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
+ if (!usfExpose) return cursor
+ val document = parseDocumentId(documentId) ?: return cursor
+ document.encryptedVolume.getAttr(document.path)?.let { stat ->
+ val name = if (document.path == "/") {
+ document.volumeData.shortName
+ } else {
+ File(document.path).name
+ }
+ addDocumentRow(cursor, document.volumeData, documentId, name, stat)
+ }
+ return cursor
+ }
+
+ override fun queryChildDocuments(
+ parentDocumentId: String,
+ projection: Array?,
+ sortOrder: String?
+ ): Cursor {
+ val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
+ if (!usfExpose) return cursor
+ val document = parseDocumentId(parentDocumentId) ?: return cursor
+ document.encryptedVolume.readDir(document.path)?.let { content ->
+ for (i in content) {
+ if (i.isParentFolder) continue
+ addDocumentRow(cursor, document.volumeData, document.rootId+i.fullPath, i.name, i.stat)
+ }
+ }
+ return cursor
+ }
+
+ override fun openDocument(
+ documentId: String,
+ mode: String,
+ signal: CancellationSignal?
+ ): ParcelFileDescriptor? {
+ if (!usfExpose) return null
+ val document = parseDocumentId(documentId) ?: return null
+ val size = document.encryptedVolume.getAttr(document.path)?.size ?: run {
+ Log.e(TAG, "stat() failed")
+ return null
+ }
+ val exportedFile = encryptedFileProvider.createFile(document.path, size) ?: run {
+ Log.e(TAG, "Can't create exported file")
+ return null
+ }
+ if (!encryptedFileProvider.exportFile(exportedFile, document.encryptedVolume)) {
+ Log.e(TAG, "File export failed")
+ return null
+ }
+ val result = encryptedFileProvider.openFile(
+ exportedFile,
+ mode,
+ document.encryptedVolume,
+ volumeManager.getCoroutineScope(document.volumeId),
+ true,
+ usfSafWrite,
+ )
+ when (result.second) {
+ EncryptedFileProvider.Error.SUCCESS -> return result.first!!
+ EncryptedFileProvider.Error.WRITE_ACCESS_DENIED -> Log.e(TAG, "Unauthorized write access requested from $callingPackage")
+ else -> result.second.log()
+ }
+ return null
+ }
+
+ override fun createDocument(
+ parentDocumentId: String,
+ mimeType: String?,
+ displayName: String
+ ): String? {
+ if (!usfExpose || !usfSafWrite) return null
+ val document = parseDocumentId(parentDocumentId) ?: return null
+ val newFile = PathUtils.pathJoin(document.path, displayName)
+ val f = document.encryptedVolume.openFileWriteMode(newFile)
+ return if (f == -1L) {
+ Log.e(TAG, "Failed to create file: $document")
+ null
+ } else {
+ document.encryptedVolume.closeFile(f)
+ document.rootId+"/"+newFile
+ }
+ }
+
+ override fun deleteDocument(documentId: String) {
+ if (!usfExpose || !usfSafWrite) return
+
+ fun recursiveRemoveDirectory(document: DocumentData) {
+ document.encryptedVolume.readDir(document.path)?.forEach { e ->
+ val childPath = PathUtils.pathJoin(document.path, e.name)
+ if (e.isDirectory) {
+ recursiveRemoveDirectory(document.child(childPath))
+ } else {
+ document.encryptedVolume.deleteFile(childPath)
+ }
+ revokeDocumentPermission(document.rootId+childPath)
+ }
+ document.encryptedVolume.rmdir(document.path)
+ }
+
+ val document = parseDocumentId(documentId) ?: return
+ document.encryptedVolume.getAttr(document.path)?.let { stat ->
+ if (stat.type == Stat.S_IFDIR) {
+ recursiveRemoveDirectory(document)
+ } else {
+ document.encryptedVolume.deleteFile(document.path)
+ }
+ }
+ }
+
+ override fun renameDocument(documentId: String, displayName: String): String {
+ if (!usfExpose || !usfSafWrite) return documentId
+ val document = parseDocumentId(documentId) ?: return documentId
+ val newPath = PathUtils.pathJoin(PathUtils.getParentPath(document.path), displayName)
+ return if (document.encryptedVolume.rename(document.path, newPath)) {
+ document.rootId+newPath
+ } else {
+ documentId
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt
index 643081a..f07c792 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt
@@ -29,6 +29,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import sushi.hardcore.droidfs.BaseActivity
import sushi.hardcore.droidfs.Constants
+import sushi.hardcore.droidfs.EncryptedFileProvider
import sushi.hardcore.droidfs.FileShare
import sushi.hardcore.droidfs.FileTypes
import sushi.hardcore.droidfs.LoadingTask
@@ -36,8 +37,7 @@ import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.VolumeManagerApp
import sushi.hardcore.droidfs.adapters.ExplorerElementAdapter
import sushi.hardcore.droidfs.adapters.OpenAsDialogAdapter
-import sushi.hardcore.droidfs.content_providers.DiskFileProvider
-import sushi.hardcore.droidfs.content_providers.MemoryFileProvider
+import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
import sushi.hardcore.droidfs.file_operations.FileOperationService
import sushi.hardcore.droidfs.file_operations.OperationFile
import sushi.hardcore.droidfs.file_operations.TaskResult
@@ -84,9 +84,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
private lateinit var numberOfFilesText: TextView
private lateinit var numberOfFoldersText: TextView
private lateinit var totalSizeText: TextView
- protected val fileShare by lazy {
- FileShare(encryptedVolume, this)
- }
+ protected val fileShare by lazy { FileShare(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -204,22 +202,38 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
startActivity(intent)
}
+ protected fun onExportFailed(errorResId: Int) {
+ CustomAlertDialogBuilder(this, theme)
+ .setTitle(R.string.error)
+ .setMessage(getString(R.string.tmp_export_failed, getString(errorResId)))
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ }
+
private fun openWithExternalApp(path: String, size: Long) {
- app.isStartingExternalApp = true
- object : LoadingTask(this, theme, R.string.loading_msg_export) {
- override suspend fun doTask(): Intent? {
- return fileShare.openWith(path, size)
+ app.isExporting = true
+ val exportedFile = TemporaryFileProvider.instance.encryptedFileProvider.createFile(path, size)
+ if (exportedFile == null) {
+ onExportFailed(R.string.export_failed_create)
+ return
+ }
+ val msg = when (exportedFile) {
+ is EncryptedFileProvider.ExportedMemFile -> R.string.export_mem
+ is EncryptedFileProvider.ExportedDiskFile -> R.string.export_disk
+ else -> R.string.loading_msg_export
+ }
+ object : LoadingTask>(this, theme, msg) {
+ override suspend fun doTask(): Pair {
+ return fileShare.openWith(exportedFile, size, volumeId)
}
- }.startTask(lifecycleScope) { openIntent ->
- if (openIntent == null) {
- CustomAlertDialogBuilder(this, theme)
- .setTitle(R.string.error)
- .setMessage(getString(R.string.export_failed, path))
- .setPositiveButton(R.string.ok, null)
- .show()
+ }.startTask(lifecycleScope) { (intent, error) ->
+ if (intent == null) {
+ onExportFailed(error!!)
} else {
- startActivity(openIntent)
+ app.isStartingExternalApp = true
+ startActivity(intent)
}
+ app.isExporting = false
}
}
@@ -644,8 +658,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
override fun onResume() {
super.onResume()
if (app.isStartingExternalApp) {
- MemoryFileProvider.wipe()
- DiskFileProvider.wipe()
+ TemporaryFileProvider.instance.wipe()
}
if (encryptedVolume.isClosed()) {
finish()
diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt
index 7a65aee..6f0e619 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt
@@ -164,7 +164,7 @@ class ExplorerActivity : BaseExplorerActivity() {
} else {
val adapter = IconTextDialogAdapter(this)
adapter.items = listOf(
- listOf("importFromOtherVolumes", R.string.import_from_other_volume, R.drawable.icon_transfert),
+ listOf("importFromOtherVolumes", R.string.import_from_other_volume, R.drawable.icon_transfer),
listOf("importFiles", R.string.import_files, R.drawable.icon_encrypt),
listOf("importFolder", R.string.import_folder, R.drawable.icon_import_folder),
listOf("createFile", R.string.new_file, R.drawable.icon_file_unknown),
@@ -385,26 +385,24 @@ class ExplorerActivity : BaseExplorerActivity() {
true
}
R.id.share -> {
- app.isStartingExternalApp = true
val files = explorerAdapter.selectedItems.map { i ->
explorerElements[i].let {
Pair(it.fullPath, it.stat.size)
}
}
- object : LoadingTask>(this, theme, R.string.loading_msg_export) {
- override suspend fun doTask(): Pair {
- return fileShare.share(files)
+ app.isExporting = true
+ object : LoadingTask>(this, theme, R.string.loading_msg_export) {
+ override suspend fun doTask(): Pair {
+ return fileShare.share(files, volumeId)
}
- }.startTask(lifecycleScope) { (intent, failedItem) ->
+ }.startTask(lifecycleScope) { (intent, error) ->
if (intent == null) {
- CustomAlertDialogBuilder(this, theme)
- .setTitle(R.string.error)
- .setMessage(getString(R.string.export_failed, failedItem))
- .setPositiveButton(R.string.ok, null)
- .show()
+ onExportFailed(error!!)
} else {
+ app.isStartingExternalApp = true
startActivity(Intent.createChooser(intent, getString(R.string.share_chooser)))
}
+ app.isExporting = false
}
unselectAll()
true
diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt
index e128d31..21cfaee 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerElement.kt
@@ -6,7 +6,7 @@ import sushi.hardcore.droidfs.util.PathUtils
import java.text.Collator
class ExplorerElement(val name: String, val stat: Stat, val parentPath: String) {
- val fullPath: String = PathUtils.pathJoin(parentPath, name)
+ val fullPath: String = PathUtils.pathJoin(parentPath.ifEmpty { "/" }, name)
val collationKey = Collator.getInstance().getCollationKeyForFileName(fullPath)
val isDirectory: Boolean
diff --git a/app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt b/app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt
index 1f270d6..6e3a4a8 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/filesystems/EncryptedVolume.kt
@@ -160,7 +160,6 @@ abstract class EncryptedVolume: Parcelable {
if (written == length) {
offset += written
} else {
- inputStream.close()
success = false
break
}
diff --git a/app/src/main/java/sushi/hardcore/droidfs/filesystems/Stat.kt b/app/src/main/java/sushi/hardcore/droidfs/filesystems/Stat.kt
index e08545f..a610688 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/filesystems/Stat.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/filesystems/Stat.kt
@@ -1,14 +1,17 @@
package sushi.hardcore.droidfs.filesystems
-class Stat(val type: Int, var size: Long, val mTime: Long) {
+class Stat(mode: Int, var size: Long, val mTime: Long) {
companion object {
+ private const val S_IFMT = 0xF000
const val S_IFDIR = 0x4000
const val S_IFREG = 0x8000
const val S_IFLNK = 0xA000
- const val PARENT_FOLDER_TYPE = -1
+ const val PARENT_FOLDER_TYPE = 0xE000
fun parentFolderStat(): Stat {
return Stat(PARENT_FOLDER_TYPE, -1, -1)
}
}
+
+ val type = mode and S_IFMT
}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/util/Compat.kt b/app/src/main/java/sushi/hardcore/droidfs/util/Compat.kt
index 5addff5..1b58bb7 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/util/Compat.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/util/Compat.kt
@@ -13,4 +13,11 @@ object Compat {
bundle.getParcelable(name)
}
}
+
+ val MEMFD_CREATE_MINIMUM_KERNEL_VERSION = Version("3.17")
+
+ fun isMemFileSupported(): Boolean {
+ val kernel = System.getProperty("os.version") ?: return false
+ return Version(kernel) >= MEMFD_CREATE_MINIMUM_KERNEL_VERSION
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/sushi/hardcore/droidfs/util/Version.kt b/app/src/main/java/sushi/hardcore/droidfs/util/Version.kt
index 844f0fc..36adb58 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/util/Version.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/util/Version.kt
@@ -3,7 +3,7 @@ package sushi.hardcore.droidfs.util
import java.lang.Integer.max
class Version(inputVersion: String) : Comparable {
- private var version: String
+ val version: String
init {
val regex = "[0-9]+(\\.[0-9]+)*".toRegex()
diff --git a/app/src/main/native/memfile.cpp b/app/src/main/native/memfile.c
similarity index 52%
rename from app/src/main/native/memfile.cpp
rename to app/src/main/native/memfile.c
index 8f71128..c0951b1 100644
--- a/app/src/main/native/memfile.cpp
+++ b/app/src/main/native/memfile.c
@@ -1,24 +1,30 @@
+#include
+#include
#include
#include
#include
#include
+#include
+
+const char* LOG_TAG = "MemFile";
+
+void log_err(const char* function) {
+ __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "%s(): %s", function, strerror(errno));
+}
-extern "C"
JNIEXPORT jint JNICALL
Java_sushi_hardcore_droidfs_MemFile_00024Companion_createMemFile(JNIEnv *env, jobject thiz, jstring jname,
jlong size) {
- const char* name = env->GetStringUTFChars(jname, nullptr);
+ const char* name = (*env)->GetStringUTFChars(env, jname, NULL);
int fd = syscall(SYS_memfd_create, name, MFD_CLOEXEC);
- if (fd < 0) return fd;
+ if (fd < 0) {
+ log_err("memfd_create");
+ return fd;
+ }
if (ftruncate64(fd, size) == -1) {
+ log_err("ftruncate64");
close(fd);
return -1;
}
return fd;
-}
-
-extern "C"
-JNIEXPORT void JNICALL
-Java_sushi_hardcore_droidfs_MemFile_close(JNIEnv *env, jobject thiz, jint fd) {
- close(fd);
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-hdpi/icon_document_provider.png b/app/src/main/res/drawable-hdpi/icon_document_provider.png
new file mode 100644
index 0000000..a314350
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon_document_provider.png differ
diff --git a/app/src/main/res/drawable-mdpi/icon_document_provider.png b/app/src/main/res/drawable-mdpi/icon_document_provider.png
new file mode 100644
index 0000000..4f52db5
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/icon_document_provider.png differ
diff --git a/app/src/main/res/drawable-xhdpi/icon_document_provider.png b/app/src/main/res/drawable-xhdpi/icon_document_provider.png
new file mode 100644
index 0000000..754bfa2
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/icon_document_provider.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/icon_document_provider.png b/app/src/main/res/drawable-xxhdpi/icon_document_provider.png
new file mode 100644
index 0000000..0fb2781
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/icon_document_provider.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/icon_document_provider.png b/app/src/main/res/drawable-xxxhdpi/icon_document_provider.png
new file mode 100644
index 0000000..c38fbb9
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/icon_document_provider.png differ
diff --git a/app/src/main/res/drawable/icon_edit.xml b/app/src/main/res/drawable/icon_edit.xml
new file mode 100644
index 0000000..6af5f99
--- /dev/null
+++ b/app/src/main/res/drawable/icon_edit.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/icon_transfert.xml b/app/src/main/res/drawable/icon_transfer.xml
similarity index 100%
rename from app/src/main/res/drawable/icon_transfert.xml
rename to app/src/main/res/drawable/icon_transfer.xml
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f49f63c..216cc06 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -263,4 +263,14 @@
The filesystem id in the config file is different to the last time we opened this volume. This could mean an attacker replaced the filesystem with a different one.
The volume doesn\'t exist or is inaccessible.
The task failed: %s
+ Expose open volumes
+ Allow other applications to browse open volumes as documents providers
+ Grant write access
+ Grant write access when opening files with other applications
+ Storage Access Framework
+ Export failed: %s
+ can\'t create exported file
+ failed to export file
+ Exporting to memory…
+ Exporting to disk…
diff --git a/app/src/main/res/xml/unsafe_features_preferences.xml b/app/src/main/res/xml/unsafe_features_preferences.xml
index a1237f9..13f02b4 100644
--- a/app/src/main/res/xml/unsafe_features_preferences.xml
+++ b/app/src/main/res/xml/unsafe_features_preferences.xml
@@ -1,6 +1,19 @@
+
+
+
+
+
+
+
+
-
+
-
-
@@ -45,18 +48,35 @@
android:key="usf_fingerprint"
android:title="@string/usf_fingerprint" />
+
+
-
+
-
-
-
+
+
+
+
+