diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
index d28c53b..2d67425 100644
--- a/app/CMakeLists.txt
+++ b/app/CMakeLists.txt
@@ -5,6 +5,8 @@ project(DroidFS)
option(GOCRYPTFS "build libgocryptfs" ON)
option(CRYFS "build libcryfs" ON)
+add_library(memfile SHARED src/main/native/memfile.cpp)
+
if (GOCRYPTFS)
add_library(gocryptfs SHARED IMPORTED)
set_target_properties(
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0c43b8b..c4ed4c4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -68,8 +68,14 @@
+
+
diff --git a/app/src/main/java/sushi/hardcore/droidfs/FileShare.kt b/app/src/main/java/sushi/hardcore/droidfs/FileShare.kt
new file mode 100644
index 0000000..14b5e2b
--- /dev/null
+++ b/app/src/main/java/sushi/hardcore/droidfs/FileShare.kt
@@ -0,0 +1,93 @@
+package sushi.hardcore.droidfs
+
+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 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) {
+
+ companion object {
+ private const val content_type_all = "*/*"
+ fun getContentType(filename: String, previousContentType: String?): String {
+ if (content_type_all != previousContentType) {
+ var contentType = MimeTypeMap.getSingleton()
+ .getMimeTypeFromExtension(File(filename).extension)
+ if (contentType == null) {
+ contentType = content_type_all
+ }
+ if (previousContentType == null) {
+ return contentType
+ } else if (previousContentType != contentType) {
+ return content_type_all
+ }
+ }
+ return previousContentType
+ }
+ }
+
+ private val fileProvider: TemporaryFileProvider<*>
+
+ init {
+ var provider: MemoryFileProvider? = null
+ System.getProperty("os.version")?.let {
+ if (Version(it) >= Version("3.17")) {
+ provider = MemoryFileProvider()
+ }
+ }
+ fileProvider = provider ?: DiskFileProvider()
+ }
+
+ 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 {
+ 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)
+ } else {
+ uris.add(result.first!!)
+ result.second
+ }
+ }
+ return Pair(Intent().apply {
+ type = contentType
+ if (uris.size == 1) {
+ action = Intent.ACTION_SEND
+ putExtra(Intent.EXTRA_STREAM, uris[0])
+ } else {
+ action = Intent.ACTION_SEND_MULTIPLE
+ putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
+ }
+ }, 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)
+ }
+ } else {
+ 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
new file mode 100644
index 0000000..d8b3fcb
--- /dev/null
+++ b/app/src/main/java/sushi/hardcore/droidfs/MemFile.kt
@@ -0,0 +1,22 @@
+package sushi.hardcore.droidfs
+
+import android.os.ParcelFileDescriptor
+
+class MemFile private constructor(private val fd: Int) {
+ companion object {
+ private external fun createMemFile(name: String, size: Long): Int
+ init {
+ System.loadLibrary("memfile")
+ }
+
+ fun create(name: String, size: Long): MemFile? {
+ val fd = createMemFile(name, size)
+ return if (fd > 0) MemFile(fd) else null
+ }
+ }
+
+ private external fun close(fd: Int)
+
+ fun getParcelFileDescriptor(): ParcelFileDescriptor = ParcelFileDescriptor.fromFd(fd)
+ fun close() = close(fd)
+}
\ 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 4e270c7..94849fc 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/VolumeManagerApp.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/VolumeManagerApp.kt
@@ -6,7 +6,8 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.preference.PreferenceManager
-import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
+import sushi.hardcore.droidfs.content_providers.MemoryFileProvider
+import sushi.hardcore.droidfs.content_providers.DiskFileProvider
class VolumeManagerApp : Application(), DefaultLifecycleObserver {
companion object {
@@ -45,7 +46,8 @@ class VolumeManagerApp : Application(), DefaultLifecycleObserver {
if (!usfKeepOpen) {
volumeManager.closeAll()
}
- RestrictedFileProvider.wipeAll(applicationContext)
+ DiskFileProvider.wipe()
+ MemoryFileProvider.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
new file mode 100644
index 0000000..87d0903
--- /dev/null
+++ b/app/src/main/java/sushi/hardcore/droidfs/content_providers/DiskFileProvider.kt
@@ -0,0 +1,68 @@
+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/ExternalProvider.kt b/app/src/main/java/sushi/hardcore/droidfs/content_providers/ExternalProvider.kt
deleted file mode 100644
index 2330b03..0000000
--- a/app/src/main/java/sushi/hardcore/droidfs/content_providers/ExternalProvider.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-package sushi.hardcore.droidfs.content_providers
-
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import android.webkit.MimeTypeMap
-import androidx.appcompat.app.AppCompatActivity
-import androidx.lifecycle.lifecycleScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.GlobalScope
-import kotlinx.coroutines.launch
-import sushi.hardcore.droidfs.LoadingTask
-import sushi.hardcore.droidfs.R
-import sushi.hardcore.droidfs.Theme
-import sushi.hardcore.droidfs.filesystems.EncryptedVolume
-import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
-import java.io.File
-
-object ExternalProvider {
- private const val content_type_all = "*/*"
- private var storedFiles = HashSet()
- private fun getContentType(filename: String, previous_content_type: String?): String {
- if (content_type_all != previous_content_type) {
- var contentType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(File(filename).extension)
- if (contentType == null) {
- contentType = content_type_all
- }
- if (previous_content_type == null) {
- return contentType
- } else if (previous_content_type != contentType) {
- return content_type_all
- }
- }
- return previous_content_type
- }
-
- private fun exportFile(context: Context, encryptedVolume: EncryptedVolume, file_path: String, previous_content_type: String?): Pair {
- val fileName = File(file_path).name
- val tmpFileUri = RestrictedFileProvider.newFile(fileName)
- if (tmpFileUri != null){
- storedFiles.add(tmpFileUri)
- if (encryptedVolume.exportFile(context, file_path, tmpFileUri)) {
- return Pair(tmpFileUri, getContentType(fileName, previous_content_type))
- }
- }
- return Pair(null, null)
- }
-
- fun share(activity: AppCompatActivity, theme: Theme, encryptedVolume: EncryptedVolume, file_paths: List) {
- var contentType: String? = null
- val uris = ArrayList(file_paths.size)
- object : LoadingTask(activity, theme, R.string.loading_msg_export) {
- override suspend fun doTask(): String? {
- for (path in file_paths) {
- val result = exportFile(activity, encryptedVolume, path, contentType)
- contentType = if (result.first != null) {
- uris.add(result.first!!)
- result.second
- } else {
- return path
- }
- }
- return null
- }
- }.startTask(activity.lifecycleScope) { failedItem ->
- if (failedItem == null) {
- val shareIntent = Intent()
- shareIntent.type = contentType
- if (uris.size == 1) {
- shareIntent.action = Intent.ACTION_SEND
- shareIntent.putExtra(Intent.EXTRA_STREAM, uris[0])
- } else {
- shareIntent.action = Intent.ACTION_SEND_MULTIPLE
- shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
- }
- activity.startActivity(Intent.createChooser(shareIntent, activity.getString(R.string.share_chooser)))
- } else {
- CustomAlertDialogBuilder(activity, theme)
- .setTitle(R.string.error)
- .setMessage(activity.getString(R.string.export_failed, failedItem))
- .setPositiveButton(R.string.ok, null)
- .show()
- }
- }
- }
-
- fun open(activity: AppCompatActivity, theme: Theme, encryptedVolume: EncryptedVolume, file_path: String) {
- object : LoadingTask(activity, theme, R.string.loading_msg_export) {
- override suspend fun doTask(): Intent? {
- val result = exportFile(activity, encryptedVolume, file_path, null)
- return if (result.first != null) {
- Intent(Intent.ACTION_VIEW).apply {
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- setDataAndType(result.first, result.second)
- }
- } else {
- null
- }
- }
- }.startTask(activity.lifecycleScope) { openIntent ->
- if (openIntent == null) {
- CustomAlertDialogBuilder(activity, theme)
- .setTitle(R.string.error)
- .setMessage(activity.getString(R.string.export_failed, file_path))
- .setPositiveButton(R.string.ok, null)
- .show()
- } else {
- activity.startActivity(openIntent)
- }
- }
- }
-
- fun removeFilesAsync(context: Context) = GlobalScope.launch(Dispatchers.IO) {
- val success = HashSet(storedFiles.size)
- for (uri in storedFiles) {
- if (context.contentResolver.delete(uri, null, null) == 1) {
- success.add(uri)
- }
- }
- for (uri in success) {
- storedFiles.remove(uri)
- }
- }
-}
\ 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
new file mode 100644
index 0000000..f6bf759
--- /dev/null
+++ b/app/src/main/java/sushi/hardcore/droidfs/content_providers/MemoryFileProvider.kt
@@ -0,0 +1,48 @@
+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/RestrictedFileProvider.kt b/app/src/main/java/sushi/hardcore/droidfs/content_providers/RestrictedFileProvider.kt
deleted file mode 100644
index 5c83108..0000000
--- a/app/src/main/java/sushi/hardcore/droidfs/content_providers/RestrictedFileProvider.kt
+++ /dev/null
@@ -1,194 +0,0 @@
-package sushi.hardcore.droidfs.content_providers
-
-import android.content.ContentProvider
-import android.content.ContentValues
-import android.content.Context
-import android.database.Cursor
-import android.database.MatrixCursor
-import android.database.sqlite.SQLiteDatabase
-import android.database.sqlite.SQLiteOpenHelper
-import android.net.Uri
-import android.os.ParcelFileDescriptor
-import android.provider.MediaStore
-import sushi.hardcore.droidfs.BuildConfig
-import sushi.hardcore.droidfs.util.SQLUtil.appendSelectionArgs
-import sushi.hardcore.droidfs.util.SQLUtil.concatenateWhere
-import sushi.hardcore.droidfs.util.Wiper
-import java.io.File
-import java.util.UUID
-import java.util.regex.Pattern
-
-class RestrictedFileProvider: ContentProvider() {
- companion object {
- private const val DB_NAME = "temporary_files.db"
- private const val TABLE_FILES = "files"
- private const val DB_VERSION = 3
- private var dbHelper: RestrictedDatabaseHelper? = null
- private const val AUTHORITY = BuildConfig.APPLICATION_ID + ".temporary_provider"
- private val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY")
- const val TEMPORARY_FILES_DIR_NAME = "temp"
- private val UUID_PATTERN = Pattern.compile("[a-fA-F0-9-]+")
-
- private lateinit var tempFilesDir: File
-
- internal class TemporaryFileColumns {
- companion object {
- const val COLUMN_UUID = "uuid"
- const val COLUMN_NAME = "name"
- }
- }
-
- internal class RestrictedDatabaseHelper(context: Context?): SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
- override fun onCreate(db: SQLiteDatabase) {
- db.execSQL(
- "CREATE TABLE IF NOT EXISTS " + TABLE_FILES + " (" +
- TemporaryFileColumns.COLUMN_UUID + " TEXT PRIMARY KEY, " +
- TemporaryFileColumns.COLUMN_NAME + " TEXT" +
- ");"
- )
- }
-
- override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
- if (oldVersion == 1) {
- db.execSQL("DROP TABLE IF EXISTS files")
- db.execSQL(
- "CREATE TABLE IF NOT EXISTS " + TABLE_FILES + " (" +
- TemporaryFileColumns.COLUMN_UUID + " TEXT PRIMARY KEY, " +
- TemporaryFileColumns.COLUMN_NAME + " TEXT" +
- ");"
- )
- }
- }
- }
-
- fun newFile(fileName: String): Uri? {
- val uuid = UUID.randomUUID().toString()
- val file = File(tempFilesDir, uuid)
- return if (file.createNewFile()){
- val contentValues = ContentValues()
- contentValues.put(TemporaryFileColumns.COLUMN_UUID, uuid)
- contentValues.put(TemporaryFileColumns.COLUMN_NAME, fileName)
- if (dbHelper?.writableDatabase?.insert(TABLE_FILES, null, contentValues)?.toInt() != -1){
- Uri.withAppendedPath(CONTENT_URI, uuid)
- } else {
- null
- }
- } else {
- null
- }
- }
-
- fun wipeAll(context: Context) {
- tempFilesDir.listFiles()?.let{
- for (file in it) {
- Wiper.wipe(file)
- }
- }
- dbHelper?.close()
- context.deleteDatabase(DB_NAME)
- }
-
- private fun isValidUUID(uuid: String): Boolean {
- return UUID_PATTERN.matcher(uuid).matches()
- }
-
- private fun getUuidFromUri(uri: Uri): String? {
- val uuid = uri.lastPathSegment
- if (uuid != null) {
- if (isValidUUID(uuid)) {
- return uuid
- }
- }
- return null
- }
-
- private fun getFileFromUUID(uuid: String): File? {
- if (isValidUUID(uuid)){
- return File(tempFilesDir, uuid)
- }
- return null
- }
-
- private fun getFileFromUri(uri: Uri): File? {
- getUuidFromUri(uri)?.let {
- return getFileFromUUID(it)
- }
- return null
- }
- }
-
- override fun onCreate(): Boolean {
- context?.let {
- dbHelper = RestrictedDatabaseHelper(it)
- tempFilesDir = File(it.cacheDir, TEMPORARY_FILES_DIR_NAME)
- return tempFilesDir.mkdirs()
- }
- return false
- }
-
- override fun insert(uri: Uri, values: ContentValues?): Uri? {
- throw RuntimeException("Operation not supported")
- }
-
- override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int {
- throw RuntimeException("Operation not supported")
- }
-
- override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? {
- var resultCursor: MatrixCursor? = null
- val temporaryFile = getFileFromUri(uri)
- temporaryFile?.let{
- val fileName = dbHelper?.readableDatabase?.query(TABLE_FILES, arrayOf(TemporaryFileColumns.COLUMN_NAME), TemporaryFileColumns.COLUMN_UUID + "=?", arrayOf(uri.lastPathSegment), null, null, null)
- fileName?.let{
- if (fileName.moveToNext()) {
- resultCursor = MatrixCursor(
- arrayOf(
- MediaStore.MediaColumns.DISPLAY_NAME,
- MediaStore.MediaColumns.SIZE
- )
- )
- resultCursor!!.newRow()
- .add(fileName.getString(0))
- .add(temporaryFile.length())
- }
- fileName.close()
- }
- }
- return resultCursor
- }
-
- override fun delete(uri: Uri, givenSelection: String?, givenSelectionArgs: Array?): Int {
- val uuid = getUuidFromUri(uri)
- uuid?.let{
- val selection = concatenateWhere(givenSelection ?: "" , TemporaryFileColumns.COLUMN_UUID + "=?")
- val selectionArgs = appendSelectionArgs(givenSelectionArgs, arrayOf(it))
-
- val files = dbHelper?.readableDatabase?.query(TABLE_FILES, arrayOf(TemporaryFileColumns.COLUMN_UUID), selection, selectionArgs, null, null, null)
- if (files != null) {
- while (files.moveToNext()) {
- getFileFromUUID(files.getString(0))?.let { file ->
- Wiper.wipe(file)
- }
- }
- files.close()
- return dbHelper?.writableDatabase?.delete(TABLE_FILES, selection, selectionArgs) ?: 0
- }
- }
- return 0
- }
-
- override fun getType(uri: Uri): String {
- return "application/octet-stream"
- }
-
- override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
- if (("w" in mode && callingPackage == BuildConfig.APPLICATION_ID) || "w" !in mode) {
- getFileFromUri(uri)?.let{
- return ParcelFileDescriptor.open(it, ParcelFileDescriptor.parseMode(mode))
- }
- } else {
- throw SecurityException("Read-only access")
- }
- return null
- }
-}
\ 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
new file mode 100644
index 0000000..52658bb
--- /dev/null
+++ b/app/src/main/java/sushi/hardcore/droidfs/content_providers/TemporaryFileProvider.kt
@@ -0,0 +1,36 @@
+package sushi.hardcore.droidfs.content_providers
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.provider.OpenableColumns
+import android.webkit.MimeTypeMap
+import java.io.File
+
+abstract class TemporaryFileProvider: ContentProvider() {
+ protected inner class SharedFile(val name: String, val size: Long, val file: T)
+
+ protected abstract fun getFile(uri: Uri): SharedFile?
+ abstract fun newFile(name: String, size: Long): Uri?
+
+ override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? {
+ val file = getFile(uri) ?: return null
+ return MatrixCursor(arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE), 1).apply {
+ addRow(arrayOf(file.name, file.size))
+ }
+ }
+
+ override fun insert(uri: Uri, values: ContentValues?): Uri? {
+ throw UnsupportedOperationException("Operation not supported")
+ }
+
+ 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 {
+ MimeTypeMap.getSingleton().getMimeTypeFromExtension(File(it).extension)
+ } ?: "application/octet-stream"
+}
\ 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 35f4c0a..a252309 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt
@@ -26,10 +26,12 @@ import kotlinx.coroutines.*
import sushi.hardcore.droidfs.*
import sushi.hardcore.droidfs.adapters.ExplorerElementAdapter
import sushi.hardcore.droidfs.adapters.OpenAsDialogAdapter
-import sushi.hardcore.droidfs.content_providers.ExternalProvider
+import sushi.hardcore.droidfs.content_providers.MemoryFileProvider
+import sushi.hardcore.droidfs.content_providers.DiskFileProvider
import sushi.hardcore.droidfs.file_operations.FileOperationService
import sushi.hardcore.droidfs.file_operations.OperationFile
import sushi.hardcore.droidfs.file_operations.TaskResult
+import sushi.hardcore.droidfs.FileShare
import sushi.hardcore.droidfs.file_viewers.*
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat
@@ -69,6 +71,9 @@ 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)
+ }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -187,12 +192,27 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
startActivity(intent)
}
- private fun openWithExternalApp(fullPath: String) {
+ private fun openWithExternalApp(path: String, size: Long) {
app.isStartingExternalApp = true
- ExternalProvider.open(this, theme, encryptedVolume, fullPath)
+ object : LoadingTask(this, theme, R.string.loading_msg_export) {
+ override suspend fun doTask(): Intent? {
+ return fileShare.openWith(path, size)
+ }
+ }.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()
+ } else {
+ startActivity(openIntent)
+ }
+ }
}
- private fun showOpenAsDialog(path: String) {
+ private fun showOpenAsDialog(explorerElement: ExplorerElement) {
+ val path = explorerElement.fullPath
val adapter = OpenAsDialogAdapter(this, usf_open)
CustomAlertDialogBuilder(this, theme)
.setSingleChoiceItems(adapter, -1) { dialog, which ->
@@ -203,7 +223,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
"pdf" -> startFileViewer(PdfViewer::class.java, path)
"text" -> startFileViewer(TextEditor::class.java, path)
"external" -> if (usf_open) {
- openWithExternalApp(path)
+ openWithExternalApp(path, explorerElement.stat.size)
}
}
dialog.dismiss()
@@ -250,7 +270,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
FileTypes.isAudio(fullPath) -> {
startFileViewer(AudioPlayer::class.java, fullPath)
}
- else -> showOpenAsDialog(fullPath)
+ else -> showOpenAsDialog(explorerElements[position])
}
}
invalidateOptionsMenu()
@@ -575,22 +595,13 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
true
}
R.id.open_as -> {
- showOpenAsDialog(
- PathUtils.pathJoin(
- currentDirectoryPath,
- explorerElements[explorerAdapter.selectedItems.first()].name
- )
- )
+ showOpenAsDialog(explorerElements[explorerAdapter.selectedItems.first()])
true
}
R.id.external_open -> {
if (usf_open){
- openWithExternalApp(
- PathUtils.pathJoin(
- currentDirectoryPath,
- explorerElements[explorerAdapter.selectedItems.first()].name
- )
- )
+ val explorerElement = explorerElements[explorerAdapter.selectedItems.first()]
+ openWithExternalApp(explorerElement.fullPath, explorerElement.stat.size)
unselectAll()
}
true
@@ -617,7 +628,8 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
override fun onResume() {
super.onResume()
if (app.isStartingExternalApp) {
- ExternalProvider.removeFilesAsync(this)
+ MemoryFileProvider.wipe()
+ DiskFileProvider.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 d3c919e..7a65aee 100644
--- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt
+++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt
@@ -17,7 +17,6 @@ import sushi.hardcore.droidfs.LoadingTask
import sushi.hardcore.droidfs.MainActivity
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.adapters.IconTextDialogAdapter
-import sushi.hardcore.droidfs.content_providers.ExternalProvider
import sushi.hardcore.droidfs.file_operations.OperationFile
import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.PathUtils
@@ -386,12 +385,27 @@ class ExplorerActivity : BaseExplorerActivity() {
true
}
R.id.share -> {
- val paths: MutableList = ArrayList()
- for (i in explorerAdapter.selectedItems) {
- paths.add(explorerElements[i].fullPath)
- }
app.isStartingExternalApp = true
- ExternalProvider.share(this, theme, encryptedVolume, paths)
+ 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)
+ }
+ }.startTask(lifecycleScope) { (intent, failedItem) ->
+ if (intent == null) {
+ CustomAlertDialogBuilder(this, theme)
+ .setTitle(R.string.error)
+ .setMessage(getString(R.string.export_failed, failedItem))
+ .setPositiveButton(R.string.ok, null)
+ .show()
+ } else {
+ startActivity(Intent.createChooser(intent, getString(R.string.share_chooser)))
+ }
+ }
unselectAll()
true
}
diff --git a/app/src/main/java/sushi/hardcore/droidfs/util/Version.kt b/app/src/main/java/sushi/hardcore/droidfs/util/Version.kt
new file mode 100644
index 0000000..844f0fc
--- /dev/null
+++ b/app/src/main/java/sushi/hardcore/droidfs/util/Version.kt
@@ -0,0 +1,27 @@
+package sushi.hardcore.droidfs.util
+
+import java.lang.Integer.max
+
+class Version(inputVersion: String) : Comparable {
+ private var version: String
+
+ init {
+ val regex = "[0-9]+(\\.[0-9]+)*".toRegex()
+ val match = regex.find(inputVersion) ?: throw IllegalArgumentException("Invalid version format")
+ version = match.value
+ }
+
+ fun split() = version.split(".").toTypedArray()
+
+ override fun compareTo(other: Version) =
+ (split() to other.split()).let { (split, otherSplit) ->
+ val length = max(split.size, otherSplit.size)
+ for (i in 0 until length) {
+ val part = if (i < split.size) split[i].toInt() else 0
+ val otherPart = if (i < otherSplit.size) otherSplit[i].toInt() else 0
+ if (part < otherPart) return -1
+ if (part > otherPart) return 1
+ }
+ 0
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/native/memfile.cpp b/app/src/main/native/memfile.cpp
new file mode 100644
index 0000000..8f71128
--- /dev/null
+++ b/app/src/main/native/memfile.cpp
@@ -0,0 +1,24 @@
+#include
+#include
+#include
+#include
+
+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);
+ int fd = syscall(SYS_memfd_create, name, MFD_CLOEXEC);
+ if (fd < 0) return fd;
+ if (ftruncate64(fd, size) == -1) {
+ 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