Allow to open & create volumes without remembering

This commit is contained in:
Matéo Duparc 2022-09-30 21:22:37 +02:00
parent 4df1086734
commit 7c2f87109a
Signed by untrusted user: hardcoresushi
GPG Key ID: AFE384344A45E13A
30 changed files with 774 additions and 512 deletions

View File

@ -18,7 +18,7 @@ open class BaseActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this) sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
if (shouldCheckTheme && applyCustomTheme) { if (shouldCheckTheme && applyCustomTheme) {
themeValue = sharedPrefs.getString("theme", ConstValues.DEFAULT_THEME_VALUE)!! themeValue = sharedPrefs.getString(ConstValues.THEME_VALUE_KEY, ConstValues.DEFAULT_THEME_VALUE)!!
when (themeValue) { when (themeValue) {
"black_green" -> setTheme(R.style.BlackGreen) "black_green" -> setTheme(R.style.BlackGreen)
"dark_red" -> setTheme(R.style.DarkRed) "dark_red" -> setTheme(R.style.DarkRed)
@ -47,13 +47,4 @@ open class BaseActivity: AppCompatActivity() {
recreate() recreate()
} }
} }
inline fun <reified T: Parcelable> getParcelableExtra(intent: Intent, name: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(name, T::class.java)
} else {
@Suppress("Deprecation")
intent.getParcelableExtra(name)
}
}
} }

View File

