MemoryFileProvider
This commit is contained in:
parent
1727170cb6
commit
a08da2eacb
app
CMakeLists.txt
src/main
AndroidManifest.xml
java/sushi/hardcore/droidfs
FileShare.ktMemFile.ktVolumeManagerApp.kt
content_providers
DiskFileProvider.ktExternalProvider.ktMemoryFileProvider.ktRestrictedFileProvider.ktTemporaryFileProvider.kt
explorers
util
native
@ -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(
|
||||
|
@ -68,8 +68,14 @@
|
||||
</receiver>
|
||||
|
||||
<provider
|
||||
android:name=".content_providers.RestrictedFileProvider"
|
||||
android:authorities="${applicationId}.temporary_provider"
|
||||
android:name=".content_providers.MemoryFileProvider"
|
||||
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:writePermission="${applicationId}.WRITE_TEMPORARY_STORAGE" />
|
||||
</application>
|
||||
|
93
app/src/main/java/sushi/hardcore/droidfs/FileShare.kt
Normal file
93
app/src/main/java/sushi/hardcore/droidfs/FileShare.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
22
app/src/main/java/sushi/hardcore/droidfs/MemFile.kt
Normal file
22
app/src/main/java/sushi/hardcore/droidfs/MemFile.kt
Normal 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)
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
@ -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<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)
|
||||
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()
|
||||
|
@ -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<String> = 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<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()
|
||||
true
|
||||
}
|
||||
|
27
app/src/main/java/sushi/hardcore/droidfs/util/Version.kt
Normal file
27
app/src/main/java/sushi/hardcore/droidfs/util/Version.kt
Normal 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
|
||||
}
|
||||
}
|
24
app/src/main/native/memfile.cpp
Normal file
24
app/src/main/native/memfile.cpp
Normal 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);
|
||||
}
|
Loading…
Reference in New Issue
Block a user