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