forked from hardcoresushi/DroidFS
MemoryFileProvider
This commit is contained in:
parent
1727170cb6
commit
a08da2eacb
@ -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(
|
||||||
|
@ -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>
|
||||||
|
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.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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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.*
|
||||||
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()
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
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