@ -32,6 +32,7 @@ import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
import sushi.hardcore.droidfs.databinding.ActivityCameraBinding import sushi.hardcore.droidfs.databinding.ActivityCameraBinding
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.video_recording.SeekableWriter import sushi.hardcore.droidfs.video_recording.SeekableWriter
import sushi.hardcore.droidfs.video_recording.VideoCapture import sushi.hardcore.droidfs.video_recording.VideoCapture
@ -95,7 +96,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
binding = ActivityCameraBinding.inflate(layoutInflater) binding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
supportActionBar?.hide() supportActionBar?.hide()
encryptedVolume = getParcelableExtra(intent, "volume")!! encryptedVolume = IntentUtils.getParcelableExtra(intent, "volume")!!
outputDirectory = intent.getStringExtra("path")!! outputDirectory = intent.getStringExtra("path")!!
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {

View File

@ -14,6 +14,7 @@ import sushi.hardcore.droidfs.databinding.ActivityChangePasswordBinding
import sushi.hardcore.droidfs.filesystems.CryfsVolume import sushi.hardcore.droidfs.filesystems.CryfsVolume
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.GocryptfsVolume import sushi.hardcore.droidfs.filesystems.GocryptfsVolume
import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.ObjRef import sushi.hardcore.droidfs.util.ObjRef
import sushi.hardcore.droidfs.util.WidgetUtil import sushi.hardcore.droidfs.util.WidgetUtil
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
@ -22,7 +23,7 @@ import java.util.*
class ChangePasswordActivity: BaseActivity() { class ChangePasswordActivity: BaseActivity() {
private lateinit var binding: ActivityChangePasswordBinding private lateinit var binding: ActivityChangePasswordBinding
private lateinit var volume: SavedVolume private lateinit var volume: VolumeData
private lateinit var volumeDatabase: VolumeDatabase private lateinit var volumeDatabase: VolumeDatabase
private var fingerprintProtector: FingerprintProtector? = null private var fingerprintProtector: FingerprintProtector? = null
private var usfFingerprint: Boolean = false private var usfFingerprint: Boolean = false
@ -32,7 +33,7 @@ class ChangePasswordActivity: BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
volume = getParcelableExtra(intent, "volume")!! volume = IntentUtils.getParcelableExtra(intent, "volume")!!
binding = ActivityChangePasswordBinding.inflate(layoutInflater) binding = ActivityChangePasswordBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
title = getString(R.string.change_password) title = getString(R.string.change_password)

View File

@ -4,7 +4,6 @@ import android.net.Uri
import java.io.File import java.io.File
object ConstValues { object ConstValues {
const val CREATOR = "DroidFS"
const val VOLUME_DATABASE_NAME = "SavedVolumes" const val VOLUME_DATABASE_NAME = "SavedVolumes"
const val CRYFS_LOCAL_STATE_DIR = "cryfsLocalState" const val CRYFS_LOCAL_STATE_DIR = "cryfsLocalState"
const val SORT_ORDER_KEY = "sort_order" const val SORT_ORDER_KEY = "sort_order"
@ -13,6 +12,9 @@ object ConstValues {
const val IO_BUFF_SIZE = 16384 const val IO_BUFF_SIZE = 16384
const val SLIDESHOW_DELAY: Long = 4000 const val SLIDESHOW_DELAY: Long = 4000
const val DEFAULT_THEME_VALUE = "dark_green" const val DEFAULT_THEME_VALUE = "dark_green"
const val THEME_VALUE_KEY = "theme"
const val DEFAULT_VOLUME_KEY = "default_volume"
const val REMEMBER_VOLUME_KEY = "remember_volume"
const val THUMBNAIL_MAX_SIZE_KEY = "thumbnail_max_size" const val THUMBNAIL_MAX_SIZE_KEY = "thumbnail_max_size"
const val DEFAULT_THUMBNAIL_MAX_SIZE = 10_000L const val DEFAULT_THUMBNAIL_MAX_SIZE = 10_000L
const val PIN_PASSWORDS_KEY = "pin_passwords" const val PIN_PASSWORDS_KEY = "pin_passwords"

View File

@ -133,7 +133,7 @@ class FingerprintProtector private constructor(
private lateinit var cipher: Cipher private lateinit var cipher: Cipher
private var isCipherReady = false private var isCipherReady = false
private var cipherActionMode: Int? = null private var cipherActionMode: Int? = null
private lateinit var volume: SavedVolume private lateinit var volume: VolumeData
private lateinit var dataToProcess: ByteArray private lateinit var dataToProcess: ByteArray
private fun resetHashStorage() { private fun resetHashStorage() {
@ -207,7 +207,7 @@ class FingerprintProtector private constructor(
.show() .show()
} }
fun savePasswordHash(volume: SavedVolume, plainText: ByteArray) { fun savePasswordHash(volume: VolumeData, plainText: ByteArray) {
this.volume = volume this.volume = volume
val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder() val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(activity.getString(R.string.encrypt_action_description)) .setTitle(activity.getString(R.string.encrypt_action_description))

View File

@ -1,6 +1,6 @@
package sushi.hardcore.droidfs package sushi.hardcore.droidfs
import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -8,7 +8,7 @@ import kotlinx.coroutines.withContext
import sushi.hardcore.droidfs.databinding.DialogLoadingBinding import sushi.hardcore.droidfs.databinding.DialogLoadingBinding
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
abstract class LoadingTask<T>(val activity: AppCompatActivity, themeValue: String, loadingMessageResId: Int) { abstract class LoadingTask<T>(val activity: FragmentActivity, themeValue: String, loadingMessageResId: Int) {
private val dialogLoading = CustomAlertDialogBuilder(activity, themeValue) private val dialogLoading = CustomAlertDialogBuilder(activity, themeValue)
.setView( .setView(
DialogLoadingBinding.inflate(activity.layoutInflater).apply { DialogLoadingBinding.inflate(activity.layoutInflater).apply {

View File

@ -1,19 +1,15 @@
package sushi.hardcore.droidfs package sushi.hardcore.droidfs
import android.annotation.SuppressLint
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.text.InputType
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.WindowManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@ -21,45 +17,31 @@ import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.ConstValues.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.RestrictedFileProvider import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
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.databinding.DialogOpenVolumeBinding import sushi.hardcore.droidfs.explorers.ExplorerRouter
import sushi.hardcore.droidfs.explorers.ExplorerActivity
import sushi.hardcore.droidfs.explorers.ExplorerActivityDrop
import sushi.hardcore.droidfs.explorers.ExplorerActivityPick
import sushi.hardcore.droidfs.file_operations.FileOperationService import sushi.hardcore.droidfs.file_operations.FileOperationService
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.ObjRef import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.WidgetUtil
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog import sushi.hardcore.droidfs.widgets.EditTextDialog
import java.io.File import java.io.File
import java.util.*
class MainActivity : BaseActivity(), VolumeAdapter.Listener { class MainActivity : BaseActivity(), VolumeAdapter.Listener {
companion object {
const val DEFAULT_VOLUME_KEY = "default_volume"
}
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var volumeDatabase: VolumeDatabase private lateinit var volumeDatabase: VolumeDatabase
private lateinit var volumeAdapter: VolumeAdapter private lateinit var volumeAdapter: VolumeAdapter
private var fingerprintProtector: FingerprintProtector? = null private lateinit var volumeOpener: VolumeOpener
private var usfFingerprint: Boolean = false
private var usfKeepOpen: Boolean = false private var usfKeepOpen: Boolean = false
private var defaultVolumeName: String? = null
private var addVolume = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private var addVolume = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
when (result.resultCode) { if ((explorerRouter.pickMode || explorerRouter.dropMode) && result.resultCode != AddVolumeActivity.RESULT_USER_BACK) {
AddVolumeActivity.RESULT_VOLUME_ADDED -> onVolumeAdded() setResult(result.resultCode, result.data) // forward result
AddVolumeActivity.RESULT_HASH_STORAGE_RESET -> { finish()
volumeAdapter.refresh()
binding.textNoVolumes.visibility = View.GONE
}
} }
} }
private var changePasswordPosition: Int? = null private var changePasswordPosition: Int? = null
@ -71,8 +53,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
onDirectoryPicked(uri) onDirectoryPicked(uri)
} }
private lateinit var fileOperationService: FileOperationService private lateinit var fileOperationService: FileOperationService
private var pickMode = false private lateinit var explorerRouter: ExplorerRouter
private var dropMode = false
private var shouldCloseVolume = true // used when launched to pick file from another volume private var shouldCloseVolume = true // used when launched to pick file from another volume
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -98,14 +79,13 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
} }
.show() .show()
} }
pickMode = intent.action == "pick" explorerRouter = ExplorerRouter(this, intent)
dropMode = (intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE) && intent.extras != null
volumeDatabase = VolumeDatabase(this) volumeDatabase = VolumeDatabase(this)
volumeAdapter = VolumeAdapter( volumeAdapter = VolumeAdapter(
this, this,
volumeDatabase, volumeDatabase,
!pickMode && !dropMode, !explorerRouter.pickMode && !explorerRouter.dropMode,
!dropMode, !explorerRouter.dropMode,
this, this,
) )
binding.recyclerViewVolumes.adapter = volumeAdapter binding.recyclerViewVolumes.adapter = volumeAdapter
@ -113,24 +93,22 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
if (volumeAdapter.volumes.isEmpty()) { if (volumeAdapter.volumes.isEmpty()) {
binding.textNoVolumes.visibility = View.VISIBLE binding.textNoVolumes.visibility = View.VISIBLE
} }
if (pickMode) { if (explorerRouter.pickMode) {
title = getString(R.string.select_volume) title = getString(R.string.select_volume)
binding.fab.visibility = View.GONE }
} else { binding.fab.setOnClickListener {
binding.fab.setOnClickListener { addVolume.launch(Intent(this, AddVolumeActivity::class.java).also {
addVolume.launch(Intent(this, AddVolumeActivity::class.java)) if (explorerRouter.dropMode || explorerRouter.pickMode) {
} IntentUtils.forwardIntent(intent, it)
shouldCloseVolume = false
}
})
} }
usfKeepOpen = sharedPrefs.getBoolean("usf_keep_open", false) usfKeepOpen = sharedPrefs.getBoolean("usf_keep_open", false)
usfFingerprint = sharedPrefs.getBoolean("usf_fingerprint", false) volumeOpener = VolumeOpener(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { volumeOpener.defaultVolumeName?.let { name ->
fingerprintProtector = FingerprintProtector.new(this, themeValue, volumeDatabase)
}
defaultVolumeName = sharedPrefs.getString(DEFAULT_VOLUME_KEY, null)
defaultVolumeName?.let { name ->
try { try {
val (position, volume) = volumeAdapter.volumes.withIndex().first { it.value.name == name } openVolume(volumeAdapter.volumes.first { it.name == name })
openVolume(volume, position)
} catch (e: NoSuchElementException) { } catch (e: NoSuchElementException) {
unsetDefaultVolume() unsetDefaultVolume()
} }
@ -139,8 +117,9 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
if (volumeAdapter.selectedItems.isNotEmpty()) { if (volumeAdapter.selectedItems.isNotEmpty()) {
unselectAll() unselectAll()
} else { } else {
if (pickMode) if (explorerRouter.pickMode) {
shouldCloseVolume = false shouldCloseVolume = false
}
isEnabled = false isEnabled = false
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
@ -158,10 +137,15 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
// refresh theme if changed in SettingsActivity // refresh theme if changed in SettingsActivity
val newThemeValue = sharedPrefs.getString("theme", ConstValues.DEFAULT_THEME_VALUE)!! val newThemeValue = sharedPrefs.getString(ConstValues.THEME_VALUE_KEY, ConstValues.DEFAULT_THEME_VALUE)!!
onThemeChanged(newThemeValue) onThemeChanged(newThemeValue)
volumeOpener.themeValue = newThemeValue
volumeAdapter.refresh()
if (volumeAdapter.volumes.isNotEmpty()) {
binding.textNoVolumes.visibility = View.GONE
}
// refresh this in case another instance of MainActivity changes its value // refresh this in case another instance of MainActivity changes its value
defaultVolumeName = sharedPrefs.getString(DEFAULT_VOLUME_KEY, null) volumeOpener.defaultVolumeName = sharedPrefs.getString(DEFAULT_VOLUME_KEY, null)
} }
override fun onSelectionChanged(size: Int) { override fun onSelectionChanged(size: Int) {
@ -172,9 +156,9 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
} }
} }
override fun onVolumeItemClick(volume: SavedVolume, position: Int) { override fun onVolumeItemClick(volume: VolumeData, position: Int) {
if (volumeAdapter.selectedItems.isEmpty()) if (volumeAdapter.selectedItems.isEmpty())
openVolume(volume, position) openVolume(volume)
else else
invalidateOptionsMenu() invalidateOptionsMenu()
} }
@ -183,14 +167,6 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
invalidateOptionsMenu() invalidateOptionsMenu()
} }
private fun onVolumeAdded() {
volumeAdapter.apply {
volumes = volumeDatabase.getVolumes()
notifyItemInserted(volumes.size)
}
binding.textNoVolumes.visibility = View.GONE
}
private fun unselectAll(notifyChange: Boolean = true) { private fun unselectAll(notifyChange: Boolean = true) {
volumeAdapter.unSelectAll(notifyChange) volumeAdapter.unSelectAll(notifyChange)
invalidateOptionsMenu() invalidateOptionsMenu()
@ -203,7 +179,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
invalidateOptionsMenu() invalidateOptionsMenu()
} }
private fun removeVolumes(volumes: List<SavedVolume>, i: Int = 0, doDeleteVolumeContent: Boolean? = null) { private fun removeVolumes(volumes: List<VolumeData>, i: Int = 0, doDeleteVolumeContent: Boolean? = null) {
if (i < volumes.size) { if (i < volumes.size) {
if (volumes[i].isHidden) { if (volumes[i].isHidden) {
if (doDeleteVolumeContent == null) { if (doDeleteVolumeContent == null) {
@ -258,15 +234,16 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
remove(DEFAULT_VOLUME_KEY) remove(DEFAULT_VOLUME_KEY)
apply() apply()
} }
defaultVolumeName = null volumeOpener.defaultVolumeName = null
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
if (pickMode || dropMode) { if (explorerRouter.pickMode || explorerRouter.dropMode) {
if (pickMode) if (explorerRouter.pickMode) {
shouldCloseVolume = false shouldCloseVolume = false
}
finish() finish()
} else { } else {
unselectAll() unselectAll()
@ -323,7 +300,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
DocumentFile.fromFile(File(volume.name)), DocumentFile.fromFile(File(volume.name)),
DocumentFile.fromFile(filesDir), DocumentFile.fromFile(filesDir),
) { ) {
SavedVolume(volume.shortName, true, volume.type, volume.encryptedHash, volume.iv) VolumeData(volume.shortName, true, volume.type, volume.encryptedHash, volume.iv)
} }
} }
} }
@ -345,7 +322,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_activity, menu) menuInflater.inflate(R.menu.main_activity, menu)
menu.findItem(R.id.settings).isVisible = !pickMode && !dropMode menu.findItem(R.id.settings).isVisible = !explorerRouter.pickMode && !explorerRouter.dropMode
val isSelecting = volumeAdapter.selectedItems.isNotEmpty() val isSelecting = volumeAdapter.selectedItems.isNotEmpty()
menu.findItem(R.id.select_all).isVisible = isSelecting menu.findItem(R.id.select_all).isVisible = isSelecting
menu.findItem(R.id.remove).isVisible = isSelecting menu.findItem(R.id.remove).isVisible = isSelecting
@ -359,7 +336,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
menu.findItem(R.id.change_password).isVisible = onlyOneAndWriteable menu.findItem(R.id.change_password).isVisible = onlyOneAndWriteable
menu.findItem(R.id.remove_default_open).isVisible = menu.findItem(R.id.remove_default_open).isVisible =
onlyOneSelected && onlyOneSelected &&
volumeAdapter.volumes[volumeAdapter.selectedItems.first()].name == defaultVolumeName volumeAdapter.volumes[volumeAdapter.selectedItems.first()].name == volumeOpener.defaultVolumeName
with(menu.findItem(R.id.copy)) { with(menu.findItem(R.id.copy)) {
isVisible = onlyOneSelected isVisible = onlyOneSelected
if (isVisible) { if (isVisible) {
@ -371,7 +348,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
} }
} }
menu.findItem(R.id.rename).isVisible = onlyOneAndWriteable menu.findItem(R.id.rename).isVisible = onlyOneAndWriteable
supportActionBar?.setDisplayHomeAsUpEnabled(isSelecting || pickMode || dropMode) supportActionBar?.setDisplayHomeAsUpEnabled(isSelecting || explorerRouter.pickMode || explorerRouter.dropMode)
return true return true
} }
@ -394,7 +371,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
dstRootDirectory.name?.let { name -> dstRootDirectory.name?.let { name ->
val path = PathUtils.getFullPathFromTreeUri(dstRootDirectory.uri, this) val path = PathUtils.getFullPathFromTreeUri(dstRootDirectory.uri, this)
if (path == null) null if (path == null) null
else SavedVolume( else VolumeData(
PathUtils.pathJoin(path, name), PathUtils.pathJoin(path, name),
false, false,
volume.type, volume.type,
@ -406,7 +383,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
} }
} }
private fun copyVolume(srcDocumentFile: DocumentFile, dstDocumentFile: DocumentFile, getResultVolume: (DocumentFile) -> SavedVolume?) { private fun copyVolume(srcDocumentFile: DocumentFile, dstDocumentFile: DocumentFile, getResultVolume: (DocumentFile) -> VolumeData?) {
lifecycleScope.launch { lifecycleScope.launch {
val result = fileOperationService.copyVolume(srcDocumentFile, dstDocumentFile) val result = fileOperationService.copyVolume(srcDocumentFile, dstDocumentFile)
when { when {
@ -417,7 +394,11 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
result.dstRootDirectory?.let { result.dstRootDirectory?.let {
getResultVolume(it)?.let { volume -> getResultVolume(it)?.let { volume ->
volumeDatabase.saveVolume(volume) volumeDatabase.saveVolume(volume)
onVolumeAdded() volumeAdapter.apply {
volumes = volumeDatabase.getVolumes()
notifyItemInserted(volumes.size)
}
binding.textNoVolumes.visibility = View.GONE
Toast.makeText(this@MainActivity, R.string.copy_success, Toast.LENGTH_SHORT).show() Toast.makeText(this@MainActivity, R.string.copy_success, Toast.LENGTH_SHORT).show()
} }
} }
@ -433,7 +414,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
} }
} }
private fun renameVolume(volume: SavedVolume, position: Int) { private fun renameVolume(volume: VolumeData, position: Int) {
with (EditTextDialog(this, R.string.new_volume_name) { newName -> with (EditTextDialog(this, R.string.new_volume_name) { newName ->
val srcPath = File(volume.getFullPath(filesDir.path)) val srcPath = File(volume.getFullPath(filesDir.path))
val dstPath = File(srcPath.parent, newName).canonicalFile val dstPath = File(srcPath.parent, newName).canonicalFile
@ -453,12 +434,12 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
if (success) { if (success) {
volumeDatabase.renameVolume(volume.name, newDBName) volumeDatabase.renameVolume(volume.name, newDBName)
unselect(position) unselect(position)
if (volume.name == defaultVolumeName) { if (volume.name == volumeOpener.defaultVolumeName) {
with (sharedPrefs.edit()) { with (sharedPrefs.edit()) {
putString(DEFAULT_VOLUME_KEY, newDBName) putString(DEFAULT_VOLUME_KEY, newDBName)
apply() apply()
} }
defaultVolumeName = newDBName volumeOpener.defaultVolumeName = newDBName
} }
} else { } else {
Toast.makeText(this, R.string.volume_rename_failed, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.volume_rename_failed, Toast.LENGTH_SHORT).show()
@ -469,193 +450,35 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
} }
} }
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23 private fun openVolume(volume: VolumeData) {
private fun openVolume(volume: SavedVolume, position: Int) { volumeOpener.openVolume(volume, true, object : VolumeOpener.VolumeOpenerCallbacks {
if (volume.type == EncryptedVolume.GOCRYPTFS_VOLUME_TYPE && BuildConfig.GOCRYPTFS_DISABLED) { override fun onHashStorageReset() {
Toast.makeText(this, R.string.gocryptfs_disabled, Toast.LENGTH_SHORT).show() volumeAdapter.refresh()
return }
} else if (volume.type == EncryptedVolume.CRYFS_VOLUME_TYPE && BuildConfig.CRYFS_DISABLED) {
Toast.makeText(this, R.string.cryfs_disabled, Toast.LENGTH_SHORT).show() override fun onVolumeOpened(encryptedVolume: EncryptedVolume, volumeShortName: String) {
return startActivity(explorerRouter.getExplorerIntent(encryptedVolume, volumeShortName))
} if (explorerRouter.pickMode) {
var askForPassword = true shouldCloseVolume = false
fingerprintProtector?.let { fingerprintProtector -> }
volume.encryptedHash?.let { encryptedHash -> if (explorerRouter.dropMode || explorerRouter.pickMode) {
volume.iv?.let { iv -> finish()
askForPassword = false
fingerprintProtector.listener = object : FingerprintProtector.Listener {
override fun onHashStorageReset() {
volumeAdapter.refresh()
}
override fun onPasswordHashDecrypted(hash: ByteArray) {
object : LoadingTask<EncryptedVolume?>(this@MainActivity, themeValue, R.string.loading_msg_open) {
override suspend fun doTask(): EncryptedVolume? {
val encryptedVolume = EncryptedVolume.init(volume, filesDir.path, null, hash, null)
Arrays.fill(hash, 0)
return encryptedVolume
}
}.startTask(lifecycleScope) { encryptedVolume ->
if (encryptedVolume == null) {
CustomAlertDialogBuilder(this@MainActivity, themeValue)
.setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_failed_hash_msg)
.setPositiveButton(R.string.ok, null)
.show()
} else {
startExplorer(encryptedVolume, volume.shortName)
}
}
}
override fun onPasswordHashSaved() {}
override fun onFailed(pending: Boolean) {
if (!pending) {
askForPassword(volume, position)
}
}
}
fingerprintProtector.loadPasswordHash(volume.shortName, encryptedHash, iv)
} }
} }
} })
if (askForPassword)
askForPassword(volume, position)
} }
private fun onPasswordSubmitted(volume: SavedVolume, position: Int, dialogBinding: DialogOpenVolumeBinding) { override fun onResume() {
if (dialogBinding.checkboxDefaultOpen.isChecked xor (defaultVolumeName == volume.name)) { super.onResume()
with (sharedPrefs.edit()) { shouldCloseVolume = true
defaultVolumeName = if (dialogBinding.checkboxDefaultOpen.isChecked) {
putString(DEFAULT_VOLUME_KEY, volume.name)
volume.name
} else {
remove(DEFAULT_VOLUME_KEY)
null
}
apply()
}
}
// openVolumeWithPassword is responsible for wiping the password
openVolumeWithPassword(
volume,
position,
WidgetUtil.encodeEditTextContent(dialogBinding.editPassword),
dialogBinding.checkboxSavePassword.isChecked,
)
}
private fun askForPassword(volume: SavedVolume, position: Int, savePasswordHash: Boolean = false) {
val dialogBinding = DialogOpenVolumeBinding.inflate(layoutInflater)
if (!usfFingerprint || fingerprintProtector == null || volume.encryptedHash != null) {
dialogBinding.checkboxSavePassword.visibility = View.GONE
} else {
dialogBinding.checkboxSavePassword.isChecked = savePasswordHash
}
dialogBinding.checkboxDefaultOpen.isChecked = defaultVolumeName == volume.name
val dialog = CustomAlertDialogBuilder(this, themeValue)
.setTitle(getString(R.string.open_dialog_title, volume.shortName))
.setView(dialogBinding.root)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.open) { _, _ ->
onPasswordSubmitted(volume, position, dialogBinding)
}
.create()
dialogBinding.editPassword.apply {
setOnEditorActionListener { _, _, _ ->
dialog.dismiss()
onPasswordSubmitted(volume, position, dialogBinding)
true
}
if (sharedPrefs.getBoolean(ConstValues.PIN_PASSWORDS_KEY, false)) {
inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
}
}
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
dialog.show()
}
private fun openVolumeWithPassword(volume: SavedVolume, position: Int, password: ByteArray, savePasswordHash: Boolean) {
val returnedHash: ObjRef<ByteArray?>? = if (savePasswordHash) {
ObjRef(null)
} else {
null
}
object : LoadingTask<EncryptedVolume?>(this, themeValue, R.string.loading_msg_open) {
override suspend fun doTask(): EncryptedVolume? {
val encryptedVolume = EncryptedVolume.init(volume, filesDir.path, password, null, returnedHash)
Arrays.fill(password, 0)
return encryptedVolume
}
}.startTask(lifecycleScope) { encryptedVolume ->
if (encryptedVolume == null) {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_volume_failed_msg)
.setPositiveButton(R.string.ok) { _, _ ->
askForPassword(volume, position, savePasswordHash)
}
.show()
} else {
val fingerprintProtector = fingerprintProtector
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
if (savePasswordHash && returnedHash != null && fingerprintProtector != null) {
fingerprintProtector.listener = object : FingerprintProtector.Listener {
override fun onHashStorageReset() {
volumeAdapter.refresh()
}
override fun onPasswordHashDecrypted(hash: ByteArray) {}
override fun onPasswordHashSaved() {
Arrays.fill(returnedHash.value!!, 0)
volumeAdapter.onVolumeChanged(position)
startExplorer(encryptedVolume, volume.shortName)
}
private var isClosed = false
override fun onFailed(pending: Boolean) {
if (!isClosed) {
encryptedVolume.close()
isClosed = true
}
Arrays.fill(returnedHash.value!!, 0)
}
}
fingerprintProtector.savePasswordHash(volume, returnedHash.value!!)
} else {
startExplorer(encryptedVolume, volume.shortName)
}
}
}
}
private fun startExplorer(encryptedVolume: EncryptedVolume, volumeShortName: String) {
var explorerIntent: Intent? = null
if (dropMode) { //import via android share menu
explorerIntent = Intent(this, ExplorerActivityDrop::class.java)
explorerIntent.action = intent.action //forward action
explorerIntent.putExtras(intent.extras!!) //forward extras
} else if (pickMode) {
explorerIntent = Intent(this, ExplorerActivityPick::class.java)
explorerIntent.putExtra("destinationVolume", getParcelableExtra<EncryptedVolume>(intent, "volume")!!)
explorerIntent.flags = Intent.FLAG_ACTIVITY_FORWARD_RESULT
}
if (explorerIntent == null) {
explorerIntent = Intent(this, ExplorerActivity::class.java) //default opening
}
explorerIntent.putExtra("volume", encryptedVolume)
explorerIntent.putExtra("volume_name", volumeShortName)
startActivity(explorerIntent)
if (pickMode)
shouldCloseVolume = false
if (dropMode || pickMode)
finish()
} }
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
if (pickMode && !usfKeepOpen) { if (explorerRouter.pickMode && !usfKeepOpen && shouldCloseVolume) {
IntentUtils.getParcelableExtra<EncryptedVolume>(intent, "volume")?.close()
RestrictedFileProvider.wipeAll(this)
finish() finish()
if (shouldCloseVolume) {
getParcelableExtra<EncryptedVolume>(intent, "volume")?.close()
RestrictedFileProvider.wipeAll(this)
}
} }
} }
} }

View File

@ -82,7 +82,7 @@ class SettingsActivity : BaseActivity() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey) setPreferencesFromResource(R.xml.root_preferences, rootKey)
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
findPreference<ListPreference>("theme")?.setOnPreferenceChangeListener { _, newValue -> findPreference<ListPreference>(ConstValues.THEME_VALUE_KEY)?.setOnPreferenceChangeListener { _, newValue ->
(activity as BaseActivity).onThemeChanged(newValue as String) (activity as BaseActivity).onThemeChanged(newValue as String)
true true
} }

View File

@ -5,7 +5,7 @@ import android.os.Parcelable
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import java.io.File import java.io.File
class SavedVolume(val name: String, val isHidden: Boolean = false, val type: Byte, var encryptedHash: ByteArray? = null, var iv: ByteArray? = null): Parcelable { class VolumeData(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()!!,
@ -48,9 +48,9 @@ class SavedVolume(val name: String, val isHidden: Boolean = false, val type: Byt
const val VOLUMES_DIRECTORY = "volumes" const val VOLUMES_DIRECTORY = "volumes"
@JvmField @JvmField
val CREATOR = object : Parcelable.Creator<SavedVolume> { val CREATOR = object : Parcelable.Creator<VolumeData> {
override fun createFromParcel(parcel: Parcel) = SavedVolume(parcel) override fun createFromParcel(parcel: Parcel) = VolumeData(parcel)
override fun newArray(size: Int) = arrayOfNulls<SavedVolume>(size) override fun newArray(size: Int) = arrayOfNulls<VolumeData>(size)
} }
fun getHiddenVolumeFullPath(filesDir: String, name: String): String { fun getHiddenVolumeFullPath(filesDir: String, name: String): String {

View File

@ -2,6 +2,7 @@ package sushi.hardcore.droidfs
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper import android.database.sqlite.SQLiteOpenHelper
import android.util.Log import android.util.Log
@ -18,7 +19,7 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
const val COLUMN_HASH = "hash" const val COLUMN_HASH = "hash"
const val COLUMN_IV = "iv" const val COLUMN_IV = "iv"
private fun contentValuesFromVolume(volume: SavedVolume): ContentValues { private fun contentValuesFromVolume(volume: VolumeData): ContentValues {
val contentValues = ContentValues() val contentValues = ContentValues()
contentValues.put(COLUMN_NAME, volume.name) contentValues.put(COLUMN_NAME, volume.name)
contentValues.put(COLUMN_HIDDEN, volume.isHidden) contentValues.put(COLUMN_HIDDEN, volume.isHidden)
@ -38,7 +39,7 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
"$COLUMN_IV BLOB" + "$COLUMN_IV BLOB" +
");" ");"
) )
File(context.filesDir, SavedVolume.VOLUMES_DIRECTORY).mkdir() File(context.filesDir, VolumeData.VOLUMES_DIRECTORY).mkdir()
} }
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@ -49,7 +50,7 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
}, null, null) }, null, null)
// Moving hidden volumes to the "volumes" directory // Moving hidden volumes to the "volumes" directory
if (File(context.filesDir, SavedVolume.VOLUMES_DIRECTORY).mkdir()) { if (File(context.filesDir, VolumeData.VOLUMES_DIRECTORY).mkdir()) {
val cursor = db.query( val cursor = db.query(
TABLE_NAME, TABLE_NAME,
arrayOf(COLUMN_NAME), arrayOf(COLUMN_NAME),
@ -68,7 +69,7 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
) )
).renameTo( ).renameTo(
File( File(
SavedVolume( VolumeData(
volumeName, volumeName,
true, true,
EncryptedVolume.GOCRYPTFS_VOLUME_TYPE EncryptedVolume.GOCRYPTFS_VOLUME_TYPE
@ -82,37 +83,55 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
} }
} }
fun isVolumeSaved(volumeName: String, isHidden: Boolean): Boolean { private fun extractVolumeData(cursor: Cursor): VolumeData {
val cursor = readableDatabase.query(TABLE_NAME, return VolumeData(
arrayOf(COLUMN_NAME), "$COLUMN_NAME=? AND $COLUMN_HIDDEN=?", cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)),
cursor.getShort(cursor.getColumnIndexOrThrow(COLUMN_HIDDEN)) == 1.toShort(),
cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_TYPE))[0],
cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_HASH)),
cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_IV))
)
}
private fun getVolumeCursor(volumeName: String, isHidden: Boolean): Cursor {
return readableDatabase.query(
TABLE_NAME, null,
"$COLUMN_NAME=? AND $COLUMN_HIDDEN=?",
arrayOf(volumeName, (if (isHidden) 1 else 0).toString()), arrayOf(volumeName, (if (isHidden) 1 else 0).toString()),
null, null, null null, null, null
) )
}
fun getVolume(volumeName: String, isHidden: Boolean): VolumeData? {
val cursor = getVolumeCursor(volumeName, isHidden)
val volumeData = if (cursor.moveToNext()) {
extractVolumeData(cursor)
} else {
null
}
cursor.close()
return volumeData
}
fun isVolumeSaved(volumeName: String, isHidden: Boolean): Boolean {
val cursor = getVolumeCursor(volumeName, isHidden)
val result = cursor.count > 0 val result = cursor.count > 0
cursor.close() cursor.close()
return result return result
} }
fun saveVolume(volume: SavedVolume): 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, contentValuesFromVolume(volume)) >= 0.toLong())
} }
return false return false
} }
fun getVolumes(): List<SavedVolume> { fun getVolumes(): List<VolumeData> {
val list: MutableList<SavedVolume> = ArrayList() val list: MutableList<VolumeData> = ArrayList()
val cursor = readableDatabase.rawQuery("SELECT * FROM $TABLE_NAME", null) val cursor = readableDatabase.rawQuery("SELECT * FROM $TABLE_NAME", null)
while (cursor.moveToNext()){ while (cursor.moveToNext()){
list.add( list.add(extractVolumeData(cursor))
SavedVolume(
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)),
cursor.getShort(cursor.getColumnIndexOrThrow(COLUMN_HIDDEN)) == 1.toShort(),
cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_TYPE))[0],
cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_HASH)),
cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_IV))
)
)
} }
cursor.close() cursor.close()
return list return list
@ -130,14 +149,14 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
return isHashSaved return isHashSaved
} }
fun addHash(volume: SavedVolume): Boolean { fun addHash(volume: VolumeData): Boolean {
return writableDatabase.update(TABLE_NAME, contentValuesFromVolume(volume), "$COLUMN_NAME=?", arrayOf(volume.name)) > 0 return writableDatabase.update(TABLE_NAME, contentValuesFromVolume(volume), "$COLUMN_NAME=?", arrayOf(volume.name)) > 0
} }
fun removeHash(volume: SavedVolume): Boolean { fun removeHash(volume: VolumeData): Boolean {
return writableDatabase.update( return writableDatabase.update(
TABLE_NAME, contentValuesFromVolume( TABLE_NAME, contentValuesFromVolume(
SavedVolume( VolumeData(
volume.name, volume.name,
volume.isHidden, volume.isHidden,
volume.type, volume.type,

View File

@ -0,0 +1,201 @@
package sushi.hardcore.droidfs
import android.annotation.SuppressLint
import android.os.Build
import android.text.InputType
import android.view.View
import android.view.WindowManager
import android.widget.Toast
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import sushi.hardcore.droidfs.ConstValues.DEFAULT_VOLUME_KEY
import sushi.hardcore.droidfs.databinding.DialogOpenVolumeBinding
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.ObjRef
import sushi.hardcore.droidfs.util.WidgetUtil
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import java.util.*
class VolumeOpener(
private val activity: FragmentActivity,
) {
interface VolumeOpenerCallbacks {
fun onHashStorageReset() {}
fun onVolumeOpened(encryptedVolume: EncryptedVolume, volumeShortName: String)
}
private val volumeDatabase = VolumeDatabase(activity)
private var fingerprintProtector: FingerprintProtector? = null
private val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(activity)
var themeValue = sharedPrefs.getString(ConstValues.THEME_VALUE_KEY, ConstValues.DEFAULT_THEME_VALUE)!!
var defaultVolumeName: String? = sharedPrefs.getString(DEFAULT_VOLUME_KEY, null)
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fingerprintProtector = FingerprintProtector.new(activity, themeValue, volumeDatabase)
}
}
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
fun openVolume(volume: VolumeData, isVolumeSaved: Boolean, callbacks: VolumeOpenerCallbacks) {
if (volume.type == EncryptedVolume.GOCRYPTFS_VOLUME_TYPE && BuildConfig.GOCRYPTFS_DISABLED) {
Toast.makeText(activity, R.string.gocryptfs_disabled, Toast.LENGTH_SHORT).show()
return
} else if (volume.type == EncryptedVolume.CRYFS_VOLUME_TYPE && BuildConfig.CRYFS_DISABLED) {
Toast.makeText(activity, R.string.cryfs_disabled, Toast.LENGTH_SHORT).show()
return
}
var askForPassword = true
fingerprintProtector?.let { fingerprintProtector ->
volume.encryptedHash?.let { encryptedHash ->
volume.iv?.let { iv ->
askForPassword = false
fingerprintProtector.listener = object : FingerprintProtector.Listener {
override fun onHashStorageReset() {
callbacks.onHashStorageReset()
}
override fun onPasswordHashDecrypted(hash: ByteArray) {
object : LoadingTask<EncryptedVolume?>(activity, themeValue, R.string.loading_msg_open) {
override suspend fun doTask(): EncryptedVolume? {
val encryptedVolume = EncryptedVolume.init(volume, activity.filesDir.path, null, hash, null)
Arrays.fill(hash, 0)
return encryptedVolume
}
}.startTask(activity.lifecycleScope) { encryptedVolume ->
if (encryptedVolume == null) {
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_failed_hash_msg)
.setPositiveButton(R.string.ok, null)
.show()
} else {
callbacks.onVolumeOpened(encryptedVolume, volume.shortName)
}
}
}
override fun onPasswordHashSaved() {}
override fun onFailed(pending: Boolean) {
if (!pending) {
askForPassword(volume, isVolumeSaved, callbacks)
}
}
}
fingerprintProtector.loadPasswordHash(volume.shortName, encryptedHash, iv)
}
}
}
if (askForPassword) {
askForPassword(volume, isVolumeSaved, callbacks)
}
}
private fun onPasswordSubmitted(volume: VolumeData, isVolumeSaved: Boolean, dialogBinding: DialogOpenVolumeBinding, callbacks: VolumeOpenerCallbacks) {
if (dialogBinding.checkboxDefaultOpen.isChecked xor (defaultVolumeName == volume.name)) {
with (sharedPrefs.edit()) {
defaultVolumeName = if (dialogBinding.checkboxDefaultOpen.isChecked) {
putString(DEFAULT_VOLUME_KEY, volume.name)
volume.name
} else {
remove(DEFAULT_VOLUME_KEY)
null
}
apply()
}
}
// openVolumeWithPassword is responsible for wiping the password
openVolumeWithPassword(
volume,
WidgetUtil.encodeEditTextContent(dialogBinding.editPassword),
isVolumeSaved,
dialogBinding.checkboxSavePassword.isChecked,
callbacks,
)
}
private fun askForPassword(volume: VolumeData, isVolumeSaved: Boolean, callbacks: VolumeOpenerCallbacks, savePasswordHash: Boolean = false) {
val dialogBinding = DialogOpenVolumeBinding.inflate(activity.layoutInflater)
if (isVolumeSaved) {
if (!sharedPrefs.getBoolean("usf_fingerprint", false) || fingerprintProtector == null || volume.encryptedHash != null) {
dialogBinding.checkboxSavePassword.visibility = View.GONE
} else {
dialogBinding.checkboxSavePassword.isChecked = savePasswordHash
}
dialogBinding.checkboxDefaultOpen.isChecked = defaultVolumeName == volume.name
} else {
dialogBinding.checkboxSavePassword.visibility = View.GONE
dialogBinding.checkboxDefaultOpen.visibility = View.GONE
}
val dialog = CustomAlertDialogBuilder(activity, themeValue)
.setTitle(activity.getString(R.string.open_dialog_title, volume.shortName))
.setView(dialogBinding.root)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.open) { _, _ ->
onPasswordSubmitted(volume, isVolumeSaved, dialogBinding, callbacks)
}
.create()
dialogBinding.editPassword.apply {
setOnEditorActionListener { _, _, _ ->
dialog.dismiss()
onPasswordSubmitted(volume, isVolumeSaved, dialogBinding, callbacks)
true
}
if (sharedPrefs.getBoolean(ConstValues.PIN_PASSWORDS_KEY, false)) {
inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
}
}
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
dialog.show()
}
private fun openVolumeWithPassword(volume: VolumeData, password: ByteArray, isVolumeSaved: Boolean, savePasswordHash: Boolean, callbacks: VolumeOpenerCallbacks) {
val returnedHash: ObjRef<ByteArray?>? = if (savePasswordHash) {
ObjRef(null)
} else {
null
}
object : LoadingTask<EncryptedVolume?>(activity, themeValue, R.string.loading_msg_open) {
override suspend fun doTask(): EncryptedVolume? {
val encryptedVolume = EncryptedVolume.init(volume, activity.filesDir.path, password, null, returnedHash)
Arrays.fill(password, 0)
return encryptedVolume
}
}.startTask(activity.lifecycleScope) { encryptedVolume ->
if (encryptedVolume == null) {
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_volume_failed_msg)
.setPositiveButton(R.string.ok) { _, _ ->
askForPassword(volume, isVolumeSaved, callbacks, savePasswordHash)
}
.show()
} else {
val fingerprintProtector = fingerprintProtector
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
if (savePasswordHash && returnedHash != null && fingerprintProtector != null) {
fingerprintProtector.listener = object : FingerprintProtector.Listener {
override fun onHashStorageReset() {
callbacks.onHashStorageReset()
}
override fun onPasswordHashDecrypted(hash: ByteArray) {}
override fun onPasswordHashSaved() {
Arrays.fill(returnedHash.value!!, 0)
callbacks.onVolumeOpened(encryptedVolume, volume.shortName)
}
private var isClosed = false
override fun onFailed(pending: Boolean) {
if (!isClosed) {
encryptedVolume.close()
isClosed = true
}
Arrays.fill(returnedHash.value!!, 0)
}
}
fingerprintProtector.savePasswordHash(volume, returnedHash.value!!)
} else {
callbacks.onVolumeOpened(encryptedVolume, volume.shortName)
}
}
}
}
}

View File

@ -10,7 +10,7 @@ import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.SavedVolume import sushi.hardcore.droidfs.VolumeData
import sushi.hardcore.droidfs.VolumeDatabase import sushi.hardcore.droidfs.VolumeDatabase
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
@ -20,9 +20,9 @@ class VolumeAdapter(
private val allowSelection: Boolean, private val allowSelection: Boolean,
private val showReadOnly: Boolean, private val showReadOnly: Boolean,
private val listener: Listener, private val listener: Listener,
) : SelectableAdapter<SavedVolume>(listener::onSelectionChanged) { ) : SelectableAdapter<VolumeData>(listener::onSelectionChanged) {
private val inflater: LayoutInflater = LayoutInflater.from(context) private val inflater: LayoutInflater = LayoutInflater.from(context)
lateinit var volumes: List<SavedVolume> lateinit var volumes: List<VolumeData>
init { init {
reloadVolumes() reloadVolumes()
@ -30,11 +30,11 @@ class VolumeAdapter(
interface Listener { interface Listener {
fun onSelectionChanged(size: Int) fun onSelectionChanged(size: Int)
fun onVolumeItemClick(volume: SavedVolume, position: Int) fun onVolumeItemClick(volume: VolumeData, position: Int)
fun onVolumeItemLongClick() fun onVolumeItemLongClick()
} }
override fun getItems(): List<SavedVolume> { override fun getItems(): List<VolumeData> {
return volumes return volumes
} }

View File

@ -2,31 +2,39 @@ package sushi.hardcore.droidfs.add_volume
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import sushi.hardcore.droidfs.BaseActivity import sushi.hardcore.droidfs.*
import sushi.hardcore.droidfs.ConstValues import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.databinding.ActivityAddVolumeBinding import sushi.hardcore.droidfs.databinding.ActivityAddVolumeBinding
import sushi.hardcore.droidfs.explorers.ExplorerRouter
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils
class AddVolumeActivity: BaseActivity() { class AddVolumeActivity: BaseActivity() {
companion object { companion object {
const val RESULT_VOLUME_ADDED = 1 const val RESULT_USER_BACK = 10
const val RESULT_HASH_STORAGE_RESET = 2
} }
private lateinit var binding: ActivityAddVolumeBinding private lateinit var binding: ActivityAddVolumeBinding
private lateinit var explorerRouter: ExplorerRouter
private lateinit var volumeOpener: VolumeOpener
private var usfKeepOpen = false
var shouldCloseVolume = true // used when launched to pick file from another volume
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityAddVolumeBinding.inflate(layoutInflater) binding = ActivityAddVolumeBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
usfKeepOpen = sharedPrefs.getBoolean("usf_keep_open", false)
explorerRouter = ExplorerRouter(this, intent)
volumeOpener = VolumeOpener(this)
if (savedInstanceState == null) { if (savedInstanceState == null) {
supportFragmentManager supportFragmentManager
.beginTransaction() .beginTransaction()
.add( .add(
R.id.fragment_container, R.id.fragment_container,
SelectPathFragment.newInstance(themeValue), SelectPathFragment.newInstance(themeValue, explorerRouter.pickMode),
) )
.commit() .commit()
} }
@ -36,12 +44,21 @@ class AddVolumeActivity: BaseActivity() {
if (item.itemId == android.R.id.home) { if (item.itemId == android.R.id.home) {
if (supportFragmentManager.backStackEntryCount > 0) if (supportFragmentManager.backStackEntryCount > 0)
supportFragmentManager.popBackStack() supportFragmentManager.popBackStack()
else else {
setResult(RESULT_USER_BACK)
shouldCloseVolume = false
finish() finish()
}
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
override fun onBackPressed() {
setResult(RESULT_USER_BACK)
shouldCloseVolume = false
super.onBackPressed()
}
fun onFragmentLoaded(selectPathFragment: Boolean) { fun onFragmentLoaded(selectPathFragment: Boolean) {
title = getString( title = getString(
if (selectPathFragment) { if (selectPathFragment) {
@ -52,16 +69,27 @@ class AddVolumeActivity: BaseActivity() {
) )
} }
fun onSelectedAlreadySavedVolume() { fun startExplorer(encryptedVolume: EncryptedVolume, volumeShortName: String) {
startActivity(explorerRouter.getExplorerIntent(encryptedVolume, volumeShortName))
shouldCloseVolume = false
finish() finish()
} }
fun onVolumeAdded(hashStorageReset: Boolean) { fun onVolumeSelected(volume: VolumeData, rememberVolume: Boolean) {
setResult(if (hashStorageReset) RESULT_HASH_STORAGE_RESET else RESULT_VOLUME_ADDED) if (rememberVolume) {
finish() setResult(RESULT_USER_BACK)
shouldCloseVolume = false
finish()
} else {
volumeOpener.openVolume(volume, false, object : VolumeOpener.VolumeOpenerCallbacks {
override fun onVolumeOpened(encryptedVolume: EncryptedVolume, volumeShortName: String) {
startExplorer(encryptedVolume, volumeShortName)
}
})
}
} }
fun createVolume(volumePath: String, isHidden: Boolean) { fun createVolume(volumePath: String, isHidden: Boolean, rememberVolume: Boolean) {
supportFragmentManager supportFragmentManager
.beginTransaction() .beginTransaction()
.replace( .replace(
@ -69,6 +97,7 @@ class AddVolumeActivity: BaseActivity() {
themeValue, themeValue,
volumePath, volumePath,
isHidden, isHidden,
rememberVolume,
sharedPrefs.getBoolean(ConstValues.PIN_PASSWORDS_KEY, false), sharedPrefs.getBoolean(ConstValues.PIN_PASSWORDS_KEY, false),
sharedPrefs.getBoolean("usf_fingerprint", false), sharedPrefs.getBoolean("usf_fingerprint", false),
) )
@ -76,4 +105,18 @@ class AddVolumeActivity: BaseActivity() {
.addToBackStack(null) .addToBackStack(null)
.commit() .commit()
} }
override fun onStart() {
super.onStart()
shouldCloseVolume = true
}
override fun onStop() {
super.onStop()
if (explorerRouter.pickMode && !usfKeepOpen && shouldCloseVolume) {
IntentUtils.getParcelableExtra<EncryptedVolume>(intent, "volume")?.close()
RestrictedFileProvider.wipeAll(this)
finish()
}
}
} }

View File

@ -23,13 +23,13 @@ import sushi.hardcore.droidfs.util.WidgetUtil
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import java.io.File import java.io.File
import java.util.* import java.util.*
import kotlin.collections.ArrayList
class CreateVolumeFragment: Fragment() { class CreateVolumeFragment: Fragment() {
companion object { companion object {
private const val KEY_THEME_VALUE = "theme" private const val KEY_THEME_VALUE = "theme"
private const val KEY_VOLUME_PATH = "path" private const val KEY_VOLUME_PATH = "path"
private const val KEY_IS_HIDDEN = "hidden" private const val KEY_IS_HIDDEN = "hidden"
private const val KEY_REMEMBER_VOLUME = "remember"
private const val KEY_PIN_PASSWORDS = ConstValues.PIN_PASSWORDS_KEY private const val KEY_PIN_PASSWORDS = ConstValues.PIN_PASSWORDS_KEY
private const val KEY_USF_FINGERPRINT = "fingerprint" private const val KEY_USF_FINGERPRINT = "fingerprint"
@ -37,6 +37,7 @@ class CreateVolumeFragment: Fragment() {
themeValue: String, themeValue: String,
volumePath: String, volumePath: String,
isHidden: Boolean, isHidden: Boolean,
rememberVolume: Boolean,
pinPasswords: Boolean, pinPasswords: Boolean,
usfFingerprint: Boolean, usfFingerprint: Boolean,
): CreateVolumeFragment { ): CreateVolumeFragment {
@ -45,6 +46,7 @@ class CreateVolumeFragment: Fragment() {
putString(KEY_THEME_VALUE, themeValue) putString(KEY_THEME_VALUE, themeValue)
putString(KEY_VOLUME_PATH, volumePath) putString(KEY_VOLUME_PATH, volumePath)
putBoolean(KEY_IS_HIDDEN, isHidden) putBoolean(KEY_IS_HIDDEN, isHidden)
putBoolean(KEY_REMEMBER_VOLUME, rememberVolume)
putBoolean(KEY_PIN_PASSWORDS, pinPasswords) putBoolean(KEY_PIN_PASSWORDS, pinPasswords)
putBoolean(KEY_USF_FINGERPRINT, usfFingerprint) putBoolean(KEY_USF_FINGERPRINT, usfFingerprint)
} }
@ -57,6 +59,7 @@ class CreateVolumeFragment: Fragment() {
private val volumeTypes = ArrayList<String>(2) private val volumeTypes = ArrayList<String>(2)
private lateinit var volumePath: String private lateinit var volumePath: String
private var isHiddenVolume: Boolean = false private var isHiddenVolume: Boolean = false
private var rememberVolume: Boolean = false
private var usfFingerprint: Boolean = false private var usfFingerprint: Boolean = false
private lateinit var volumeDatabase: VolumeDatabase private lateinit var volumeDatabase: VolumeDatabase
private var fingerprintProtector: FingerprintProtector? = null private var fingerprintProtector: FingerprintProtector? = null
@ -76,6 +79,7 @@ class CreateVolumeFragment: Fragment() {
arguments.getString(KEY_THEME_VALUE)?.let { themeValue = it } arguments.getString(KEY_THEME_VALUE)?.let { themeValue = it }
volumePath = arguments.getString(KEY_VOLUME_PATH)!! volumePath = arguments.getString(KEY_VOLUME_PATH)!!
isHiddenVolume = arguments.getBoolean(KEY_IS_HIDDEN) isHiddenVolume = arguments.getBoolean(KEY_IS_HIDDEN)
rememberVolume = arguments.getBoolean(KEY_REMEMBER_VOLUME)
usfFingerprint = arguments.getBoolean(KEY_USF_FINGERPRINT) usfFingerprint = arguments.getBoolean(KEY_USF_FINGERPRINT)
arguments.getBoolean(KEY_PIN_PASSWORDS) arguments.getBoolean(KEY_PIN_PASSWORDS)
} }
@ -83,7 +87,7 @@ class CreateVolumeFragment: Fragment() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fingerprintProtector = FingerprintProtector.new(requireActivity(), themeValue, volumeDatabase) fingerprintProtector = FingerprintProtector.new(requireActivity(), themeValue, volumeDatabase)
} }
if (!usfFingerprint || fingerprintProtector == null) { if (!rememberVolume || !usfFingerprint || fingerprintProtector == null) {
binding.checkboxSavePassword.visibility = View.GONE binding.checkboxSavePassword.visibility = View.GONE
} }
if (!BuildConfig.GOCRYPTFS_DISABLED) { if (!BuildConfig.GOCRYPTFS_DISABLED) {
@ -140,21 +144,6 @@ class CreateVolumeFragment: Fragment() {
(activity as AddVolumeActivity).onFragmentLoaded(false) (activity as AddVolumeActivity).onFragmentLoaded(false)
} }
private fun saveVolume(success: Boolean, volumeType: Byte): SavedVolume? {
return if (success) {
val volumeName = if (isHiddenVolume) File(volumePath).name else volumePath
val volume = SavedVolume(volumeName, isHiddenVolume, volumeType)
volumeDatabase.apply {
if (isVolumeSaved(volumeName, isHiddenVolume)) // cleaning old saved path
removeVolume(volumeName)
saveVolume(volume)
}
volume
} else {
null
}
}
private fun createVolume() { private fun createVolume() {
val password = WidgetUtil.encodeEditTextContent(binding.editPassword) val password = WidgetUtil.encodeEditTextContent(binding.editPassword)
val passwordConfirm = WidgetUtil.encodeEditTextContent(binding.editPasswordConfirm) val passwordConfirm = WidgetUtil.encodeEditTextContent(binding.editPasswordConfirm)
@ -169,50 +158,73 @@ class CreateVolumeFragment: Fragment() {
} else { } else {
null null
} }
object: LoadingTask<SavedVolume?>(requireActivity() as AppCompatActivity, themeValue, R.string.loading_msg_create) { val encryptedVolume = if (rememberVolume) {
override suspend fun doTask(): SavedVolume? { null
} else {
ObjRef<EncryptedVolume?>(null)
}
object: LoadingTask<Byte>(requireActivity() as AppCompatActivity, themeValue, R.string.loading_msg_create) {
private fun generateResult(success: Boolean, volumeType: Byte): Byte {
return if (success) {
volumeType
} else {
-1
}
}
override suspend fun doTask(): Byte {
val volumeFile = File(volumePath) val volumeFile = File(volumePath)
if (!volumeFile.exists()) if (!volumeFile.exists())
volumeFile.mkdirs() volumeFile.mkdirs()
val volume = if (volumeTypes[binding.spinnerVolumeType.selectedItemPosition] == resources.getString(R.string.gocryptfs)) { val result = if (volumeTypes[binding.spinnerVolumeType.selectedItemPosition] == resources.getString(R.string.gocryptfs)) {
val xchacha = when (binding.spinnerCipher.selectedItemPosition) { val xchacha = when (binding.spinnerCipher.selectedItemPosition) {
0 -> 0 0 -> 0
1 -> 1 1 -> 1
else -> -1 else -> -1
} }
saveVolume(GocryptfsVolume.createVolume( generateResult(GocryptfsVolume.createAndOpenVolume(
volumePath, volumePath,
password, password,
false, false,
xchacha, xchacha,
GocryptfsVolume.ScryptDefaultLogN,
ConstValues.CREATOR,
returnedHash?.apply { returnedHash?.apply {
value = ByteArray(GocryptfsVolume.KeyLen) value = ByteArray(GocryptfsVolume.KeyLen)
}?.value, }?.value,
encryptedVolume,
), EncryptedVolume.GOCRYPTFS_VOLUME_TYPE) ), EncryptedVolume.GOCRYPTFS_VOLUME_TYPE)
} else { } else {
saveVolume(CryfsVolume.create( generateResult(CryfsVolume.create(
volumePath, volumePath,
CryfsVolume.getLocalStateDir(activity.filesDir.path), CryfsVolume.getLocalStateDir(activity.filesDir.path),
password, password,
returnedHash, returnedHash,
resources.getStringArray(R.array.cryfs_encryption_ciphers)[binding.spinnerCipher.selectedItemPosition] resources.getStringArray(R.array.cryfs_encryption_ciphers)[binding.spinnerCipher.selectedItemPosition],
encryptedVolume,
), EncryptedVolume.CRYFS_VOLUME_TYPE) ), EncryptedVolume.CRYFS_VOLUME_TYPE)
} }
Arrays.fill(password, 0) Arrays.fill(password, 0)
return volume return result
} }
}.startTask(lifecycleScope) { volume -> }.startTask(lifecycleScope) { result ->
if (volume == null) { if (result.compareTo(-1) == 0) {
CustomAlertDialogBuilder(requireContext(), themeValue) CustomAlertDialogBuilder(requireContext(), themeValue)
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(R.string.create_volume_failed) .setMessage(R.string.create_volume_failed)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} else { } else {
val volumeName = if (isHiddenVolume) File(volumePath).name else volumePath
val volume = VolumeData(volumeName, isHiddenVolume, result)
var isVolumeSaved = false
volumeDatabase.apply {
if (isVolumeSaved(volumeName, isHiddenVolume)) // cleaning old saved path
removeVolume(volumeName)
if (rememberVolume) {
isVolumeSaved = saveVolume(volume)
}
}
@SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden @SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
if (binding.checkboxSavePassword.isChecked && returnedHash != null) { if (isVolumeSaved && binding.checkboxSavePassword.isChecked && returnedHash != null) {
fingerprintProtector!!.let { fingerprintProtector!!.let {
it.listener = object : FingerprintProtector.Listener { it.listener = object : FingerprintProtector.Listener {
override fun onHashStorageReset() { override fun onHashStorageReset() {
@ -223,24 +235,32 @@ class CreateVolumeFragment: Fragment() {
override fun onPasswordHashDecrypted(hash: ByteArray) {} // shouldn't happen here override fun onPasswordHashDecrypted(hash: ByteArray) {} // shouldn't happen here
override fun onPasswordHashSaved() { override fun onPasswordHashSaved() {
Arrays.fill(returnedHash.value!!, 0) Arrays.fill(returnedHash.value!!, 0)
onVolumeCreated() onVolumeCreated(encryptedVolume?.value, volume.shortName)
} }
override fun onFailed(pending: Boolean) { override fun onFailed(pending: Boolean) {
if (!pending) { if (!pending) {
Arrays.fill(returnedHash.value!!, 0) Arrays.fill(returnedHash.value!!, 0)
onVolumeCreated() onVolumeCreated(encryptedVolume?.value, volume.shortName)
} }
} }
} }
it.savePasswordHash(volume, returnedHash.value!!) it.savePasswordHash(volume, returnedHash.value!!)
} }
} else onVolumeCreated() } else {
onVolumeCreated(encryptedVolume?.value, volume.shortName)
}
} }
} }
} }
} }
private fun onVolumeCreated() { private fun onVolumeCreated(encryptedVolume: EncryptedVolume?, volumeShortName: String) {
(activity as AddVolumeActivity).onVolumeAdded(hashStorageReset) (activity as AddVolumeActivity).apply {
if (rememberVolume || encryptedVolume == null) {
finish()
} else {
startExplorer(encryptedVolume, volumeShortName)
}
}
} }
} }

View File

@ -2,6 +2,7 @@ package sushi.hardcore.droidfs.add_volume
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -15,7 +16,11 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import sushi.hardcore.droidfs.* import androidx.preference.PreferenceManager
import sushi.hardcore.droidfs.ConstValues
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.VolumeData
import sushi.hardcore.droidfs.VolumeDatabase
import sushi.hardcore.droidfs.databinding.DialogSdcardErrorBinding import sushi.hardcore.droidfs.databinding.DialogSdcardErrorBinding
import sushi.hardcore.droidfs.databinding.FragmentSelectPathBinding import sushi.hardcore.droidfs.databinding.FragmentSelectPathBinding
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
@ -26,11 +31,13 @@ import java.io.File
class SelectPathFragment: Fragment() { class SelectPathFragment: Fragment() {
companion object { companion object {
private const val KEY_THEME_VALUE = "theme" private const val KEY_THEME_VALUE = "theme"
private const val KEY_PICK_MODE = "pick"
fun newInstance(themeValue: String): SelectPathFragment { fun newInstance(themeValue: String, pickMode: Boolean): SelectPathFragment {
return SelectPathFragment().apply { return SelectPathFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putString(KEY_THEME_VALUE, themeValue) putString(KEY_THEME_VALUE, themeValue)
putBoolean(KEY_PICK_MODE, pickMode)
} }
} }
} }
@ -39,7 +46,7 @@ class SelectPathFragment: Fragment() {
private lateinit var binding: FragmentSelectPathBinding private lateinit var binding: FragmentSelectPathBinding
private val askStoragePermissions = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> private val askStoragePermissions = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
if (result[Manifest.permission.READ_EXTERNAL_STORAGE] == true && result[Manifest.permission.WRITE_EXTERNAL_STORAGE] == true) if (result[Manifest.permission.READ_EXTERNAL_STORAGE] == true && result[Manifest.permission.WRITE_EXTERNAL_STORAGE] == true)
PathUtils.safePickDirectory(pickDirectory, requireContext(), themeValue) launchPickDirectory()
else else
CustomAlertDialogBuilder(requireContext(), themeValue) CustomAlertDialogBuilder(requireContext(), themeValue)
.setTitle(R.string.storage_perm_denied) .setTitle(R.string.storage_perm_denied)
@ -54,6 +61,12 @@ class SelectPathFragment: Fragment() {
} }
private var themeValue = ConstValues.DEFAULT_THEME_VALUE private var themeValue = ConstValues.DEFAULT_THEME_VALUE
private lateinit var volumeDatabase: VolumeDatabase private lateinit var volumeDatabase: VolumeDatabase
private lateinit var filesDir: String
private lateinit var sharedPrefs: SharedPreferences
private var pickMode = false
private var originalRememberVolume = true
private var currentVolumeData: VolumeData? = null
private var volumeAction: Action? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -65,15 +78,24 @@ class SelectPathFragment: Fragment() {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
originalRememberVolume = sharedPrefs.getBoolean(ConstValues.REMEMBER_VOLUME_KEY, true)
binding.switchRemember.isChecked = originalRememberVolume
arguments?.let { arguments -> arguments?.let { arguments ->
arguments.getString(KEY_THEME_VALUE)?.let { themeValue = it } arguments.getString(KEY_THEME_VALUE)?.let { themeValue = it }
pickMode = arguments.getBoolean(KEY_PICK_MODE)
}
if (pickMode) {
binding.buttonAction.text = getString(R.string.add_volume)
} }
volumeDatabase = VolumeDatabase(requireContext()) volumeDatabase = VolumeDatabase(requireContext())
filesDir = requireContext().filesDir.path
binding.containerHiddenVolume.setOnClickListener { binding.containerHiddenVolume.setOnClickListener {
binding.switchHiddenVolume.performClick() binding.switchHiddenVolume.performClick()
} }
binding.switchHiddenVolume.setOnClickListener { binding.switchHiddenVolume.setOnClickListener {
showRightSection() showRightSection()
refreshStatus(binding.editVolumeName.text)
} }
binding.buttonPickDirectory.setOnClickListener { binding.buttonPickDirectory.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@ -86,7 +108,7 @@ class SelectPathFragment: Fragment() {
Manifest.permission.WRITE_EXTERNAL_STORAGE Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED ) == PackageManager.PERMISSION_GRANTED
) )
PathUtils.safePickDirectory(pickDirectory, requireContext(), themeValue) launchPickDirectory()
else else
askStoragePermissions.launch( askStoragePermissions.launch(
arrayOf( arrayOf(
@ -95,35 +117,18 @@ class SelectPathFragment: Fragment() {
) )
) )
} else } else
PathUtils.safePickDirectory(pickDirectory, requireContext(), themeValue) launchPickDirectory()
} }
var isVolumeAlreadySaved = false
var volumeAction: Action? = null
binding.editVolumeName.addTextChangedListener(object: TextWatcher { binding.editVolumeName.addTextChangedListener(object: TextWatcher {
override fun afterTextChanged(s: Editable?) {} override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
isVolumeAlreadySaved = volumeDatabase.isVolumeSaved(s.toString(), binding.switchHiddenVolume.isChecked) refreshStatus(s)
if (isVolumeAlreadySaved)
binding.textWarning.apply {
text = getString(R.string.volume_alread_saved)
visibility = View.VISIBLE
}
else
binding.textWarning.visibility = View.GONE
val path = File(getCurrentVolumePath())
volumeAction = if (path.isDirectory)
if (path.list()?.isEmpty() == true) Action.CREATE else Action.ADD
else
Action.CREATE
binding.buttonAction.text = getString(when (volumeAction) {
Action.CREATE -> R.string.create
else -> R.string.add_volume
})
} }
}) })
binding.editVolumeName.setOnEditorActionListener { _, _, _ -> onPathSelected(isVolumeAlreadySaved, volumeAction); true } binding.switchRemember.setOnCheckedChangeListener { _, _ -> refreshButtonText() }
binding.buttonAction.setOnClickListener { onPathSelected(isVolumeAlreadySaved, volumeAction) } binding.editVolumeName.setOnEditorActionListener { _, _, _ -> onPathSelected(); true }
binding.buttonAction.setOnClickListener { onPathSelected() }
} }
override fun onViewStateRestored(savedInstanceState: Bundle?) { override fun onViewStateRestored(savedInstanceState: Bundle?) {
@ -132,6 +137,11 @@ class SelectPathFragment: Fragment() {
showRightSection() showRightSection()
} }
private fun launchPickDirectory() {
(activity as AddVolumeActivity).shouldCloseVolume = false
PathUtils.safePickDirectory(pickDirectory, requireContext(), themeValue)
}
private fun showRightSection() { private fun showRightSection() {
if (binding.switchHiddenVolume.isChecked) { if (binding.switchHiddenVolume.isChecked) {
binding.textLabel.text = requireContext().getString(R.string.volume_name_label) binding.textLabel.text = requireContext().getString(R.string.volume_name_label)
@ -144,6 +154,48 @@ class SelectPathFragment: Fragment() {
} }
} }
private fun refreshButtonText() {
binding.buttonAction.text = getString(
if (pickMode || volumeAction == Action.ADD) {
if (binding.switchRemember.isChecked || currentVolumeData != null) {
R.string.add_volume
} else {
R.string.open_volume
}
} else {
R.string.create_volume
}
)
}
private fun refreshStatus(content: CharSequence) {
val path = File(getCurrentVolumePath())
volumeAction = if (path.isDirectory) {
if (path.list()?.isEmpty() == true || content.isEmpty()) Action.CREATE else Action.ADD
} else {
Action.CREATE
}
currentVolumeData = if (volumeAction == Action.CREATE) {
null
} else {
volumeDatabase.getVolume(content.toString(), binding.switchHiddenVolume.isChecked)
}
binding.textWarning.visibility = if (volumeAction == Action.CREATE && pickMode) {
binding.textWarning.text = getString(R.string.choose_existing_volume)
binding.buttonAction.isEnabled = false
View.VISIBLE
} else {
refreshButtonText()
binding.buttonAction.isEnabled = true
if (currentVolumeData == null) {
View.GONE
} else {
binding.textWarning.text = getString(R.string.volume_alread_saved)
View.VISIBLE
}
}
}
private fun onDirectoryPicked(uri: Uri) { private fun onDirectoryPicked(uri: Uri) {
val path = PathUtils.getFullPathFromTreeUri(uri, requireContext()) val path = PathUtils.getFullPathFromTreeUri(uri, requireContext())
if (path != null) if (path != null)
@ -158,95 +210,105 @@ class SelectPathFragment: Fragment() {
private fun getCurrentVolumePath(): String { private fun getCurrentVolumePath(): String {
return if (binding.switchHiddenVolume.isChecked) return if (binding.switchHiddenVolume.isChecked)
SavedVolume.getHiddenVolumeFullPath(requireContext().filesDir.path, binding.editVolumeName.text.toString()) VolumeData.getHiddenVolumeFullPath(filesDir, binding.editVolumeName.text.toString())
else else
binding.editVolumeName.text.toString() binding.editVolumeName.text.toString()
} }
private fun onPathSelected(isVolumeAlreadySaved: Boolean, volumeAction: Action?) { private fun onPathSelected() {
if (isVolumeAlreadySaved) { if (binding.switchRemember.isChecked != originalRememberVolume) {
(activity as AddVolumeActivity).onSelectedAlreadySavedVolume() with(sharedPrefs.edit()) {
} else { putBoolean(ConstValues.REMEMBER_VOLUME_KEY, binding.switchRemember.isChecked)
if (binding.switchHiddenVolume.isChecked && volumeAction == Action.CREATE) { apply()
}
}
if (currentVolumeData == null) { // volume not known
val currentVolumeValue = binding.editVolumeName.text.toString()
val isHidden = binding.switchHiddenVolume.isChecked
if (currentVolumeValue.isEmpty()) {
Toast.makeText(
requireContext(),
if (isHidden) R.string.enter_volume_name else R.string.enter_volume_path,
Toast.LENGTH_SHORT
).show()
} else if (isHidden && currentVolumeValue.contains(PathUtils.SEPARATOR)) {
Toast.makeText(requireContext(), R.string.error_slash_in_name, Toast.LENGTH_SHORT).show()
} else if (isHidden && volumeAction == Action.CREATE) {
CustomAlertDialogBuilder(requireContext(), themeValue) CustomAlertDialogBuilder(requireContext(), themeValue)
.setTitle(R.string.warning) .setTitle(R.string.warning)
.setMessage(R.string.hidden_volume_warning) .setMessage(R.string.hidden_volume_warning)
.setPositiveButton(R.string.ok) { _, _ -> .setPositiveButton(R.string.ok) { _, _ ->
addVolume(volumeAction) onNewVolumeSelected(currentVolumeValue, isHidden)
} }
.show() .show()
} else { } else {
addVolume(volumeAction) onNewVolumeSelected(currentVolumeValue, isHidden)
} }
} else {
(activity as AddVolumeActivity).onVolumeSelected(currentVolumeData!!, true)
} }
} }
private fun addVolume(volumeAction: Action?) { private fun onNewVolumeSelected(currentVolumeValue: String, isHidden: Boolean) {
val currentVolumeValue = binding.editVolumeName.text.toString() val volumePath = getCurrentVolumePath()
val isHidden = binding.switchHiddenVolume.isChecked when (volumeAction!!) {
if (currentVolumeValue.isEmpty()) { Action.CREATE -> {
Toast.makeText( val volumeFile = File(volumePath)
requireContext(), var goodDirectory = false
if (isHidden) R.string.enter_volume_name else R.string.enter_volume_path, if (volumeFile.isFile) {
Toast.LENGTH_SHORT Toast.makeText(requireContext(), R.string.error_is_file, Toast.LENGTH_SHORT).show()
).show() } else if (volumeFile.isDirectory) {
} else if (isHidden && currentVolumeValue.contains(PathUtils.SEPARATOR)) { val dirContent = volumeFile.list()
Toast.makeText(requireContext(), R.string.error_slash_in_name, Toast.LENGTH_SHORT).show() if (dirContent != null) {
} else { if (dirContent.isEmpty()) {
val volumePath = getCurrentVolumePath() if (volumeFile.canWrite()) {
when (volumeAction!!) { goodDirectory = true
Action.CREATE -> { } else {
val volumeFile = File(volumePath) errorDirectoryNotWritable(volumePath)
var goodDirectory = false }
if (volumeFile.isFile) { } else {
Toast.makeText(requireContext(), R.string.error_is_file, Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), R.string.dir_not_empty, Toast.LENGTH_SHORT).show()
} else if (volumeFile.isDirectory) { }
val dirContent = volumeFile.list()
if (dirContent != null) {
if (dirContent.isEmpty()) {
if (volumeFile.canWrite())
goodDirectory = true
else
errorDirectoryNotWritable(volumePath)
} else
Toast.makeText(requireContext(), R.string.dir_not_empty, Toast.LENGTH_SHORT).show()
} else
Toast.makeText(requireContext(), R.string.listdir_null_error_msg, Toast.LENGTH_SHORT).show()
} else { } else {
if (File(PathUtils.getParentPath(volumePath)).canWrite()) Toast.makeText(requireContext(), R.string.listdir_null_error_msg, Toast.LENGTH_SHORT).show()
goodDirectory = true }
else } else {
errorDirectoryNotWritable(volumePath) if (File(PathUtils.getParentPath(volumePath)).canWrite()) {
goodDirectory = true
} else {
errorDirectoryNotWritable(volumePath)
} }
if (goodDirectory)
(activity as AddVolumeActivity).createVolume(volumePath, isHidden)
} }
Action.ADD -> { if (goodDirectory) {
val volumeType = EncryptedVolume.getVolumeType(volumePath) (activity as AddVolumeActivity).createVolume(volumePath, isHidden, binding.switchRemember.isChecked)
if (volumeType < 0) { }
CustomAlertDialogBuilder(requireContext(), themeValue) }
.setTitle(R.string.error) Action.ADD -> {
.setMessage(R.string.error_not_a_volume) val volumeType = EncryptedVolume.getVolumeType(volumePath)
.setPositiveButton(R.string.ok, null) if (volumeType < 0) {
.show() CustomAlertDialogBuilder(requireContext(), themeValue)
} else if (!File(volumePath).canWrite()) { .setTitle(R.string.error)
val dialog = CustomAlertDialogBuilder(requireContext(), themeValue) .setMessage(R.string.error_not_a_volume)
.setTitle(R.string.warning) .setPositiveButton(R.string.ok, null)
.setCancelable(false) .show()
.setPositiveButton(R.string.ok) { _, _ -> addVolume(if (isHidden) currentVolumeValue else volumePath, isHidden, volumeType) } } else if (!File(volumePath).canWrite()) {
if (PathUtils.isPathOnExternalStorage(volumePath, requireContext())) val dialog = CustomAlertDialogBuilder(requireContext(), themeValue)
dialog.setView( .setTitle(R.string.warning)
DialogSdcardErrorBinding.inflate(layoutInflater).apply { .setCancelable(false)
path.text = PathUtils.getPackageDataFolder(requireContext()) .setPositiveButton(R.string.ok) { _, _ -> addVolume(if (isHidden) currentVolumeValue else volumePath, isHidden, volumeType) }
footer.text = getString(R.string.sdcard_error_add_footer) if (PathUtils.isPathOnExternalStorage(volumePath, requireContext())) {
}.root dialog.setView(
) DialogSdcardErrorBinding.inflate(layoutInflater).apply {
else path.text = PathUtils.getPackageDataFolder(requireContext())
dialog.setMessage(R.string.add_cant_write_warning) footer.text = getString(R.string.sdcard_error_add_footer)
dialog.show() }.root
)
} else { } else {
addVolume(if (isHidden) currentVolumeValue else volumePath, isHidden, volumeType) dialog.setMessage(R.string.add_cant_write_warning)
} }
dialog.show()
} else {
addVolume(if (isHidden) currentVolumeValue else volumePath, isHidden, volumeType)
} }
} }
} }
@ -270,7 +332,10 @@ class SelectPathFragment: Fragment() {
} }
private fun addVolume(volumeName: String, isHidden: Boolean, volumeType: Byte) { private fun addVolume(volumeName: String, isHidden: Boolean, volumeType: Byte) {
volumeDatabase.saveVolume(SavedVolume(volumeName, isHidden, volumeType)) val volumeData = VolumeData(volumeName, isHidden, volumeType)
(activity as AddVolumeActivity).onVolumeAdded(false) if (binding.switchRemember.isChecked) {
volumeDatabase.saveVolume(volumeData)
}
(activity as AddVolumeActivity).onVolumeSelected(volumeData, binding.switchRemember.isChecked)
} }
} }

View File

@ -39,6 +39,7 @@ import sushi.hardcore.droidfs.file_operations.OperationFile
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
import sushi.hardcore.droidfs.util.IntentUtils
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
@ -82,7 +83,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
usf_open = sharedPrefs.getBoolean("usf_open", false) usf_open = sharedPrefs.getBoolean("usf_open", false)
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false) usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
volumeName = intent.getStringExtra("volume_name") ?: "" volumeName = intent.getStringExtra("volume_name") ?: ""
encryptedVolume = getParcelableExtra(intent, "volume")!! encryptedVolume = IntentUtils.getParcelableExtra(intent, "volume")!!
sortOrderEntries = resources.getStringArray(R.array.sort_orders_entries) sortOrderEntries = resources.getStringArray(R.array.sort_orders_entries)
sortOrderValues = resources.getStringArray(R.array.sort_orders_values) sortOrderValues = resources.getStringArray(R.array.sort_orders_values)
foldersFirst = sharedPrefs.getBoolean("folders_first", true) foldersFirst = sharedPrefs.getBoolean("folders_first", true)
@ -156,7 +157,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
} }
protected open fun init() { protected open fun init() {
setContentView(R.layout.activity_explorer_base) setContentView(R.layout.activity_explorer)
} }
protected open fun bindFileOperationService(){ protected open fun bindFileOperationService(){

View File

@ -8,16 +8,17 @@ import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.CameraActivity import sushi.hardcore.droidfs.CameraActivity
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.content_providers.ExternalProvider
import sushi.hardcore.droidfs.databinding.ActivityExplorerBinding
import sushi.hardcore.droidfs.file_operations.OperationFile import sushi.hardcore.droidfs.file_operations.OperationFile
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.IntentUtils
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
@ -31,11 +32,10 @@ class ExplorerActivity : BaseExplorerActivity() {
private var usf_share = false private var usf_share = false
private var currentItemAction = ItemsActions.NONE private var currentItemAction = ItemsActions.NONE
private val itemsToProcess = ArrayList<OperationFile>() private val itemsToProcess = ArrayList<OperationFile>()
private lateinit var binding: ActivityExplorerBinding
private val pickFromOtherVolumes = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val pickFromOtherVolumes = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) { if (result.resultCode == Activity.RESULT_OK) {
result.data?.let { resultIntent -> result.data?.let { resultIntent ->
val remoteEncryptedVolume = getParcelableExtra<EncryptedVolume>(resultIntent, "volume")!! val remoteEncryptedVolume = IntentUtils.getParcelableExtra<EncryptedVolume>(resultIntent, "volume")!!
val path = resultIntent.getStringExtra("path") val path = resultIntent.getStringExtra("path")
val operationFiles = ArrayList<OperationFile>() val operationFiles = ArrayList<OperationFile>()
if (path == null){ //multiples elements if (path == null){ //multiples elements
@ -168,9 +168,8 @@ class ExplorerActivity : BaseExplorerActivity() {
} }
override fun init() { override fun init() {
binding = ActivityExplorerBinding.inflate(layoutInflater) super.init()
setContentView(binding.root) findViewById<FloatingActionButton>(R.id.fab).setOnClickListener {
binding.fab.setOnClickListener {
if (currentItemAction != ItemsActions.NONE){ if (currentItemAction != ItemsActions.NONE){
openDialogCreateFolder() openDialogCreateFolder()
} else { } else {

View File

@ -5,17 +5,16 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import com.google.android.material.floatingactionbutton.FloatingActionButton
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.databinding.ActivityExplorerDropBinding import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
class ExplorerActivityDrop : BaseExplorerActivity() { class ExplorerActivityDrop : BaseExplorerActivity() {
private lateinit var binding: ActivityExplorerDropBinding
override fun init() { override fun init() {
binding = ActivityExplorerDropBinding.inflate(layoutInflater) super.init()
setContentView(binding.root) findViewById<FloatingActionButton>(R.id.fab).setOnClickListener {
binding.fab.setOnClickListener {
openDialogCreateFolder() openDialogCreateFolder()
} }
} }
@ -34,7 +33,7 @@ class ExplorerActivityDrop : BaseExplorerActivity() {
val errorMsg: String? = if (extras != null && extras.containsKey(Intent.EXTRA_STREAM)) { val errorMsg: String? = if (extras != null && extras.containsKey(Intent.EXTRA_STREAM)) {
when (intent.action) { when (intent.action) {
Intent.ACTION_SEND -> { Intent.ACTION_SEND -> {
val uri = getParcelableExtra<Uri>(intent, Intent.EXTRA_STREAM) val uri = IntentUtils.getParcelableExtra<Uri>(intent, Intent.EXTRA_STREAM)
if (uri == null) { if (uri == null) {
getString(R.string.share_intent_parsing_failed) getString(R.string.share_intent_parsing_failed)
} else { } else {

View File

@ -6,13 +6,14 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
class ExplorerActivityPick : BaseExplorerActivity() { class ExplorerActivityPick : BaseExplorerActivity() {
private var resultIntent = Intent() private var resultIntent = Intent()
private var isFinishingIntentionally = false private var isFinishingIntentionally = false
override fun init() { override fun init() {
super.init() setContentView(R.layout.activity_explorer_pick)
resultIntent.putExtra("volume", encryptedVolume) resultIntent.putExtra("volume", encryptedVolume)
} }
@ -83,7 +84,7 @@ class ExplorerActivityPick : BaseExplorerActivity() {
override fun closeVolumeOnDestroy() { override fun closeVolumeOnDestroy() {
if (!isFinishingIntentionally && !usf_keep_open){ if (!isFinishingIntentionally && !usf_keep_open){
getParcelableExtra<EncryptedVolume>(intent, "destinationVolume")?.close() IntentUtils.getParcelableExtra<EncryptedVolume>(intent, "destinationVolume")?.close()
super.closeVolumeOnDestroy() super.closeVolumeOnDestroy()
} }
} }

View File

@ -0,0 +1,29 @@
package sushi.hardcore.droidfs.explorers
import android.content.Context
import android.content.Intent
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils
class ExplorerRouter(private val context: Context, private val intent: Intent) {
var pickMode = intent.action == "pick"
var dropMode = (intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE) && intent.extras != null
fun getExplorerIntent(encryptedVolume: EncryptedVolume, volumeShortName: String): Intent {
var explorerIntent: Intent? = null
if (dropMode) { //import via android share menu
explorerIntent = Intent(context, ExplorerActivityDrop::class.java)
IntentUtils.forwardIntent(intent, explorerIntent)
} else if (pickMode) {
explorerIntent = Intent(context, ExplorerActivityPick::class.java)
explorerIntent.putExtra("destinationVolume", IntentUtils.getParcelableExtra<EncryptedVolume>(intent, "volume")!!)
explorerIntent.flags = Intent.FLAG_ACTIVITY_FORWARD_RESULT
}
if (explorerIntent == null) {
explorerIntent = Intent(context, ExplorerActivity::class.java) //default opening
}
explorerIntent.putExtra("volume", encryptedVolume)
explorerIntent.putExtra("volume_name", volumeShortName)
return explorerIntent
}
}

View File

@ -11,6 +11,7 @@ import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
import sushi.hardcore.droidfs.explorers.ExplorerElement import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
@ -34,7 +35,7 @@ abstract class FileViewerActivity: BaseActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
filePath = intent.getStringExtra("path")!! filePath = intent.getStringExtra("path")!!
originalParentPath = PathUtils.getParentPath(filePath) originalParentPath = PathUtils.getParentPath(filePath)
encryptedVolume = getParcelableExtra(intent, "volume")!! encryptedVolume = IntentUtils.getParcelableExtra(intent, "volume")!!
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false) usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
foldersFirst = sharedPrefs.getBoolean("folders_first", true) foldersFirst = sharedPrefs.getBoolean("folders_first", true)
windowInsetsController = WindowInsetsControllerCompat(window, window.decorView) windowInsetsController = WindowInsetsControllerCompat(window, window.decorView)

View File

@ -67,8 +67,14 @@ class CryfsVolume(private val fusePtr: Long): EncryptedVolume() {
} }
} }
fun create(baseDir: String, localStateDir: String, password: ByteArray, returnedHash: ObjRef<ByteArray?>?, cipher: String?): Boolean { fun create(baseDir: String, localStateDir: String, password: ByteArray, returnedHash: ObjRef<ByteArray?>?, cipher: String?, volume: ObjRef<EncryptedVolume?>?): Boolean {
return init(baseDir, localStateDir, password, null, returnedHash, true, cipher)?.also { it.close() } != null return init(baseDir, localStateDir, password, null, returnedHash, true, cipher)?.also {
if (volume == null) {
it.close()
} else {
volume.value = it
}
} != null
} }
fun init(baseDir: String, localStateDir: String, password: ByteArray?, givenHash: ByteArray?, returnedHash: ObjRef<ByteArray?>?): CryfsVolume? { fun init(baseDir: String, localStateDir: String, password: ByteArray?, givenHash: ByteArray?, returnedHash: ObjRef<ByteArray?>?): CryfsVolume? {

View File

@ -5,7 +5,7 @@ import android.net.Uri
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import sushi.hardcore.droidfs.ConstValues import sushi.hardcore.droidfs.ConstValues
import sushi.hardcore.droidfs.SavedVolume import sushi.hardcore.droidfs.VolumeData
import sushi.hardcore.droidfs.explorers.ExplorerElement import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.util.ObjRef import sushi.hardcore.droidfs.util.ObjRef
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
@ -42,7 +42,7 @@ abstract class EncryptedVolume: Parcelable {
} }
fun init( fun init(
volume: SavedVolume, volume: VolumeData,
filesDir: String, filesDir: String,
password: ByteArray?, password: ByteArray?,
givenHash: ByteArray?, givenHash: ByteArray?,

View File

@ -1,7 +1,9 @@
package sushi.hardcore.droidfs.filesystems package sushi.hardcore.droidfs.filesystems
import android.os.Parcel import android.os.Parcel
import android.util.Log
import sushi.hardcore.droidfs.explorers.ExplorerElement import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.util.ObjRef
import kotlin.math.min import kotlin.math.min
class GocryptfsVolume(private val sessionID: Int): EncryptedVolume() { class GocryptfsVolume(private val sessionID: Int): EncryptedVolume() {
@ -22,10 +24,20 @@ class GocryptfsVolume(private val sessionID: Int): EncryptedVolume() {
companion object { companion object {
const val KeyLen = 32 const val KeyLen = 32
const val ScryptDefaultLogN = 16 private const val ScryptDefaultLogN = 16
const val MAX_KERNEL_WRITE = 128*1024 private const val VOLUME_CREATOR = "DroidFS"
private const val MAX_KERNEL_WRITE = 128*1024
const val CONFIG_FILE_NAME = "gocryptfs.conf" const val CONFIG_FILE_NAME = "gocryptfs.conf"
external fun createVolume(root_cipher_dir: String, password: ByteArray, plainTextNames: Boolean, xchacha: Int, logN: Int, creator: String, returnedHash: ByteArray?): Boolean private external fun nativeCreateVolume(
root_cipher_dir: String,
password: ByteArray,
plainTextNames: Boolean,
xchacha: Int,
logN: Int,
creator: String,
returnedHash: ByteArray?,
openAfterCreation: Boolean,
): Int
private external fun nativeInit(root_cipher_dir: String, password: ByteArray?, givenHash: ByteArray?, returnedHash: ByteArray?): Int private external fun nativeInit(root_cipher_dir: String, password: ByteArray?, givenHash: ByteArray?, returnedHash: ByteArray?): Int
external fun changePassword( external fun changePassword(
root_cipher_dir: String, root_cipher_dir: String,
@ -35,6 +47,29 @@ class GocryptfsVolume(private val sessionID: Int): EncryptedVolume() {
returnedHash: ByteArray? returnedHash: ByteArray?
): Boolean ): Boolean
fun createAndOpenVolume(
root_cipher_dir: String,
password: ByteArray,
plainTextNames: Boolean,
xchacha: Int,
returnedHash: ByteArray?,
volume: ObjRef<EncryptedVolume?>?
): Boolean {
val openAfterCreation = volume != null
val result = nativeCreateVolume(root_cipher_dir, password, plainTextNames, xchacha, ScryptDefaultLogN, VOLUME_CREATOR, returnedHash, openAfterCreation)
return if (!openAfterCreation) {
result == 1
} else if (result == -1) {
Log.e("gocryptfs", "Failed to open volume after creation")
true
} else if (result == -2) {
false
} else {
volume!!.value = GocryptfsVolume(result)
true
}
}
fun init(root_cipher_dir: String, password: ByteArray?, givenHash: ByteArray?, returnedHash: ByteArray?): GocryptfsVolume? { fun init(root_cipher_dir: String, password: ByteArray?, givenHash: ByteArray?, returnedHash: ByteArray?): GocryptfsVolume? {
val sessionId = nativeInit(root_cipher_dir, password, givenHash, returnedHash) val sessionId = nativeInit(root_cipher_dir, password, givenHash, returnedHash)
return if (sessionId == -1) { return if (sessionId == -1) {
@ -52,7 +87,7 @@ class GocryptfsVolume(private val sessionID: Int): EncryptedVolume() {
constructor(parcel: Parcel) : this(parcel.readInt()) constructor(parcel: Parcel) : this(parcel.readInt())
override fun openFile(path: String): Long { override fun openFile(path: String): Long {
return native_open_write_mode(sessionID, path, 0).toLong() return native_open_write_mode(sessionID, path, 384).toLong() // 0600
} }
override fun read(fileHandle: Long, fileOffset: Long, buffer: ByteArray, dstOffset: Long, length: Long): Int { override fun read(fileHandle: Long, fileOffset: Long, buffer: ByteArray, dstOffset: Long, length: Long): Int {
@ -81,7 +116,7 @@ class GocryptfsVolume(private val sessionID: Int): EncryptedVolume() {
} }
override fun mkdir(path: String): Boolean { override fun mkdir(path: String): Boolean {
return native_mkdir(sessionID, path, 0) return native_mkdir(sessionID, path, 448) // 0700
} }
override fun rmdir(path: String): Boolean { override fun rmdir(path: String): Boolean {

View File

@ -0,0 +1,21 @@
package sushi.hardcore.droidfs.util
import android.content.Intent
import android.os.Build
import android.os.Parcelable
object IntentUtils {
inline fun <reified T: Parcelable> getParcelableExtra(intent: Intent, name: String): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(name, T::class.java)
} else {
@Suppress("Deprecation")
intent.getParcelableExtra(name)
}
}
fun forwardIntent(sourceIntent: Intent, targetIntent: Intent) {
targetIntent.action = sourceIntent.action
sourceIntent.extras?.let { targetIntent.putExtras(it) }
}
}

View File

@ -5,15 +5,18 @@
#include <sys/stat.h> #include <sys/stat.h>
#include "libgocryptfs.h" #include "libgocryptfs.h"
JNIEXPORT jboolean JNICALL const int KeyLen = 32;
Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_00024Companion_createVolume(JNIEnv *env, jclass clazz,
JNIEXPORT jint JNICALL
Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_00024Companion_nativeCreateVolume(JNIEnv *env, jclass clazz,
jstring jroot_cipher_dir, jstring jroot_cipher_dir,
jbyteArray jpassword, jbyteArray jpassword,
jboolean plainTextNames, jboolean plainTextNames,
jint xchacha, jint xchacha,
jint logN, jint logN,
jstring jcreator, jstring jcreator,
jbyteArray jreturned_hash) { jbyteArray jreturned_hash,
jboolean open_after_creation) {
const char* root_cipher_dir = (*env)->GetStringUTFChars(env, jroot_cipher_dir, NULL); const char* root_cipher_dir = (*env)->GetStringUTFChars(env, jroot_cipher_dir, NULL);
const char* creator = (*env)->GetStringUTFChars(env, jcreator, NULL); const char* creator = (*env)->GetStringUTFChars(env, jcreator, NULL);
GoString gofilename = {root_cipher_dir, strlen(root_cipher_dir)}, gocreator = {creator, strlen(creator)}; GoString gofilename = {root_cipher_dir, strlen(root_cipher_dir)}, gocreator = {creator, strlen(creator)};
@ -23,27 +26,43 @@ Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_00024Companion_createVol
GoSlice go_password = {password, password_len, password_len}; GoSlice go_password = {password, password_len, password_len};
size_t returned_hash_len; size_t returned_hash_len;
jbyte* returned_hash; GoSlice go_returned_hash;
GoSlice go_returned_hash = {NULL, 0, 0};
if (!(*env)->IsSameObject(env, jreturned_hash, NULL)) { if (!(*env)->IsSameObject(env, jreturned_hash, NULL)) {
returned_hash_len = (*env)->GetArrayLength(env, jreturned_hash); returned_hash_len = (*env)->GetArrayLength(env, jreturned_hash);
returned_hash = (*env)->GetByteArrayElements(env, jreturned_hash, NULL); go_returned_hash.data = (*env)->GetByteArrayElements(env, jreturned_hash, NULL);
go_returned_hash.data = returned_hash; } else if (open_after_creation) {
go_returned_hash.len = returned_hash_len; returned_hash_len = KeyLen;
go_returned_hash.cap = returned_hash_len; go_returned_hash.data = malloc(KeyLen);
} else {
returned_hash_len = 0;
go_returned_hash.data = NULL;
} }
go_returned_hash.len = returned_hash_len;
go_returned_hash.cap = returned_hash_len;
GoUint8 result = gcf_create_volume(gofilename, go_password, plainTextNames, xchacha, logN, gocreator, go_returned_hash); GoUint8 result = gcf_create_volume(gofilename, go_password, plainTextNames, xchacha, logN, gocreator, go_returned_hash);
(*env)->ReleaseStringUTFChars(env, jroot_cipher_dir, root_cipher_dir);
(*env)->ReleaseStringUTFChars(env, jcreator, creator);
(*env)->ReleaseByteArrayElements(env, jpassword, password, 0); (*env)->ReleaseByteArrayElements(env, jpassword, password, 0);
(*env)->ReleaseStringUTFChars(env, jcreator, creator);
if (!(*env)->IsSameObject(env, jreturned_hash, NULL)) { GoInt sessionID = -2;
(*env)->ReleaseByteArrayElements(env, jreturned_hash, returned_hash, 0); if (result && open_after_creation) {
GoSlice null_slice = {NULL, 0, 0};
sessionID = gcf_init(gofilename, null_slice, go_returned_hash, null_slice);
} }
return result; (*env)->ReleaseStringUTFChars(env, jroot_cipher_dir, root_cipher_dir);
if (!(*env)->IsSameObject(env, jreturned_hash, NULL)) {
(*env)->ReleaseByteArrayElements(env, jreturned_hash, go_returned_hash.data, 0);
} else if (open_after_creation) {
for (unsigned int i=0; i<returned_hash_len; ++i) {
((unsigned char*) go_returned_hash.data)[i] = 0;
}
free(go_returned_hash.data);
}
return sessionID*open_after_creation+result*!open_after_creation;
} }
JNIEXPORT jint JNICALL JNIEXPORT jint JNICALL

View File

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/explorer_info_bar"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/explorer_content"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_margin="15dp"
android:src="@drawable/icon_add"/>
</RelativeLayout>
</LinearLayout>

View File

@ -101,6 +101,14 @@
android:layout_marginHorizontal="@dimen/volume_operation_horizontal_gap" android:layout_marginHorizontal="@dimen/volume_operation_horizontal_gap"
android:visibility="gone"/> android:visibility="gone"/>
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switch_remember"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/remember_volume"
android:checked="true"
android:layout_gravity="center"/>
<androidx.appcompat.widget.AppCompatButton <androidx.appcompat.widget.AppCompatButton
android:id="@+id/button_action" android:id="@+id/button_action"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -108,6 +116,6 @@
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginHorizontal="@dimen/volume_operation_button_horizontal_margin" android:layout_marginHorizontal="@dimen/volume_operation_button_horizontal_margin"
android:layout_marginTop="@dimen/volume_operation_vertical_gap" android:layout_marginTop="@dimen/volume_operation_vertical_gap"
android:text="@string/add_volume" /> android:text="@string/create_volume" />
</LinearLayout> </LinearLayout>

View File

@ -250,4 +250,7 @@
<string name="volume_type_read_only">(%s, read-only)</string> <string name="volume_type_read_only">(%s, read-only)</string>
<string name="io_error">I/O Error.</string> <string name="io_error">I/O Error.</string>
<string name="use_fingerprint">Use fingerprint instead of current password</string> <string name="use_fingerprint">Use fingerprint instead of current password</string>
<string name="remember_volume">Remember volume</string>
<string name="open_volume">Open volume</string>
<string name="choose_existing_volume">Please choose an existing volume</string>
</resources> </resources>