forked from hardcoresushi/DroidFS
Volume provider
This commit is contained in:
parent
6d04349b2e
commit
79db84f81d
@ -5,7 +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)
|
add_library(memfile SHARED src/main/native/memfile.c)
|
||||||
|
target_link_libraries(memfile log)
|
||||||
|
|
||||||
if (GOCRYPTFS)
|
if (GOCRYPTFS)
|
||||||
add_library(gocryptfs SHARED IMPORTED)
|
add_library(gocryptfs SHARED IMPORTED)
|
||||||
|
@ -3,10 +3,6 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:installLocation="auto">
|
android:installLocation="auto">
|
||||||
|
|
||||||
<permission
|
|
||||||
android:name="${applicationId}.WRITE_TEMPORARY_STORAGE"
|
|
||||||
android:protectionLevel="signature" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
@ -68,16 +64,21 @@
|
|||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".content_providers.MemoryFileProvider"
|
android:name=".content_providers.TemporaryFileProvider"
|
||||||
android:authorities="${applicationId}.memory_provider"
|
android:authorities="${applicationId}.temporary_provider"
|
||||||
android:exported="true"
|
android:exported="true"/>
|
||||||
android:writePermission="${applicationId}.WRITE_TEMPORARY_STORAGE" />
|
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".content_providers.DiskFileProvider"
|
android:authorities="${applicationId}.volume_provider"
|
||||||
android:authorities="${applicationId}.disk_provider"
|
android:name=".content_providers.VolumeProvider"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:writePermission="${applicationId}.WRITE_TEMPORARY_STORAGE" />
|
android:grantUriPermissions="true"
|
||||||
|
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||||
|
</intent-filter>
|
||||||
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -160,7 +160,7 @@ class ChangePasswordActivity: BaseActivity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (success) {
|
if (success) {
|
||||||
if (volumeDatabase.isHashSaved(volume.name)) {
|
if (volumeDatabase.isHashSaved(volume)) {
|
||||||
volumeDatabase.removeHash(volume)
|
volumeDatabase.removeHash(volume)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,225 @@
|
|||||||
|
package sushi.hardcore.droidfs
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.system.Os
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
||||||
|
import sushi.hardcore.droidfs.util.Compat
|
||||||
|
import sushi.hardcore.droidfs.util.Wiper
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class EncryptedFileProvider(context: Context) {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "EncryptedFileProvider"
|
||||||
|
fun getTmpFilesDir(context: Context) = File(context.cacheDir, "tmp")
|
||||||
|
}
|
||||||
|
|
||||||
|
private val memoryInfo = ActivityManager.MemoryInfo()
|
||||||
|
private val isMemFileSupported = Compat.isMemFileSupported()
|
||||||
|
private val tmpFilesDir by lazy { getTmpFilesDir(context) }
|
||||||
|
private val handler by lazy { Handler(context.mainLooper) }
|
||||||
|
|
||||||
|
init {
|
||||||
|
(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(
|
||||||
|
memoryInfo
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExportedDiskFile private constructor(
|
||||||
|
path: String,
|
||||||
|
private val file: File,
|
||||||
|
private val handler: Handler
|
||||||
|
) : ExportedFile(path) {
|
||||||
|
companion object {
|
||||||
|
fun create(path: String, tmpFilesDir: File, handler: Handler): ExportedDiskFile? {
|
||||||
|
val uuid = UUID.randomUUID().toString()
|
||||||
|
val file = File(tmpFilesDir, uuid)
|
||||||
|
return if (file.createNewFile()) {
|
||||||
|
ExportedDiskFile(path, file, handler)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun open(mode: Int, furtive: Boolean): ParcelFileDescriptor {
|
||||||
|
return if (furtive) {
|
||||||
|
ParcelFileDescriptor.open(file, mode, handler) {
|
||||||
|
free()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ParcelFileDescriptor.open(file, mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun free() {
|
||||||
|
Wiper.wipe(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExportedMemFile private constructor(path: String, private val file: MemFile) :
|
||||||
|
ExportedFile(path) {
|
||||||
|
companion object {
|
||||||
|
fun create(path: String, size: Long): ExportedMemFile? {
|
||||||
|
val uuid = UUID.randomUUID().toString()
|
||||||
|
MemFile.create(uuid, size)?.let {
|
||||||
|
return ExportedMemFile(path, it)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun open(mode: Int, furtive: Boolean): ParcelFileDescriptor {
|
||||||
|
val fd = if (furtive) {
|
||||||
|
file.toParcelFileDescriptor()
|
||||||
|
} else {
|
||||||
|
file.dup()
|
||||||
|
}
|
||||||
|
if (mode and ParcelFileDescriptor.MODE_TRUNCATE != 0) {
|
||||||
|
Os.ftruncate(fd.fileDescriptor, 0)
|
||||||
|
} else {
|
||||||
|
FileInputStream(fd.fileDescriptor).apply {
|
||||||
|
channel.position(0)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fd
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun free() = file.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class ExportedFile(val path: String) {
|
||||||
|
var isValid = true
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun invalidate() {
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param furtive If set to true, the file will be deleted when closed
|
||||||
|
*/
|
||||||
|
abstract fun open(mode: Int, furtive: Boolean): ParcelFileDescriptor
|
||||||
|
abstract fun free()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createFile(
|
||||||
|
path: String,
|
||||||
|
size: Long,
|
||||||
|
): ExportedFile? {
|
||||||
|
return if (size > memoryInfo.availMem * 0.8) {
|
||||||
|
ExportedDiskFile.create(
|
||||||
|
path,
|
||||||
|
tmpFilesDir,
|
||||||
|
handler,
|
||||||
|
)
|
||||||
|
} else if (isMemFileSupported) {
|
||||||
|
ExportedMemFile.create(path, size) as ExportedFile
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exportFile(
|
||||||
|
exportedFile: ExportedFile,
|
||||||
|
encryptedVolume: EncryptedVolume,
|
||||||
|
): Boolean {
|
||||||
|
val fd = exportedFile.open(ParcelFileDescriptor.MODE_WRITE_ONLY, false).fileDescriptor
|
||||||
|
return encryptedVolume.exportFile(exportedFile.path, FileOutputStream(fd))
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Error {
|
||||||
|
SUCCESS,
|
||||||
|
INVALID_STATE,
|
||||||
|
WRITE_ACCESS_DENIED,
|
||||||
|
UNSUPPORTED_APPEND,
|
||||||
|
UNSUPPORTED_RW,
|
||||||
|
;
|
||||||
|
|
||||||
|
fun log() {
|
||||||
|
Log.e(
|
||||||
|
TAG, when (this) {
|
||||||
|
SUCCESS -> "No error"
|
||||||
|
INVALID_STATE -> "Read after write is not supported"
|
||||||
|
WRITE_ACCESS_DENIED -> "Write access unauthorized"
|
||||||
|
UNSUPPORTED_APPEND -> "Appending is not supported"
|
||||||
|
UNSUPPORTED_RW -> "Read-write access requires Android 11 or later"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param furtive If set to true, the file will be deleted when closed
|
||||||
|
*/
|
||||||
|
fun openFile(
|
||||||
|
file: ExportedFile,
|
||||||
|
mode: String,
|
||||||
|
encryptedVolume: EncryptedVolume,
|
||||||
|
volumeScope: CoroutineScope,
|
||||||
|
furtive: Boolean,
|
||||||
|
allowWrites: Boolean,
|
||||||
|
): Pair<ParcelFileDescriptor?, Error> {
|
||||||
|
val mode = ParcelFileDescriptor.parseMode(mode)
|
||||||
|
return if (mode and ParcelFileDescriptor.MODE_READ_ONLY != 0) {
|
||||||
|
if (!file.isValid) return Pair(null, Error.INVALID_STATE)
|
||||||
|
Pair(file.open(mode, furtive), Error.SUCCESS)
|
||||||
|
} else {
|
||||||
|
if (!allowWrites) {
|
||||||
|
return Pair(null, Error.WRITE_ACCESS_DENIED)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun import(input: InputStream): Boolean {
|
||||||
|
return if (encryptedVolume.importFile(input, file.path)) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Failed to import file")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode and ParcelFileDescriptor.MODE_WRITE_ONLY != 0) {
|
||||||
|
if (mode and ParcelFileDescriptor.MODE_APPEND != 0) {
|
||||||
|
return Pair(null, Error.UNSUPPORTED_APPEND)
|
||||||
|
}
|
||||||
|
if (mode and ParcelFileDescriptor.MODE_TRUNCATE == 0) {
|
||||||
|
Log.w(TAG, "Truncating file despite not being requested")
|
||||||
|
}
|
||||||
|
val pipe = ParcelFileDescriptor.createReliablePipe()
|
||||||
|
val input = FileInputStream(pipe[0].fileDescriptor)
|
||||||
|
volumeScope.launch {
|
||||||
|
if (import(input)) {
|
||||||
|
file.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Pair(pipe[1], Error.SUCCESS)
|
||||||
|
} else { // read-write
|
||||||
|
if (!file.isValid) return Pair(null, Error.INVALID_STATE)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
val fd = file.open(mode, false)
|
||||||
|
Pair(ParcelFileDescriptor.wrap(fd, handler) { e ->
|
||||||
|
if (e == null) {
|
||||||
|
import(FileInputStream(fd.fileDescriptor))
|
||||||
|
if (furtive) {
|
||||||
|
file.free()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, Error.SUCCESS)
|
||||||
|
} else {
|
||||||
|
Pair(null, Error.UNSUPPORTED_RW)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,66 +4,48 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import sushi.hardcore.droidfs.content_providers.DiskFileProvider
|
import androidx.preference.PreferenceManager
|
||||||
import sushi.hardcore.droidfs.content_providers.MemoryFileProvider
|
|
||||||
import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
|
import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
|
||||||
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
|
||||||
import sushi.hardcore.droidfs.util.Version
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class FileShare(private val encryptedVolume: EncryptedVolume, private val context: Context) {
|
class FileShare(context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val content_type_all = "*/*"
|
private const val CONTENT_TYPE_ANY = "*/*"
|
||||||
fun getContentType(filename: String, previousContentType: String?): String {
|
private fun getContentType(filename: String, previousContentType: String?): String {
|
||||||
if (content_type_all != previousContentType) {
|
if (CONTENT_TYPE_ANY != previousContentType) {
|
||||||
var contentType = MimeTypeMap.getSingleton()
|
var contentType = MimeTypeMap.getSingleton()
|
||||||
.getMimeTypeFromExtension(File(filename).extension)
|
.getMimeTypeFromExtension(File(filename).extension)
|
||||||
if (contentType == null) {
|
if (contentType == null) {
|
||||||
contentType = content_type_all
|
contentType = CONTENT_TYPE_ANY
|
||||||
}
|
}
|
||||||
if (previousContentType == null) {
|
if (previousContentType == null) {
|
||||||
return contentType
|
return contentType
|
||||||
} else if (previousContentType != contentType) {
|
} else if (previousContentType != contentType) {
|
||||||
return content_type_all
|
return CONTENT_TYPE_ANY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return previousContentType
|
return previousContentType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val fileProvider: TemporaryFileProvider<*>
|
private val usfSafWrite = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("usf_saf_write", false)
|
||||||
|
|
||||||
init {
|
private fun exportFile(exportedFile: EncryptedFileProvider.ExportedFile, size: Long, volumeId: Int, previousContentType: String? = null): Pair<Uri, String>? {
|
||||||
var provider: MemoryFileProvider? = null
|
val uri = TemporaryFileProvider.instance.exportFile(exportedFile, size, volumeId) ?: return null
|
||||||
System.getProperty("os.version")?.let {
|
return Pair(uri, getContentType(File(exportedFile.path).name, previousContentType))
|
||||||
if (Version(it) >= Version("3.17")) {
|
|
||||||
provider = MemoryFileProvider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fileProvider = provider ?: DiskFileProvider()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportFile(path: String, size: Long, previousContentType: String? = null): Pair<Uri?, String?> {
|
fun share(files: List<Pair<String, Long>>, volumeId: Int): Pair<Intent?, Int?> {
|
||||||
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
|
var contentType: String? = null
|
||||||
val uris = ArrayList<Uri>(files.size)
|
val uris = ArrayList<Uri>(files.size)
|
||||||
for ((path, size) in files) {
|
for ((path, size) in files) {
|
||||||
val result = exportFile(path, size, contentType)
|
val exportedFile = TemporaryFileProvider.instance.encryptedFileProvider.createFile(path, size)
|
||||||
contentType = if (result.first == null) {
|
?: return Pair(null, R.string.export_failed_create)
|
||||||
return Pair(null, path)
|
val result = exportFile(exportedFile, size, volumeId, contentType)
|
||||||
|
contentType = if (result == null) {
|
||||||
|
return Pair(null, R.string.export_failed_export)
|
||||||
} else {
|
} else {
|
||||||
uris.add(result.first!!)
|
uris.add(result.first)
|
||||||
result.second
|
result.second
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,15 +61,18 @@ class FileShare(private val encryptedVolume: EncryptedVolume, private val contex
|
|||||||
}, null)
|
}, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun openWith(path: String, size: Long): Intent? {
|
fun openWith(exportedFile: EncryptedFileProvider.ExportedFile, size: Long, volumeId: Int): Pair<Intent?, Int?> {
|
||||||
val result = exportFile(path, size)
|
val result = exportFile(exportedFile, size, volumeId)
|
||||||
return if (result.first != null) {
|
return if (result == null) {
|
||||||
Intent(Intent.ACTION_VIEW).apply {
|
Pair(null, R.string.export_failed_export)
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
setDataAndType(result.first, result.second)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
null
|
Pair(Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
if (usfSafWrite) {
|
||||||
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
setDataAndType(result.first, result.second)
|
||||||
|
}, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -20,6 +20,7 @@ import kotlinx.coroutines.launch
|
|||||||
import sushi.hardcore.droidfs.Constants.DEFAULT_VOLUME_KEY
|
import sushi.hardcore.droidfs.Constants.DEFAULT_VOLUME_KEY
|
||||||
import sushi.hardcore.droidfs.adapters.VolumeAdapter
|
import sushi.hardcore.droidfs.adapters.VolumeAdapter
|
||||||
import sushi.hardcore.droidfs.add_volume.AddVolumeActivity
|
import sushi.hardcore.droidfs.add_volume.AddVolumeActivity
|
||||||
|
import sushi.hardcore.droidfs.content_providers.VolumeProvider
|
||||||
import sushi.hardcore.droidfs.databinding.ActivityMainBinding
|
import sushi.hardcore.droidfs.databinding.ActivityMainBinding
|
||||||
import sushi.hardcore.droidfs.databinding.DialogDeleteVolumeBinding
|
import sushi.hardcore.droidfs.databinding.DialogDeleteVolumeBinding
|
||||||
import sushi.hardcore.droidfs.explorers.ExplorerRouter
|
import sushi.hardcore.droidfs.explorers.ExplorerRouter
|
||||||
@ -195,7 +196,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
|||||||
|
|
||||||
private fun removeVolume(volume: VolumeData) {
|
private fun removeVolume(volume: VolumeData) {
|
||||||
volumeManager.getVolumeId(volume)?.let { volumeManager.closeVolume(it) }
|
volumeManager.getVolumeId(volume)?.let { volumeManager.closeVolume(it) }
|
||||||
volumeDatabase.removeVolume(volume.name)
|
volumeDatabase.removeVolume(volume)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeVolumes(volumes: List<VolumeData>, i: Int = 0, doDeleteVolumeContent: Boolean? = null) {
|
private fun removeVolumes(volumes: List<VolumeData>, i: Int = 0, doDeleteVolumeContent: Boolean? = null) {
|
||||||
@ -324,7 +325,14 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
|||||||
DocumentFile.fromFile(File(volume.name)),
|
DocumentFile.fromFile(File(volume.name)),
|
||||||
DocumentFile.fromFile(hiddenVolumeFile.parentFile!!),
|
DocumentFile.fromFile(hiddenVolumeFile.parentFile!!),
|
||||||
) {
|
) {
|
||||||
VolumeData(volume.shortName, true, volume.type, volume.encryptedHash, volume.iv)
|
VolumeData(
|
||||||
|
VolumeData.newUuid(),
|
||||||
|
volume.shortName,
|
||||||
|
true,
|
||||||
|
volume.type,
|
||||||
|
volume.encryptedHash,
|
||||||
|
volume.iv
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -398,6 +406,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
|||||||
val path = PathUtils.getFullPathFromTreeUri(dstRootDirectory.uri, this)
|
val path = PathUtils.getFullPathFromTreeUri(dstRootDirectory.uri, this)
|
||||||
if (path == null) null
|
if (path == null) null
|
||||||
else VolumeData(
|
else VolumeData(
|
||||||
|
VolumeData.newUuid(),
|
||||||
PathUtils.pathJoin(path, name),
|
PathUtils.pathJoin(path, name),
|
||||||
false,
|
false,
|
||||||
volume.type,
|
volume.type,
|
||||||
@ -466,7 +475,8 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
|||||||
DocumentFile.fromFile(srcPath).renameTo(newName)
|
DocumentFile.fromFile(srcPath).renameTo(newName)
|
||||||
}
|
}
|
||||||
if (success) {
|
if (success) {
|
||||||
volumeDatabase.renameVolume(volume.name, newDBName)
|
volumeDatabase.renameVolume(volume, newDBName)
|
||||||
|
VolumeProvider.notifyRootsChanged(this)
|
||||||
unselect(position)
|
unselect(position)
|
||||||
if (volume.name == volumeOpener.defaultVolumeName) {
|
if (volume.name == volumeOpener.defaultVolumeName) {
|
||||||
with (sharedPrefs.edit()) {
|
with (sharedPrefs.edit()) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package sushi.hardcore.droidfs
|
package sushi.hardcore.droidfs
|
||||||
|
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.system.Os
|
||||||
|
|
||||||
class MemFile private constructor(private val fd: Int) {
|
class MemFile private constructor(private val fd: Int) {
|
||||||
companion object {
|
companion object {
|
||||||
@ -15,8 +16,7 @@ class MemFile private constructor(private val fd: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private external fun close(fd: Int)
|
fun dup(): ParcelFileDescriptor = ParcelFileDescriptor.fromFd(fd)
|
||||||
|
fun toParcelFileDescriptor(): ParcelFileDescriptor = ParcelFileDescriptor.adoptFd(fd)
|
||||||
fun getParcelFileDescriptor(): ParcelFileDescriptor = ParcelFileDescriptor.fromFd(fd)
|
fun close() = Os.close(ParcelFileDescriptor.adoptFd(fd).fileDescriptor)
|
||||||
fun close() = close(fd)
|
|
||||||
}
|
}
|
@ -7,8 +7,16 @@ import android.os.Bundle
|
|||||||
import android.text.InputType
|
import android.text.InputType
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.preference.*
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.Preference
|
||||||
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.preference.SwitchPreference
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
|
||||||
|
import sushi.hardcore.droidfs.content_providers.VolumeProvider
|
||||||
import sushi.hardcore.droidfs.databinding.ActivitySettingsBinding
|
import sushi.hardcore.droidfs.databinding.ActivitySettingsBinding
|
||||||
|
import sushi.hardcore.droidfs.util.Compat
|
||||||
import sushi.hardcore.droidfs.util.PathUtils
|
import sushi.hardcore.droidfs.util.PathUtils
|
||||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||||
import sushi.hardcore.droidfs.widgets.EditTextDialog
|
import sushi.hardcore.droidfs.widgets.EditTextDialog
|
||||||
@ -150,6 +158,47 @@ class SettingsActivity : BaseActivity() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val switchKeepOpen = findPreference<SwitchPreference>("usf_keep_open")!!
|
||||||
|
val switchExternalOpen = findPreference<SwitchPreference>("usf_open")!!
|
||||||
|
val switchExpose = findPreference<SwitchPreference>("usf_expose")!!
|
||||||
|
val switchSafWrite = findPreference<SwitchPreference>("usf_saf_write")!!
|
||||||
|
|
||||||
|
fun updateView(usfOpen: Boolean? = null, usfKeepOpen: Boolean? = null, usfExpose: Boolean? = null) {
|
||||||
|
val usfKeepOpen = usfKeepOpen ?: switchKeepOpen.isChecked
|
||||||
|
switchExpose.isEnabled = usfKeepOpen
|
||||||
|
switchSafWrite.isEnabled = usfOpen ?: switchExternalOpen.isChecked || (usfKeepOpen && usfExpose ?: switchExpose.isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateView()
|
||||||
|
switchKeepOpen.setOnPreferenceChangeListener { _, checked ->
|
||||||
|
updateView(usfKeepOpen = checked as Boolean)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
switchExternalOpen.setOnPreferenceChangeListener { _, checked ->
|
||||||
|
updateView(usfOpen = checked as Boolean)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
switchExpose.setOnPreferenceChangeListener { _, checked ->
|
||||||
|
if (checked as Boolean) {
|
||||||
|
if (!Compat.isMemFileSupported()) {
|
||||||
|
CustomAlertDialogBuilder(requireContext(), (requireActivity() as BaseActivity).theme)
|
||||||
|
.setTitle(R.string.error)
|
||||||
|
.setMessage("Your current kernel does not support memfd_create(). This feature requires a minimum kernel version of ${Compat.MEMFD_CREATE_MINIMUM_KERNEL_VERSION}.")
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
return@setOnPreferenceChangeListener false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VolumeProvider.usfExpose = checked
|
||||||
|
updateView(usfExpose = checked)
|
||||||
|
VolumeProvider.notifyRootsChanged(requireContext())
|
||||||
|
true
|
||||||
|
}
|
||||||
|
switchSafWrite.setOnPreferenceChangeListener { _, checked ->
|
||||||
|
VolumeProvider.usfSafWrite = checked as Boolean
|
||||||
|
TemporaryFileProvider.usfSafWrite = checked
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,10 +8,19 @@ import sushi.hardcore.droidfs.filesystems.GocryptfsVolume
|
|||||||
import sushi.hardcore.droidfs.util.PathUtils
|
import sushi.hardcore.droidfs.util.PathUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class VolumeData(val name: String, val isHidden: Boolean = false, val type: Byte, var encryptedHash: ByteArray? = null, var iv: ByteArray? = null): Parcelable {
|
class VolumeData(
|
||||||
|
val uuid: String,
|
||||||
|
val name: String,
|
||||||
|
val isHidden: Boolean = false,
|
||||||
|
val type: Byte,
|
||||||
|
var encryptedHash: ByteArray? = null,
|
||||||
|
var iv: ByteArray? = null
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
constructor(parcel: Parcel) : this(
|
constructor(parcel: Parcel) : this(
|
||||||
|
parcel.readString()!!,
|
||||||
parcel.readString()!!,
|
parcel.readString()!!,
|
||||||
parcel.readByte() != 0.toByte(),
|
parcel.readByte() != 0.toByte(),
|
||||||
parcel.readByte(),
|
parcel.readByte(),
|
||||||
@ -23,12 +32,7 @@ class VolumeData(val name: String, val isHidden: Boolean = false, val type: Byte
|
|||||||
File(name).name
|
File(name).name
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFullPath(filesDir: String): String {
|
fun getFullPath(filesDir: String) = getFullPath(name, isHidden, filesDir)
|
||||||
return if (isHidden)
|
|
||||||
getHiddenVolumeFullPath(filesDir, name)
|
|
||||||
else
|
|
||||||
name
|
|
||||||
}
|
|
||||||
|
|
||||||
fun canRead(filesDir: String): Boolean {
|
fun canRead(filesDir: String): Boolean {
|
||||||
val volumePath = getFullPath(filesDir)
|
val volumePath = getFullPath(filesDir)
|
||||||
@ -62,6 +66,7 @@ class VolumeData(val name: String, val isHidden: Boolean = false, val type: Byte
|
|||||||
|
|
||||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||||
with (dest) {
|
with (dest) {
|
||||||
|
writeString(uuid)
|
||||||
writeString(name)
|
writeString(name)
|
||||||
writeByte(if (isHidden) 1 else 0)
|
writeByte(if (isHidden) 1 else 0)
|
||||||
writeByte(type)
|
writeByte(type)
|
||||||
@ -74,12 +79,10 @@ class VolumeData(val name: String, val isHidden: Boolean = false, val type: Byte
|
|||||||
if (other !is VolumeData) {
|
if (other !is VolumeData) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return other.name == name && other.isHidden == isHidden
|
return other.uuid == uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode() = uuid.hashCode()
|
||||||
return name.hashCode()+isHidden.hashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val VOLUMES_DIRECTORY = "volumes"
|
const val VOLUMES_DIRECTORY = "volumes"
|
||||||
@ -90,8 +93,17 @@ class VolumeData(val name: String, val isHidden: Boolean = false, val type: Byte
|
|||||||
override fun newArray(size: Int) = arrayOfNulls<VolumeData>(size)
|
override fun newArray(size: Int) = arrayOfNulls<VolumeData>(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun newUuid(): String = UUID.randomUUID().toString()
|
||||||
|
|
||||||
fun getHiddenVolumeFullPath(filesDir: String, name: String): String {
|
fun getHiddenVolumeFullPath(filesDir: String, name: String): String {
|
||||||
return PathUtils.pathJoin(filesDir, VOLUMES_DIRECTORY, name)
|
return PathUtils.pathJoin(filesDir, VOLUMES_DIRECTORY, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getFullPath(name: String, isHidden: Boolean, filesDir: String): String {
|
||||||
|
return if (isHidden)
|
||||||
|
getHiddenVolumeFullPath(filesDir, name)
|
||||||
|
else
|
||||||
|
name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,47 +9,40 @@ import android.util.Log
|
|||||||
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
||||||
import sushi.hardcore.droidfs.util.PathUtils
|
import sushi.hardcore.droidfs.util.PathUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Constants.VOLUME_DATABASE_NAME, null, 5) {
|
class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Constants.VOLUME_DATABASE_NAME, null, 6) {
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "VolumeDatabase"
|
private const val TAG = "VolumeDatabase"
|
||||||
const val TABLE_NAME = "Volumes"
|
private const val TABLE_NAME = "Volumes"
|
||||||
const val COLUMN_NAME = "name"
|
private const val COLUMN_UUID = "uuid"
|
||||||
const val COLUMN_HIDDEN = "hidden"
|
private const val COLUMN_NAME = "name"
|
||||||
const val COLUMN_TYPE = "type"
|
private const val COLUMN_HIDDEN = "hidden"
|
||||||
const val COLUMN_HASH = "hash"
|
private const val COLUMN_TYPE = "type"
|
||||||
const val COLUMN_IV = "iv"
|
private const val COLUMN_HASH = "hash"
|
||||||
|
private const val COLUMN_IV = "iv"
|
||||||
private fun contentValuesFromVolume(volume: VolumeData): ContentValues {
|
|
||||||
val contentValues = ContentValues()
|
|
||||||
contentValues.put(COLUMN_NAME, volume.name)
|
|
||||||
contentValues.put(COLUMN_HIDDEN, volume.isHidden)
|
|
||||||
contentValues.put(COLUMN_TYPE, byteArrayOf(volume.type))
|
|
||||||
contentValues.put(COLUMN_HASH, volume.encryptedHash)
|
|
||||||
contentValues.put(COLUMN_IV, volume.iv)
|
|
||||||
return contentValues
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
override fun onCreate(db: SQLiteDatabase) {
|
|
||||||
|
private fun createTable(db: SQLiteDatabase) =
|
||||||
db.execSQL(
|
db.execSQL(
|
||||||
"CREATE TABLE IF NOT EXISTS $TABLE_NAME (" +
|
"CREATE TABLE IF NOT EXISTS $TABLE_NAME (" +
|
||||||
"$COLUMN_NAME TEXT PRIMARY KEY," +
|
"$COLUMN_UUID TEXT PRIMARY KEY," +
|
||||||
"$COLUMN_HIDDEN SHORT," +
|
"$COLUMN_NAME TEXT," +
|
||||||
"$COLUMN_TYPE BLOB," +
|
"$COLUMN_HIDDEN SHORT," +
|
||||||
"$COLUMN_HASH BLOB," +
|
"$COLUMN_TYPE BLOB," +
|
||||||
"$COLUMN_IV BLOB" +
|
"$COLUMN_HASH BLOB," +
|
||||||
");"
|
"$COLUMN_IV BLOB" +
|
||||||
|
");"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override fun onCreate(db: SQLiteDatabase) {
|
||||||
|
createTable(db)
|
||||||
File(context.filesDir, VolumeData.VOLUMES_DIRECTORY).mkdir()
|
File(context.filesDir, VolumeData.VOLUMES_DIRECTORY).mkdir()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getNewVolumePath(volumeName: String): File {
|
private fun getNewVolumePath(volumeName: String): File {
|
||||||
return File(
|
return File(
|
||||||
VolumeData(
|
VolumeData.getFullPath(volumeName, true, context.filesDir.path)
|
||||||
volumeName,
|
|
||||||
true,
|
|
||||||
EncryptedVolume.GOCRYPTFS_VOLUME_TYPE
|
|
||||||
).getFullPath(context.filesDir.path)
|
|
||||||
).canonicalFile
|
).canonicalFile
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,10 +94,29 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 6) {
|
||||||
|
val volumeCount = db.rawQuery("SELECT COUNT(*) FROM $TABLE_NAME", null).let { cursor ->
|
||||||
|
cursor.moveToNext()
|
||||||
|
cursor.getInt(0).also {
|
||||||
|
cursor.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.execSQL("ALTER TABLE $TABLE_NAME RENAME TO OLD;")
|
||||||
|
createTable(db)
|
||||||
|
val uuids = (0 until volumeCount).joinToString(", ") { "('${VolumeData.newUuid()}')" }
|
||||||
|
val baseColumns = "$COLUMN_NAME, $COLUMN_HIDDEN, $COLUMN_TYPE, $COLUMN_HASH, $COLUMN_IV"
|
||||||
|
// add uuids to old data
|
||||||
|
db.execSQL("INSERT INTO $TABLE_NAME " +
|
||||||
|
"SELECT uuid, $baseColumns FROM " +
|
||||||
|
"(SELECT $baseColumns, ROW_NUMBER() OVER () i FROM OLD) NATURAL JOIN " +
|
||||||
|
"(SELECT column1 uuid, ROW_NUMBER() OVER () i FROM (VALUES $uuids));")
|
||||||
|
db.execSQL("DROP TABLE OLD;")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extractVolumeData(cursor: Cursor): VolumeData {
|
private fun extractVolumeData(cursor: Cursor): VolumeData {
|
||||||
return VolumeData(
|
return VolumeData(
|
||||||
|
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_UUID)),
|
||||||
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)),
|
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)),
|
||||||
cursor.getShort(cursor.getColumnIndexOrThrow(COLUMN_HIDDEN)) == 1.toShort(),
|
cursor.getShort(cursor.getColumnIndexOrThrow(COLUMN_HIDDEN)) == 1.toShort(),
|
||||||
cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_TYPE))[0],
|
cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_TYPE))[0],
|
||||||
@ -142,7 +154,14 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
|
|||||||
|
|
||||||
fun saveVolume(volume: VolumeData): Boolean {
|
fun saveVolume(volume: VolumeData): Boolean {
|
||||||
if (!isVolumeSaved(volume.name, volume.isHidden)) {
|
if (!isVolumeSaved(volume.name, volume.isHidden)) {
|
||||||
return (writableDatabase.insert(TABLE_NAME, null, contentValuesFromVolume(volume)) >= 0.toLong())
|
return (writableDatabase.insert(TABLE_NAME, null, ContentValues().apply {
|
||||||
|
put(COLUMN_UUID, volume.uuid)
|
||||||
|
put(COLUMN_NAME, volume.name)
|
||||||
|
put(COLUMN_HIDDEN, volume.isHidden)
|
||||||
|
put(COLUMN_TYPE, byteArrayOf(volume.type))
|
||||||
|
put(COLUMN_HASH, volume.encryptedHash)
|
||||||
|
put(COLUMN_IV, volume.iv)
|
||||||
|
}) == 1.toLong())
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -157,8 +176,8 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
|
|||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isHashSaved(volumeName: String): Boolean {
|
fun isHashSaved(volume: VolumeData): Boolean {
|
||||||
val cursor = readableDatabase.query(TABLE_NAME, arrayOf(COLUMN_NAME, COLUMN_HASH), "$COLUMN_NAME=?", arrayOf(volumeName), null, null, null)
|
val cursor = readableDatabase.rawQuery("SELECT $COLUMN_HASH FROM $TABLE_NAME WHERE $COLUMN_UUID=?", arrayOf(volume.uuid))
|
||||||
var isHashSaved = false
|
var isHashSaved = false
|
||||||
if (cursor.moveToNext()) {
|
if (cursor.moveToNext()) {
|
||||||
if (cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_HASH)) != null) {
|
if (cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_HASH)) != null) {
|
||||||
@ -170,32 +189,33 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addHash(volume: VolumeData): Boolean {
|
fun addHash(volume: VolumeData): Boolean {
|
||||||
return writableDatabase.update(TABLE_NAME, contentValuesFromVolume(volume), "$COLUMN_NAME=?", arrayOf(volume.name)) > 0
|
return writableDatabase.update(TABLE_NAME, ContentValues().apply {
|
||||||
|
put(COLUMN_HASH, volume.encryptedHash)
|
||||||
|
put(COLUMN_IV, volume.iv)
|
||||||
|
}, "$COLUMN_UUID=?", arrayOf(volume.uuid)) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeHash(volume: VolumeData): Boolean {
|
fun removeHash(volume: VolumeData): Boolean {
|
||||||
return writableDatabase.update(
|
return writableDatabase.update(
|
||||||
TABLE_NAME, contentValuesFromVolume(
|
TABLE_NAME,
|
||||||
VolumeData(
|
|
||||||
volume.name,
|
|
||||||
volume.isHidden,
|
|
||||||
volume.type,
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
)
|
|
||||||
), "$COLUMN_NAME=?", arrayOf(volume.name)) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fun renameVolume(oldName: String, newName: String): Boolean {
|
|
||||||
return writableDatabase.update(TABLE_NAME,
|
|
||||||
ContentValues().apply {
|
ContentValues().apply {
|
||||||
put(COLUMN_NAME, newName)
|
put(COLUMN_HASH, null as ByteArray?)
|
||||||
},
|
put(COLUMN_IV, null as ByteArray?)
|
||||||
"$COLUMN_NAME=?",arrayOf(oldName)
|
}, "$COLUMN_UUID=?", arrayOf(volume.uuid)
|
||||||
) > 0
|
) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeVolume(volumeName: String): Boolean {
|
fun renameVolume(volume: VolumeData, newName: String): Boolean {
|
||||||
return writableDatabase.delete(TABLE_NAME, "$COLUMN_NAME=?", arrayOf(volumeName)) > 0
|
return writableDatabase.update(
|
||||||
|
TABLE_NAME,
|
||||||
|
ContentValues().apply {
|
||||||
|
put(COLUMN_NAME, newName)
|
||||||
|
},
|
||||||
|
"$COLUMN_UUID=?", arrayOf(volume.uuid)
|
||||||
|
) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeVolume(volume: VolumeData): Boolean {
|
||||||
|
return writableDatabase.delete(TABLE_NAME, "$COLUMN_UUID=?", arrayOf(volume.uuid)) > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,12 +1,14 @@
|
|||||||
package sushi.hardcore.droidfs
|
package sushi.hardcore.droidfs
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
|
import sushi.hardcore.droidfs.content_providers.VolumeProvider
|
||||||
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
||||||
|
|
||||||
class VolumeManager {
|
class VolumeManager(private val context: Context) {
|
||||||
private var id = 0
|
private var id = 0
|
||||||
private val volumes = HashMap<Int, EncryptedVolume>()
|
private val volumes = HashMap<Int, EncryptedVolume>()
|
||||||
private val volumesData = HashMap<VolumeData, Int>()
|
private val volumesData = HashMap<VolumeData, Int>()
|
||||||
@ -15,6 +17,7 @@ class VolumeManager {
|
|||||||
fun insert(volume: EncryptedVolume, data: VolumeData): Int {
|
fun insert(volume: EncryptedVolume, data: VolumeData): Int {
|
||||||
volumes[id] = volume
|
volumes[id] = volume
|
||||||
volumesData[data] = id
|
volumesData[data] = id
|
||||||
|
VolumeProvider.notifyRootsChanged(context)
|
||||||
return id++
|
return id++
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,6 +33,10 @@ class VolumeManager {
|
|||||||
return volumes[id]
|
return volumes[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun listVolumes(): List<Pair<Int, VolumeData>> {
|
||||||
|
return volumesData.map { (data, id) -> Pair(id, data) }
|
||||||
|
}
|
||||||
|
|
||||||
fun getCoroutineScope(volumeId: Int): CoroutineScope {
|
fun getCoroutineScope(volumeId: Int): CoroutineScope {
|
||||||
return scopes[volumeId] ?: CoroutineScope(SupervisorJob() + Dispatchers.IO).also { scopes[volumeId] = it }
|
return scopes[volumeId] ?: CoroutineScope(SupervisorJob() + Dispatchers.IO).also { scopes[volumeId] = it }
|
||||||
}
|
}
|
||||||
@ -41,6 +48,7 @@ class VolumeManager {
|
|||||||
volumesData.filter { it.value == id }.forEach {
|
volumesData.filter { it.value == id }.forEach {
|
||||||
volumesData.remove(it.key)
|
volumesData.remove(it.key)
|
||||||
}
|
}
|
||||||
|
VolumeProvider.notifyRootsChanged(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,5 +59,6 @@ class VolumeManager {
|
|||||||
}
|
}
|
||||||
volumes.clear()
|
volumes.clear()
|
||||||
volumesData.clear()
|
volumesData.clear()
|
||||||
|
VolumeProvider.notifyRootsChanged(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -6,8 +6,7 @@ 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.MemoryFileProvider
|
import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
|
||||||
import sushi.hardcore.droidfs.content_providers.DiskFileProvider
|
|
||||||
|
|
||||||
class VolumeManagerApp : Application(), DefaultLifecycleObserver {
|
class VolumeManagerApp : Application(), DefaultLifecycleObserver {
|
||||||
companion object {
|
companion object {
|
||||||
@ -21,8 +20,9 @@ class VolumeManagerApp : Application(), DefaultLifecycleObserver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var usfKeepOpen = false
|
private var usfKeepOpen = false
|
||||||
|
var isExporting = false
|
||||||
var isStartingExternalApp = false
|
var isStartingExternalApp = false
|
||||||
val volumeManager = VolumeManager()
|
val volumeManager = VolumeManager(this)
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super<Application>.onCreate()
|
super<Application>.onCreate()
|
||||||
@ -46,8 +46,9 @@ class VolumeManagerApp : Application(), DefaultLifecycleObserver {
|
|||||||
if (!usfKeepOpen) {
|
if (!usfKeepOpen) {
|
||||||
volumeManager.closeAll()
|
volumeManager.closeAll()
|
||||||
}
|
}
|
||||||
DiskFileProvider.wipe()
|
if (!usfKeepOpen || !isExporting) {
|
||||||
MemoryFileProvider.wipe()
|
TemporaryFileProvider.instance.wipe()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -211,11 +211,11 @@ class CreateVolumeFragment: Fragment() {
|
|||||||
.show()
|
.show()
|
||||||
} else {
|
} else {
|
||||||
val volumeName = if (isHiddenVolume) File(volumePath).name else volumePath
|
val volumeName = if (isHiddenVolume) File(volumePath).name else volumePath
|
||||||
val volume = VolumeData(volumeName, isHiddenVolume, result)
|
val volume = VolumeData(VolumeData.newUuid(), volumeName, isHiddenVolume, result)
|
||||||
var isVolumeSaved = false
|
var isVolumeSaved = false
|
||||||
volumeDatabase.apply {
|
volumeDatabase.apply {
|
||||||
if (isVolumeSaved(volumeName, isHiddenVolume)) // cleaning old saved path
|
if (isVolumeSaved(volumeName, isHiddenVolume)) // cleaning old saved path
|
||||||
removeVolume(volumeName)
|
removeVolume(volume)
|
||||||
if (rememberVolume) {
|
if (rememberVolume) {
|
||||||
isVolumeSaved = saveVolume(volume)
|
isVolumeSaved = saveVolume(volume)
|
||||||
}
|
}
|
||||||
|
@ -350,7 +350,7 @@ class SelectPathFragment: Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun addVolume(volumeName: String, isHidden: Boolean, volumeType: Byte) {
|
private fun addVolume(volumeName: String, isHidden: Boolean, volumeType: Byte) {
|
||||||
val volumeData = VolumeData(volumeName, isHidden, volumeType)
|
val volumeData = VolumeData(VolumeData.newUuid(), volumeName, isHidden, volumeType)
|
||||||
if (binding.switchRemember.isChecked) {
|
if (binding.switchRemember.isChecked) {
|
||||||
volumeDatabase.saveVolume(volumeData)
|
volumeDatabase.saveVolume(volumeData)
|
||||||
}
|
}
|
||||||
|
@ -1,68 +0,0 @@
|
|||||||
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,48 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,23 +2,87 @@ package sushi.hardcore.droidfs.content_providers
|
|||||||
|
|
||||||
import android.content.ContentProvider
|
import android.content.ContentProvider
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
|
import android.content.Intent
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.database.MatrixCursor
|
import android.database.MatrixCursor
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
|
import android.util.Log
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import sushi.hardcore.droidfs.BuildConfig
|
||||||
|
import sushi.hardcore.droidfs.EncryptedFileProvider
|
||||||
|
import sushi.hardcore.droidfs.VolumeManager
|
||||||
|
import sushi.hardcore.droidfs.VolumeManagerApp
|
||||||
|
import sushi.hardcore.droidfs.util.Wiper
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
abstract class TemporaryFileProvider<T>: ContentProvider() {
|
class TemporaryFileProvider : ContentProvider() {
|
||||||
protected inner class SharedFile(val name: String, val size: Long, val file: T)
|
private inner class ProvidedFile(
|
||||||
|
val file: EncryptedFileProvider.ExportedFile,
|
||||||
|
val size: Long,
|
||||||
|
val volumeId: Int
|
||||||
|
)
|
||||||
|
|
||||||
protected abstract fun getFile(uri: Uri): SharedFile?
|
companion object {
|
||||||
abstract fun newFile(name: String, size: Long): Uri?
|
private const val TAG = "TemporaryFileProvider"
|
||||||
|
private const val AUTHORITY = BuildConfig.APPLICATION_ID + ".temporary_provider"
|
||||||
|
private val BASE_URI: Uri = Uri.parse("content://$AUTHORITY")
|
||||||
|
|
||||||
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
|
lateinit var instance: TemporaryFileProvider
|
||||||
val file = getFile(uri) ?: return null
|
private set
|
||||||
|
var usfSafWrite = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var volumeManager: VolumeManager
|
||||||
|
lateinit var encryptedFileProvider: EncryptedFileProvider
|
||||||
|
private val files = HashMap<Uri, ProvidedFile>()
|
||||||
|
|
||||||
|
override fun onCreate(): Boolean {
|
||||||
|
return context?.let {
|
||||||
|
volumeManager = (it.applicationContext as VolumeManagerApp).volumeManager
|
||||||
|
usfSafWrite =
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(it).getBoolean("usf_saf_write", false)
|
||||||
|
encryptedFileProvider = EncryptedFileProvider(it)
|
||||||
|
instance = this
|
||||||
|
val tmpFilesDir = EncryptedFileProvider.getTmpFilesDir(it)
|
||||||
|
val success = tmpFilesDir.mkdirs()
|
||||||
|
// wipe any additional files not previously deleted
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
tmpFilesDir.listFiles()?.onEach { f -> Wiper.wipe(f) }
|
||||||
|
}
|
||||||
|
success
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exportFile(
|
||||||
|
exportedFile: EncryptedFileProvider.ExportedFile,
|
||||||
|
size: Long,
|
||||||
|
volumeId: Int
|
||||||
|
): Uri? {
|
||||||
|
if (!encryptedFileProvider.exportFile(exportedFile, volumeManager.getVolume(volumeId)!!)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return Uri.withAppendedPath(BASE_URI, UUID.randomUUID().toString()).also {
|
||||||
|
files[it] = ProvidedFile(exportedFile, size, volumeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun query(
|
||||||
|
uri: Uri,
|
||||||
|
projection: Array<String>?,
|
||||||
|
selection: String?,
|
||||||
|
selectionArgs: Array<String>?,
|
||||||
|
sortOrder: String?
|
||||||
|
): Cursor? {
|
||||||
|
val file = files[uri] ?: return null
|
||||||
return MatrixCursor(arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE), 1).apply {
|
return MatrixCursor(arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE), 1).apply {
|
||||||
addRow(arrayOf(file.name, file.size))
|
addRow(arrayOf(File(file.file.path).name, file.size))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,11 +90,58 @@ abstract class TemporaryFileProvider<T>: ContentProvider() {
|
|||||||
throw UnsupportedOperationException("Operation not supported")
|
throw UnsupportedOperationException("Operation not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
|
override fun update(
|
||||||
|
uri: Uri,
|
||||||
|
values: ContentValues?,
|
||||||
|
selection: String?,
|
||||||
|
selectionArgs: Array<String>?
|
||||||
|
): Int {
|
||||||
throw UnsupportedOperationException("Operation not supported")
|
throw UnsupportedOperationException("Operation not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getType(uri: Uri): String = getFile(uri)?.name?.let {
|
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
|
||||||
|
return if (files.remove(uri)?.file?.also { it.free() } == null) 0 else 1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getType(uri: Uri): String = files[uri]?.file?.path?.let {
|
||||||
MimeTypeMap.getSingleton().getMimeTypeFromExtension(File(it).extension)
|
MimeTypeMap.getSingleton().getMimeTypeFromExtension(File(it).extension)
|
||||||
} ?: "application/octet-stream"
|
} ?: "application/octet-stream"
|
||||||
|
|
||||||
|
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||||
|
files[uri]?.let { file ->
|
||||||
|
val encryptedVolume = volumeManager.getVolume(file.volumeId) ?: run {
|
||||||
|
Log.e(TAG, "Volume closed for $uri")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val result = encryptedFileProvider.openFile(
|
||||||
|
file.file,
|
||||||
|
mode,
|
||||||
|
encryptedVolume,
|
||||||
|
volumeManager.getCoroutineScope(file.volumeId),
|
||||||
|
false,
|
||||||
|
usfSafWrite,
|
||||||
|
)
|
||||||
|
when (result.second) {
|
||||||
|
EncryptedFileProvider.Error.SUCCESS -> return result.first!!
|
||||||
|
EncryptedFileProvider.Error.WRITE_ACCESS_DENIED -> Log.e(
|
||||||
|
TAG,
|
||||||
|
"Unauthorized write access requested from $callingPackage to $uri"
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> result.second.log()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// this must not be cancelled
|
||||||
|
fun wipe() = GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
context!!.revokeUriPermission(BASE_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
|
synchronized(this@TemporaryFileProvider) {
|
||||||
|
for (i in files.values) {
|
||||||
|
i.file.free()
|
||||||
|
}
|
||||||
|
files.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,286 @@
|
|||||||
|
package sushi.hardcore.droidfs.content_providers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.database.MatrixCursor
|
||||||
|
import android.os.CancellationSignal
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.provider.DocumentsContract
|
||||||
|
import android.provider.DocumentsProvider
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import sushi.hardcore.droidfs.BuildConfig
|
||||||
|
import sushi.hardcore.droidfs.EncryptedFileProvider
|
||||||
|
import sushi.hardcore.droidfs.R
|
||||||
|
import sushi.hardcore.droidfs.VolumeData
|
||||||
|
import sushi.hardcore.droidfs.VolumeManager
|
||||||
|
import sushi.hardcore.droidfs.VolumeManagerApp
|
||||||
|
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
||||||
|
import sushi.hardcore.droidfs.filesystems.Stat
|
||||||
|
import sushi.hardcore.droidfs.util.PathUtils
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class VolumeProvider: DocumentsProvider() {
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "DocumentsProvider"
|
||||||
|
private const val AUTHORITY = BuildConfig.APPLICATION_ID + ".volume_provider"
|
||||||
|
private val DEFAULT_ROOT_PROJECTION = arrayOf(
|
||||||
|
DocumentsContract.Root.COLUMN_ROOT_ID,
|
||||||
|
DocumentsContract.Root.COLUMN_FLAGS,
|
||||||
|
DocumentsContract.Root.COLUMN_ICON,
|
||||||
|
DocumentsContract.Root.COLUMN_TITLE,
|
||||||
|
DocumentsContract.Root.COLUMN_DOCUMENT_ID,
|
||||||
|
)
|
||||||
|
private val DEFAULT_DOCUMENT_PROJECTION = arrayOf(
|
||||||
|
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||||
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||||
|
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||||
|
DocumentsContract.Document.COLUMN_FLAGS,
|
||||||
|
DocumentsContract.Document.COLUMN_SIZE,
|
||||||
|
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
|
||||||
|
)
|
||||||
|
var usfExpose = false
|
||||||
|
var usfSafWrite = false
|
||||||
|
|
||||||
|
fun notifyRootsChanged(context: Context) {
|
||||||
|
context.contentResolver.notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var volumeManager: VolumeManager
|
||||||
|
private val volumes = HashMap<String, Pair<Int, VolumeData>>()
|
||||||
|
private lateinit var encryptedFileProvider: EncryptedFileProvider
|
||||||
|
|
||||||
|
override fun onCreate(): Boolean {
|
||||||
|
val context = (context ?: return false)
|
||||||
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
usfExpose = sharedPreferences.getBoolean("usf_expose", false)
|
||||||
|
usfSafWrite = sharedPreferences.getBoolean("usf_saf_write", false)
|
||||||
|
volumeManager = (context.applicationContext as VolumeManagerApp).volumeManager
|
||||||
|
encryptedFileProvider = EncryptedFileProvider(context)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun queryRoots(projection: Array<out String>?): Cursor {
|
||||||
|
val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION)
|
||||||
|
if (!usfExpose) return cursor
|
||||||
|
volumes.clear()
|
||||||
|
for (volume in volumeManager.listVolumes()) {
|
||||||
|
var flags = DocumentsContract.Root.FLAG_LOCAL_ONLY or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
|
||||||
|
if (usfSafWrite && volume.second.canWrite(context!!.filesDir.path)) {
|
||||||
|
flags = flags or DocumentsContract.Root.FLAG_SUPPORTS_CREATE
|
||||||
|
}
|
||||||
|
cursor.newRow().apply {
|
||||||
|
add(DocumentsContract.Root.COLUMN_ROOT_ID, volume.second.name)
|
||||||
|
add(DocumentsContract.Root.COLUMN_FLAGS, flags)
|
||||||
|
add(DocumentsContract.Root.COLUMN_ICON, R.drawable.icon_document_provider)
|
||||||
|
add(DocumentsContract.Root.COLUMN_TITLE, volume.second.name)
|
||||||
|
add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, volume.second.uuid)
|
||||||
|
}
|
||||||
|
volumes[volume.second.uuid] = volume
|
||||||
|
}
|
||||||
|
return cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
internal data class DocumentData(
|
||||||
|
val rootId: String,
|
||||||
|
val volumeId: Int,
|
||||||
|
val volumeData: VolumeData,
|
||||||
|
val encryptedVolume: EncryptedVolume,
|
||||||
|
val path: String
|
||||||
|
) {
|
||||||
|
fun child(childPath: String) = DocumentData(rootId, volumeId, volumeData, encryptedVolume, childPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseDocumentId(documentId: String): DocumentData? {
|
||||||
|
val splits = documentId.split("/", limit = 2)
|
||||||
|
if (splits.size > 2) {
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
volumes[splits[0]]?.let {
|
||||||
|
val encryptedVolume = volumeManager.getVolume(it.first) ?: return null
|
||||||
|
val path = "/"+if (splits.size == 2) {
|
||||||
|
splits[1]
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
return DocumentData(splits[0], it.first, it.second, encryptedVolume, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isChildDocument(parentDocumentId: String, documentId: String): Boolean {
|
||||||
|
if (!usfExpose) return false
|
||||||
|
val parent = parseDocumentId(parentDocumentId) ?: return false
|
||||||
|
val child = parseDocumentId(documentId) ?: return false
|
||||||
|
return parent.rootId == child.rootId && PathUtils.isChildOf(child.path, parent.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addDocumentRow(cursor: MatrixCursor, volumeData: VolumeData, documentId: String, name: String, stat: Stat) {
|
||||||
|
val isDirectory = stat.type == Stat.S_IFDIR
|
||||||
|
var flags = 0
|
||||||
|
if (usfSafWrite && volumeData.canWrite(context!!.filesDir.path)) {
|
||||||
|
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_DELETE or DocumentsContract.Document.FLAG_SUPPORTS_RENAME
|
||||||
|
if (isDirectory) {
|
||||||
|
flags = flags or DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE
|
||||||
|
} else if (stat.type == Stat.S_IFREG) {
|
||||||
|
flags = flags or DocumentsContract.Document.FLAG_SUPPORTS_WRITE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val mimeType = if (isDirectory) {
|
||||||
|
DocumentsContract.Document.MIME_TYPE_DIR
|
||||||
|
} else {
|
||||||
|
MimeTypeMap.getSingleton().getMimeTypeFromExtension(File(name).extension)
|
||||||
|
?: "application/octet-stream"
|
||||||
|
}
|
||||||
|
cursor.newRow().apply {
|
||||||
|
add(DocumentsContract.Document.COLUMN_DOCUMENT_ID, documentId)
|
||||||
|
add(DocumentsContract.Document.COLUMN_DISPLAY_NAME, name)
|
||||||
|
add(DocumentsContract.Document.COLUMN_MIME_TYPE, mimeType)
|
||||||
|
add(DocumentsContract.Document.COLUMN_FLAGS, flags)
|
||||||
|
add(DocumentsContract.Document.COLUMN_SIZE, stat.size)
|
||||||
|
add(DocumentsContract.Document.COLUMN_LAST_MODIFIED, stat.mTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun queryDocument(documentId: String, projection: Array<out String>?): Cursor {
|
||||||
|
val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
|
||||||
|
if (!usfExpose) return cursor
|
||||||
|
val document = parseDocumentId(documentId) ?: return cursor
|
||||||
|
document.encryptedVolume.getAttr(document.path)?.let { stat ->
|
||||||
|
val name = if (document.path == "/") {
|
||||||
|
document.volumeData.shortName
|
||||||
|
} else {
|
||||||
|
File(document.path).name
|
||||||
|
}
|
||||||
|
addDocumentRow(cursor, document.volumeData, documentId, name, stat)
|
||||||
|
}
|
||||||
|
return cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun queryChildDocuments(
|
||||||
|
parentDocumentId: String,
|
||||||
|
projection: Array<out String>?,
|
||||||
|
sortOrder: String?
|
||||||
|
): Cursor {
|
||||||
|
val cursor = MatrixCursor(projection ?: DEFAULT_DOCUMENT_PROJECTION)
|
||||||
|
if (!usfExpose) return cursor
|
||||||
|
val document = parseDocumentId(parentDocumentId) ?: return cursor
|
||||||
|
document.encryptedVolume.readDir(document.path)?.let { content ->
|
||||||
|
for (i in content) {
|
||||||
|
if (i.isParentFolder) continue
|
||||||
|
addDocumentRow(cursor, document.volumeData, document.rootId+i.fullPath, i.name, i.stat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
class LazyExportedFile(
|
||||||
|
private val encryptedFileProvider: EncryptedFileProvider,
|
||||||
|
private val encryptedVolume: EncryptedVolume,
|
||||||
|
path: String,
|
||||||
|
) : EncryptedFileProvider.ExportedFile(path) {
|
||||||
|
|
||||||
|
private val exportedFile: EncryptedFileProvider.ExportedFile by lazy {
|
||||||
|
val size = encryptedVolume.getAttr(path)?.size ?: run {
|
||||||
|
Log.e(TAG, "stat() failed")
|
||||||
|
throw RuntimeException("stat() failed")
|
||||||
|
}
|
||||||
|
val exportedFile = encryptedFileProvider.createFile(path, size) ?: run {
|
||||||
|
Log.e(TAG, "Can't create exported file")
|
||||||
|
throw RuntimeException("Can't create exported file")
|
||||||
|
}
|
||||||
|
if (!encryptedFileProvider.exportFile(exportedFile, encryptedVolume)) {
|
||||||
|
Log.e(TAG, "File export failed")
|
||||||
|
throw RuntimeException("File export failed")
|
||||||
|
}
|
||||||
|
exportedFile
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun open(mode: Int, furtive: Boolean) = exportedFile.open(mode, furtive)
|
||||||
|
override fun free() = exportedFile.free()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun openDocument(
|
||||||
|
documentId: String,
|
||||||
|
mode: String,
|
||||||
|
signal: CancellationSignal?
|
||||||
|
): ParcelFileDescriptor? {
|
||||||
|
if (!usfExpose) return null
|
||||||
|
val document = parseDocumentId(documentId) ?: return null
|
||||||
|
|
||||||
|
val lazyExportedFile = LazyExportedFile(encryptedFileProvider, document.encryptedVolume, document.path)
|
||||||
|
|
||||||
|
val result = encryptedFileProvider.openFile(
|
||||||
|
lazyExportedFile,
|
||||||
|
mode,
|
||||||
|
document.encryptedVolume,
|
||||||
|
volumeManager.getCoroutineScope(document.volumeId),
|
||||||
|
true,
|
||||||
|
usfSafWrite,
|
||||||
|
)
|
||||||
|
when (result.second) {
|
||||||
|
EncryptedFileProvider.Error.SUCCESS -> return result.first!!
|
||||||
|
EncryptedFileProvider.Error.WRITE_ACCESS_DENIED -> Log.e(TAG, "Unauthorized write access requested from $callingPackage")
|
||||||
|
else -> result.second.log()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createDocument(
|
||||||
|
parentDocumentId: String,
|
||||||
|
mimeType: String?,
|
||||||
|
displayName: String
|
||||||
|
): String? {
|
||||||
|
if (!usfExpose || !usfSafWrite) return null
|
||||||
|
val document = parseDocumentId(parentDocumentId) ?: return null
|
||||||
|
val newFile = PathUtils.pathJoin(document.path, displayName)
|
||||||
|
val f = document.encryptedVolume.openFileWriteMode(newFile)
|
||||||
|
return if (f == -1L) {
|
||||||
|
Log.e(TAG, "Failed to create file: $document")
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
document.encryptedVolume.closeFile(f)
|
||||||
|
document.rootId+"/"+newFile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deleteDocument(documentId: String) {
|
||||||
|
if (!usfExpose || !usfSafWrite) return
|
||||||
|
|
||||||
|
fun recursiveRemoveDirectory(document: DocumentData) {
|
||||||
|
document.encryptedVolume.readDir(document.path)?.forEach { e ->
|
||||||
|
val childPath = PathUtils.pathJoin(document.path, e.name)
|
||||||
|
if (e.isDirectory) {
|
||||||
|
recursiveRemoveDirectory(document.child(childPath))
|
||||||
|
} else {
|
||||||
|
document.encryptedVolume.deleteFile(childPath)
|
||||||
|
}
|
||||||
|
revokeDocumentPermission(document.rootId+childPath)
|
||||||
|
}
|
||||||
|
document.encryptedVolume.rmdir(document.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
val document = parseDocumentId(documentId) ?: return
|
||||||
|
document.encryptedVolume.getAttr(document.path)?.let { stat ->
|
||||||
|
if (stat.type == Stat.S_IFDIR) {
|
||||||
|
recursiveRemoveDirectory(document)
|
||||||
|
} else {
|
||||||
|
document.encryptedVolume.deleteFile(document.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun renameDocument(documentId: String, displayName: String): String {
|
||||||
|
if (!usfExpose || !usfSafWrite) return documentId
|
||||||
|
val document = parseDocumentId(documentId) ?: return documentId
|
||||||
|
val newPath = PathUtils.pathJoin(PathUtils.getParentPath(document.path), displayName)
|
||||||
|
return if (document.encryptedVolume.rename(document.path, newPath)) {
|
||||||
|
document.rootId+newPath
|
||||||
|
} else {
|
||||||
|
documentId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -29,6 +29,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import sushi.hardcore.droidfs.BaseActivity
|
import sushi.hardcore.droidfs.BaseActivity
|
||||||
import sushi.hardcore.droidfs.Constants
|
import sushi.hardcore.droidfs.Constants
|
||||||
|
import sushi.hardcore.droidfs.EncryptedFileProvider
|
||||||
import sushi.hardcore.droidfs.FileShare
|
import sushi.hardcore.droidfs.FileShare
|
||||||
import sushi.hardcore.droidfs.FileTypes
|
import sushi.hardcore.droidfs.FileTypes
|
||||||
import sushi.hardcore.droidfs.LoadingTask
|
import sushi.hardcore.droidfs.LoadingTask
|
||||||
@ -36,8 +37,7 @@ import sushi.hardcore.droidfs.R
|
|||||||
import sushi.hardcore.droidfs.VolumeManagerApp
|
import sushi.hardcore.droidfs.VolumeManagerApp
|
||||||
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.DiskFileProvider
|
import sushi.hardcore.droidfs.content_providers.TemporaryFileProvider
|
||||||
import sushi.hardcore.droidfs.content_providers.MemoryFileProvider
|
|
||||||
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
|
||||||
@ -84,9 +84,7 @@ 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 {
|
protected val fileShare by lazy { FileShare(this) }
|
||||||
FileShare(encryptedVolume, this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@ -204,22 +202,38 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
|
|||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun onExportFailed(errorResId: Int) {
|
||||||
|
CustomAlertDialogBuilder(this, theme)
|
||||||
|
.setTitle(R.string.error)
|
||||||
|
.setMessage(getString(R.string.tmp_export_failed, getString(errorResId)))
|
||||||
|
.setPositiveButton(R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun openWithExternalApp(path: String, size: Long) {
|
private fun openWithExternalApp(path: String, size: Long) {
|
||||||
app.isStartingExternalApp = true
|
app.isExporting = true
|
||||||
object : LoadingTask<Intent?>(this, theme, R.string.loading_msg_export) {
|
val exportedFile = TemporaryFileProvider.instance.encryptedFileProvider.createFile(path, size)
|
||||||
override suspend fun doTask(): Intent? {
|
if (exportedFile == null) {
|
||||||
return fileShare.openWith(path, size)
|
onExportFailed(R.string.export_failed_create)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val msg = when (exportedFile) {
|
||||||
|
is EncryptedFileProvider.ExportedMemFile -> R.string.export_mem
|
||||||
|
is EncryptedFileProvider.ExportedDiskFile -> R.string.export_disk
|
||||||
|
else -> R.string.loading_msg_export
|
||||||
|
}
|
||||||
|
object : LoadingTask<Pair<Intent?, Int?>>(this, theme, msg) {
|
||||||
|
override suspend fun doTask(): Pair<Intent?, Int?> {
|
||||||
|
return fileShare.openWith(exportedFile, size, volumeId)
|
||||||
}
|
}
|
||||||
}.startTask(lifecycleScope) { openIntent ->
|
}.startTask(lifecycleScope) { (intent, error) ->
|
||||||
if (openIntent == null) {
|
if (intent == null) {
|
||||||
CustomAlertDialogBuilder(this, theme)
|
onExportFailed(error!!)
|
||||||
.setTitle(R.string.error)
|
|
||||||
.setMessage(getString(R.string.export_failed, path))
|
|
||||||
.setPositiveButton(R.string.ok, null)
|
|
||||||
.show()
|
|
||||||
} else {
|
} else {
|
||||||
startActivity(openIntent)
|
app.isStartingExternalApp = true
|
||||||
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
app.isExporting = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -644,8 +658,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
if (app.isStartingExternalApp) {
|
if (app.isStartingExternalApp) {
|
||||||
MemoryFileProvider.wipe()
|
TemporaryFileProvider.instance.wipe()
|
||||||
DiskFileProvider.wipe()
|
|
||||||
}
|
}
|
||||||
if (encryptedVolume.isClosed()) {
|
if (encryptedVolume.isClosed()) {
|
||||||
finish()
|
finish()
|
||||||
|
@ -164,7 +164,7 @@ class ExplorerActivity : BaseExplorerActivity() {
|
|||||||
} else {
|
} else {
|
||||||
val adapter = IconTextDialogAdapter(this)
|
val adapter = IconTextDialogAdapter(this)
|
||||||
adapter.items = listOf(
|
adapter.items = listOf(
|
||||||
listOf("importFromOtherVolumes", R.string.import_from_other_volume, R.drawable.icon_transfert),
|
listOf("importFromOtherVolumes", R.string.import_from_other_volume, R.drawable.icon_transfer),
|
||||||
listOf("importFiles", R.string.import_files, R.drawable.icon_encrypt),
|
listOf("importFiles", R.string.import_files, R.drawable.icon_encrypt),
|
||||||
listOf("importFolder", R.string.import_folder, R.drawable.icon_import_folder),
|
listOf("importFolder", R.string.import_folder, R.drawable.icon_import_folder),
|
||||||
listOf("createFile", R.string.new_file, R.drawable.icon_file_unknown),
|
listOf("createFile", R.string.new_file, R.drawable.icon_file_unknown),
|
||||||
@ -385,26 +385,24 @@ class ExplorerActivity : BaseExplorerActivity() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.share -> {
|
R.id.share -> {
|
||||||
app.isStartingExternalApp = true
|
|
||||||
val files = explorerAdapter.selectedItems.map { i ->
|
val files = explorerAdapter.selectedItems.map { i ->
|
||||||
explorerElements[i].let {
|
explorerElements[i].let {
|
||||||
Pair(it.fullPath, it.stat.size)
|
Pair(it.fullPath, it.stat.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
object : LoadingTask<Pair<Intent?, String?>>(this, theme, R.string.loading_msg_export) {
|
app.isExporting = true
|
||||||
override suspend fun doTask(): Pair<Intent?, String?> {
|
object : LoadingTask<Pair<Intent?, Int?>>(this, theme, R.string.loading_msg_export) {
|
||||||
return fileShare.share(files)
|
override suspend fun doTask(): Pair<Intent?, Int?> {
|
||||||
|
return fileShare.share(files, volumeId)
|
||||||
}
|
}
|
||||||
}.startTask(lifecycleScope) { (intent, failedItem) ->
|
}.startTask(lifecycleScope) { (intent, error) ->
|
||||||
if (intent == null) {
|
if (intent == null) {
|
||||||
CustomAlertDialogBuilder(this, theme)
|
onExportFailed(error!!)
|
||||||
.setTitle(R.string.error)
|
|
||||||
.setMessage(getString(R.string.export_failed, failedItem))
|
|
||||||
.setPositiveButton(R.string.ok, null)
|
|
||||||
.show()
|
|
||||||
} else {
|
} else {
|
||||||
|
app.isStartingExternalApp = true
|
||||||
startActivity(Intent.createChooser(intent, getString(R.string.share_chooser)))
|
startActivity(Intent.createChooser(intent, getString(R.string.share_chooser)))
|
||||||
}
|
}
|
||||||
|
app.isExporting = false
|
||||||
}
|
}
|
||||||
unselectAll()
|
unselectAll()
|
||||||
true
|
true
|
||||||
|
@ -6,7 +6,7 @@ import sushi.hardcore.droidfs.util.PathUtils
|
|||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
|
|
||||||
class ExplorerElement(val name: String, val stat: Stat, val parentPath: String) {
|
class ExplorerElement(val name: String, val stat: Stat, val parentPath: String) {
|
||||||
val fullPath: String = PathUtils.pathJoin(parentPath, name)
|
val fullPath: String = PathUtils.pathJoin(parentPath.ifEmpty { "/" }, name)
|
||||||
val collationKey = Collator.getInstance().getCollationKeyForFileName(fullPath)
|
val collationKey = Collator.getInstance().getCollationKeyForFileName(fullPath)
|
||||||
|
|
||||||
val isDirectory: Boolean
|
val isDirectory: Boolean
|
||||||
|
@ -160,7 +160,6 @@ abstract class EncryptedVolume: Parcelable {
|
|||||||
if (written == length) {
|
if (written == length) {
|
||||||
offset += written
|
offset += written
|
||||||
} else {
|
} else {
|
||||||
inputStream.close()
|
|
||||||
success = false
|
success = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
package sushi.hardcore.droidfs.filesystems
|
package sushi.hardcore.droidfs.filesystems
|
||||||
|
|
||||||
class Stat(val type: Int, var size: Long, val mTime: Long) {
|
class Stat(mode: Int, var size: Long, val mTime: Long) {
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val S_IFMT = 0xF000
|
||||||
const val S_IFDIR = 0x4000
|
const val S_IFDIR = 0x4000
|
||||||
const val S_IFREG = 0x8000
|
const val S_IFREG = 0x8000
|
||||||
const val S_IFLNK = 0xA000
|
const val S_IFLNK = 0xA000
|
||||||
const val PARENT_FOLDER_TYPE = -1
|
const val PARENT_FOLDER_TYPE = 0xE000
|
||||||
|
|
||||||
fun parentFolderStat(): Stat {
|
fun parentFolderStat(): Stat {
|
||||||
return Stat(PARENT_FOLDER_TYPE, -1, -1)
|
return Stat(PARENT_FOLDER_TYPE, -1, -1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val type = mode and S_IFMT
|
||||||
}
|
}
|
@ -13,4 +13,11 @@ object Compat {
|
|||||||
bundle.getParcelable(name)
|
bundle.getParcelable(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val MEMFD_CREATE_MINIMUM_KERNEL_VERSION = Version("3.17")
|
||||||
|
|
||||||
|
fun isMemFileSupported(): Boolean {
|
||||||
|
val kernel = System.getProperty("os.version") ?: return false
|
||||||
|
return Version(kernel) >= MEMFD_CREATE_MINIMUM_KERNEL_VERSION
|
||||||
|
}
|
||||||
}
|
}
|
@ -3,7 +3,7 @@ package sushi.hardcore.droidfs.util
|
|||||||
import java.lang.Integer.max
|
import java.lang.Integer.max
|
||||||
|
|
||||||
class Version(inputVersion: String) : Comparable<Version> {
|
class Version(inputVersion: String) : Comparable<Version> {
|
||||||
private var version: String
|
private val version: String
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val regex = "[0-9]+(\\.[0-9]+)*".toRegex()
|
val regex = "[0-9]+(\\.[0-9]+)*".toRegex()
|
||||||
|
@ -1,24 +1,30 @@
|
|||||||
|
#include <errno.h>
|
||||||
|
#include <string.h>
|
||||||
#include <sys/mman.h>
|
#include <sys/mman.h>
|
||||||
#include <sys/syscall.h>
|
#include <sys/syscall.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
|
#include <android/log.h>
|
||||||
|
|
||||||
|
const char* LOG_TAG = "MemFile";
|
||||||
|
|
||||||
|
void log_err(const char* function) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "%s(): %s", function, strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jint JNICALL
|
JNIEXPORT jint JNICALL
|
||||||
Java_sushi_hardcore_droidfs_MemFile_00024Companion_createMemFile(JNIEnv *env, jobject thiz, jstring jname,
|
Java_sushi_hardcore_droidfs_MemFile_00024Companion_createMemFile(JNIEnv *env, jobject thiz, jstring jname,
|
||||||
jlong size) {
|
jlong size) {
|
||||||
const char* name = env->GetStringUTFChars(jname, nullptr);
|
const char* name = (*env)->GetStringUTFChars(env, jname, NULL);
|
||||||
int fd = syscall(SYS_memfd_create, name, MFD_CLOEXEC);
|
int fd = syscall(SYS_memfd_create, name, MFD_CLOEXEC);
|
||||||
if (fd < 0) return fd;
|
if (fd < 0) {
|
||||||
|
log_err("memfd_create");
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
if (ftruncate64(fd, size) == -1) {
|
if (ftruncate64(fd, size) == -1) {
|
||||||
|
log_err("ftruncate64");
|
||||||
close(fd);
|
close(fd);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return fd;
|
return fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT void JNICALL
|
|
||||||
Java_sushi_hardcore_droidfs_MemFile_close(JNIEnv *env, jobject thiz, jint fd) {
|
|
||||||
close(fd);
|
|
||||||
}
|
|
BIN
app/src/main/res/drawable-hdpi/icon_document_provider.png
Normal file
BIN
app/src/main/res/drawable-hdpi/icon_document_provider.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 819 B |
BIN
app/src/main/res/drawable-mdpi/icon_document_provider.png
Normal file
BIN
app/src/main/res/drawable-mdpi/icon_document_provider.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 540 B |
BIN
app/src/main/res/drawable-xhdpi/icon_document_provider.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/icon_document_provider.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
BIN
app/src/main/res/drawable-xxhdpi/icon_document_provider.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/icon_document_provider.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
BIN
app/src/main/res/drawable-xxxhdpi/icon_document_provider.png
Normal file
BIN
app/src/main/res/drawable-xxxhdpi/icon_document_provider.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
5
app/src/main/res/drawable/icon_edit.xml
Normal file
5
app/src/main/res/drawable/icon_edit.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:height="24dp"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="?attr/colorAccent" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||||
|
</vector>
|
@ -263,4 +263,14 @@
|
|||||||
<string name="filesystem_id_changed">The filesystem id in the config file is different to the last time we opened this volume. This could mean an attacker replaced the filesystem with a different one.</string>
|
<string name="filesystem_id_changed">The filesystem id in the config file is different to the last time we opened this volume. This could mean an attacker replaced the filesystem with a different one.</string>
|
||||||
<string name="inaccessible_base_dir">The volume doesn\'t exist or is inaccessible.</string>
|
<string name="inaccessible_base_dir">The volume doesn\'t exist or is inaccessible.</string>
|
||||||
<string name="task_failed">The task failed: %s</string>
|
<string name="task_failed">The task failed: %s</string>
|
||||||
|
<string name="usf_expose">Expose open volumes</string>
|
||||||
|
<string name="usf_expose_summary">Allow other applications to browse open volumes as documents providers</string>
|
||||||
|
<string name="usf_saf_write">Grant write access</string>
|
||||||
|
<string name="usf_saf_write_summary">Grant write access when opening files with other applications</string>
|
||||||
|
<string name="saf">Storage Access Framework</string>
|
||||||
|
<string name="tmp_export_failed">Export failed: %s</string>
|
||||||
|
<string name="export_failed_create">can\'t create exported file</string>
|
||||||
|
<string name="export_failed_export">failed to export file</string>
|
||||||
|
<string name="export_mem">Exporting to memory…</string>
|
||||||
|
<string name="export_disk">Exporting to disk…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -1,6 +1,19 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<PreferenceCategory android:title="@string/about">
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:icon="@drawable/icon_notes"
|
||||||
|
android:summary="@string/usf_read_doc"
|
||||||
|
android:title="@string/usf_doc">
|
||||||
|
<intent
|
||||||
|
android:action="android.intent.action.VIEW"
|
||||||
|
android:data="https://forge.chapril.org/hardcoresushi/DroidFS#unsafe-features" />
|
||||||
|
</Preference>
|
||||||
|
|
||||||
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/ux">
|
<PreferenceCategory android:title="@string/ux">
|
||||||
|
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
@ -18,23 +31,13 @@
|
|||||||
android:icon="@drawable/icon_lock_open"
|
android:icon="@drawable/icon_lock_open"
|
||||||
android:key="usf_decrypt"
|
android:key="usf_decrypt"
|
||||||
android:title="@string/usf_decrypt" />
|
android:title="@string/usf_decrypt" />
|
||||||
<SwitchPreference
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:icon="@drawable/icon_open_in_new"
|
|
||||||
android:key="usf_open"
|
|
||||||
android:title="@string/usf_open" />
|
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
android:icon="@drawable/icon_share"
|
android:icon="@drawable/icon_share"
|
||||||
android:key="usf_share"
|
android:key="usf_share"
|
||||||
android:title="@string/usf_share" />
|
android:title="@string/usf_share" />
|
||||||
|
|
||||||
<SwitchPreference
|
|
||||||
android:defaultValue="false"
|
|
||||||
android:icon="@drawable/icon_lock_open"
|
|
||||||
android:key="usf_keep_open"
|
|
||||||
android:title="@string/usf_keep_open" />
|
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/usf_volume_management">
|
<PreferenceCategory android:title="@string/usf_volume_management">
|
||||||
@ -45,18 +48,35 @@
|
|||||||
android:key="usf_fingerprint"
|
android:key="usf_fingerprint"
|
||||||
android:title="@string/usf_fingerprint" />
|
android:title="@string/usf_fingerprint" />
|
||||||
|
|
||||||
|
<SwitchPreference
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:icon="@drawable/icon_lock_open"
|
||||||
|
android:key="usf_keep_open"
|
||||||
|
android:title="@string/usf_keep_open" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/about">
|
<PreferenceCategory android:title="@string/saf">
|
||||||
|
|
||||||
<Preference
|
<SwitchPreference
|
||||||
android:icon="@drawable/icon_notes"
|
android:defaultValue="false"
|
||||||
android:summary="@string/usf_read_doc"
|
android:icon="@drawable/icon_open_in_new"
|
||||||
android:title="@string/usf_doc">
|
android:key="usf_open"
|
||||||
<intent
|
android:title="@string/usf_open" />
|
||||||
android:action="android.intent.action.VIEW"
|
|
||||||
android:data="https://forge.chapril.org/hardcoresushi/DroidFS#unsafe-features" />
|
<SwitchPreference
|
||||||
</Preference>
|
android:defaultValue="false"
|
||||||
|
android:icon="@drawable/icon_transfer"
|
||||||
|
android:key="usf_expose"
|
||||||
|
android:title="@string/usf_expose"
|
||||||
|
android:summary="@string/usf_expose_summary" />
|
||||||
|
|
||||||
|
<SwitchPreference
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:icon="@drawable/icon_edit"
|
||||||
|
android:key="usf_saf_write"
|
||||||
|
android:title="@string/usf_saf_write"
|
||||||
|
android:summary="@string/usf_saf_write_summary" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user