MemoryFileProvider

This commit is contained in:
Matéo Duparc 2023-08-20 14:56:46 +02:00
parent 1727170cb6
commit a08da2eacb
Signed by untrusted user: hardcoresushi
GPG Key ID: AFE384344A45E13A
14 changed files with 383 additions and 347 deletions

View File

@ -5,6 +5,8 @@ project(DroidFS)
option(GOCRYPTFS "build libgocryptfs" ON) option(GOCRYPTFS "build libgocryptfs" ON)
option(CRYFS "build libcryfs" ON) option(CRYFS "build libcryfs" ON)
add_library(memfile SHARED src/main/native/memfile.cpp)
if (GOCRYPTFS) if (GOCRYPTFS)
add_library(gocryptfs SHARED IMPORTED) add_library(gocryptfs SHARED IMPORTED)
set_target_properties( set_target_properties(

View File

@ -68,8 +68,14 @@
</receiver> </receiver>
<provider <provider
android:name=".content_providers.RestrictedFileProvider" android:name=".content_providers.MemoryFileProvider"
android:authorities="${applicationId}.temporary_provider" android:authorities="${applicationId}.memory_provider"
android:exported="true"
android:writePermission="${applicationId}.WRITE_TEMPORARY_STORAGE" />
<provider
android:name=".content_providers.DiskFileProvider"
android:authorities="${applicationId}.disk_provider"
android:exported="true" android:exported="true"
android:writePermission="${applicationId}.WRITE_TEMPORARY_STORAGE" /> android:writePermission="${applicationId}.WRITE_TEMPORARY_STORAGE" />
</application> </application>

View File

@ -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<Uri?, String?> {
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<String, Long>>): Pair<Intent?, String?> {
var contentType: String? = null
val uris = ArrayList<Uri>(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
}
}
}

View File

@ -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)
}

View File

@ -6,7 +6,8 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.preference.PreferenceManager 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 { class VolumeManagerApp : Application(), DefaultLifecycleObserver {
companion object { companion object {
@ -45,7 +46,8 @@ class VolumeManagerApp : Application(), DefaultLifecycleObserver {
if (!usfKeepOpen) { if (!usfKeepOpen) {
volumeManager.closeAll() volumeManager.closeAll()
} }
RestrictedFileProvider.wipeAll(applicationContext) DiskFileProvider.wipe()
MemoryFileProvider.wipe()
} }
} }
} }

View File

@ -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<File>() {
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<Uri, TemporaryFileProvider<File>.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<String>?): 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")
}
}
}

View File

@ -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<Uri>()
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<Uri?, String?> {
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<String>) {
var contentType: String? = null
val uris = ArrayList<Uri>(file_paths.size)
object : LoadingTask<String?>(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<Intent?>(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<Uri>(storedFiles.size)
for (uri in storedFiles) {
if (context.contentResolver.delete(uri, null, null) == 1) {
success.add(uri)
}
}
for (uri in success) {
storedFiles.remove(uri)
}
}
}

View File

@ -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<MemFile>() {
companion object {
private const val AUTHORITY = BuildConfig.APPLICATION_ID + ".memory_provider"
private val BASE_URI: Uri = Uri.parse("content://$AUTHORITY")
private var files = HashMap<Uri, TemporaryFileProvider<MemFile>.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<out String>?): 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()
}
}
}
}

View File

@ -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<String>?): Int {
throw RuntimeException("Operation not supported")
}
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, 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<String>?): 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
}
}

View File

@ -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<T>: 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<String>?, selection: String?, selectionArgs: Array<String>?, 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<String>?): 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"
}

View File

@ -26,10 +26,12 @@ import kotlinx.coroutines.*
import sushi.hardcore.droidfs.* import sushi.hardcore.droidfs.*
import sushi.hardcore.droidfs.adapters.ExplorerElementAdapter import sushi.hardcore.droidfs.adapters.ExplorerElementAdapter
import sushi.hardcore.droidfs.adapters.OpenAsDialogAdapter 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.FileOperationService
import sushi.hardcore.droidfs.file_operations.OperationFile import sushi.hardcore.droidfs.file_operations.OperationFile
import sushi.hardcore.droidfs.file_operations.TaskResult import sushi.hardcore.droidfs.file_operations.TaskResult
import sushi.hardcore.droidfs.FileShare
import sushi.hardcore.droidfs.file_viewers.* import sushi.hardcore.droidfs.file_viewers.*
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat import sushi.hardcore.droidfs.filesystems.Stat
@ -69,6 +71,9 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
private lateinit var numberOfFilesText: TextView private lateinit var numberOfFilesText: TextView
private lateinit var numberOfFoldersText: TextView private lateinit var numberOfFoldersText: TextView
private lateinit var totalSizeText: TextView private lateinit var totalSizeText: TextView
protected val fileShare by lazy {
FileShare(encryptedVolume, this)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -187,12 +192,27 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
startActivity(intent) startActivity(intent)
} }
private fun openWithExternalApp(fullPath: String) { private fun openWithExternalApp(path: String, size: Long) {
app.isStartingExternalApp = true app.isStartingExternalApp = true
ExternalProvider.open(this, theme, encryptedVolume, fullPath) object : LoadingTask<Intent?>(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) val adapter = OpenAsDialogAdapter(this, usf_open)
CustomAlertDialogBuilder(this, theme) CustomAlertDialogBuilder(this, theme)
.setSingleChoiceItems(adapter, -1) { dialog, which -> .setSingleChoiceItems(adapter, -1) { dialog, which ->
@ -203,7 +223,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
"pdf" -> startFileViewer(PdfViewer::class.java, path) "pdf" -> startFileViewer(PdfViewer::class.java, path)
"text" -> startFileViewer(TextEditor::class.java, path) "text" -> startFileViewer(TextEditor::class.java, path)
"external" -> if (usf_open) { "external" -> if (usf_open) {
openWithExternalApp(path) openWithExternalApp(path, explorerElement.stat.size)
} }
} }
dialog.dismiss() dialog.dismiss()
@ -250,7 +270,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
FileTypes.isAudio(fullPath) -> { FileTypes.isAudio(fullPath) -> {
startFileViewer(AudioPlayer::class.java, fullPath) startFileViewer(AudioPlayer::class.java, fullPath)
} }
else -> showOpenAsDialog(fullPath) else -> showOpenAsDialog(explorerElements[position])
} }
} }
invalidateOptionsMenu() invalidateOptionsMenu()
@ -575,22 +595,13 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
true true
} }
R.id.open_as -> { R.id.open_as -> {
showOpenAsDialog( showOpenAsDialog(explorerElements[explorerAdapter.selectedItems.first()])
PathUtils.pathJoin(
currentDirectoryPath,
explorerElements[explorerAdapter.selectedItems.first()].name
)
)
true true
} }
R.id.external_open -> { R.id.external_open -> {
if (usf_open){ if (usf_open){
openWithExternalApp( val explorerElement = explorerElements[explorerAdapter.selectedItems.first()]
PathUtils.pathJoin( openWithExternalApp(explorerElement.fullPath, explorerElement.stat.size)
currentDirectoryPath,
explorerElements[explorerAdapter.selectedItems.first()].name
)
)
unselectAll() unselectAll()
} }
true true
@ -617,7 +628,8 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
if (app.isStartingExternalApp) { if (app.isStartingExternalApp) {
ExternalProvider.removeFilesAsync(this) MemoryFileProvider.wipe()
DiskFileProvider.wipe()
} }
if (encryptedVolume.isClosed()) { if (encryptedVolume.isClosed()) {
finish() finish()

View File

@ -17,7 +17,6 @@ import sushi.hardcore.droidfs.LoadingTask
import sushi.hardcore.droidfs.MainActivity import sushi.hardcore.droidfs.MainActivity
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.adapters.IconTextDialogAdapter import sushi.hardcore.droidfs.adapters.IconTextDialogAdapter
import sushi.hardcore.droidfs.content_providers.ExternalProvider
import sushi.hardcore.droidfs.file_operations.OperationFile import sushi.hardcore.droidfs.file_operations.OperationFile
import sushi.hardcore.droidfs.filesystems.Stat import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
@ -386,12 +385,27 @@ class ExplorerActivity : BaseExplorerActivity() {
true true
} }
R.id.share -> { R.id.share -> {
val paths: MutableList<String> = ArrayList()
for (i in explorerAdapter.selectedItems) {
paths.add(explorerElements[i].fullPath)
}
app.isStartingExternalApp = true 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<Pair<Intent?, String?>>(this, theme, R.string.loading_msg_export) {
override suspend fun doTask(): Pair<Intent?, String?> {
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() unselectAll()
true true
} }

View File

@ -0,0 +1,27 @@
package sushi.hardcore.droidfs.util
import java.lang.Integer.max
class Version(inputVersion: String) : Comparable<Version> {
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
}
}

View File

@ -0,0 +1,24 @@
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <jni.h>
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);
}