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?) {
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
if (shouldCheckTheme && applyCustomTheme) {
themeValue = sharedPrefs.getString("theme", ConstValues.DEFAULT_THEME_VALUE)!!
themeValue = sharedPrefs.getString(ConstValues.THEME_VALUE_KEY, ConstValues.DEFAULT_THEME_VALUE)!!
when (themeValue) {
"black_green" -> setTheme(R.style.BlackGreen)
"dark_red" -> setTheme(R.style.DarkRed)
@ -47,13 +47,4 @@ open class BaseActivity: AppCompatActivity() {
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.databinding.ActivityCameraBinding
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.video_recording.SeekableWriter
import sushi.hardcore.droidfs.video_recording.VideoCapture
@ -95,7 +96,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
binding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.hide()
encryptedVolume = getParcelableExtra(intent, "volume")!!
encryptedVolume = IntentUtils.getParcelableExtra(intent, "volume")!!
outputDirectory = intent.getStringExtra("path")!!
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.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.GocryptfsVolume
import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.ObjRef
import sushi.hardcore.droidfs.util.WidgetUtil
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
@ -22,7 +23,7 @@ import java.util.*
class ChangePasswordActivity: BaseActivity() {
private lateinit var binding: ActivityChangePasswordBinding
private lateinit var volume: SavedVolume
private lateinit var volume: VolumeData
private lateinit var volumeDatabase: VolumeDatabase
private var fingerprintProtector: FingerprintProtector? = null
private var usfFingerprint: Boolean = false
@ -32,7 +33,7 @@ class ChangePasswordActivity: BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
volume = getParcelableExtra(intent, "volume")!!
volume = IntentUtils.getParcelableExtra(intent, "volume")!!
binding = ActivityChangePasswordBinding.inflate(layoutInflater)
setContentView(binding.root)
title = getString(R.string.change_password)

View File

@ -4,7 +4,6 @@ import android.net.Uri
import java.io.File
object ConstValues {
const val CREATOR = "DroidFS"
const val VOLUME_DATABASE_NAME = "SavedVolumes"
const val CRYFS_LOCAL_STATE_DIR = "cryfsLocalState"
const val SORT_ORDER_KEY = "sort_order"
@ -13,6 +12,9 @@ object ConstValues {
const val IO_BUFF_SIZE = 16384
const val SLIDESHOW_DELAY: Long = 4000
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 DEFAULT_THUMBNAIL_MAX_SIZE = 10_000L
const val PIN_PASSWORDS_KEY = "pin_passwords"

View File

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

View File

@ -1,6 +1,6 @@
package sushi.hardcore.droidfs
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -8,7 +8,7 @@ import kotlinx.coroutines.withContext
import sushi.hardcore.droidfs.databinding.DialogLoadingBinding
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)
.setView(
DialogLoadingBinding.inflate(activity.layoutInflater).apply {

View File

@ -1,19 +1,15 @@
package sushi.hardcore.droidfs
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.text.InputType
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
@ -21,45 +17,31 @@ import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.ConstValues.DEFAULT_VOLUME_KEY
import sushi.hardcore.droidfs.adapters.VolumeAdapter
import sushi.hardcore.droidfs.add_volume.AddVolumeActivity
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
import sushi.hardcore.droidfs.databinding.ActivityMainBinding
import sushi.hardcore.droidfs.databinding.DialogDeleteVolumeBinding
import sushi.hardcore.droidfs.databinding.DialogOpenVolumeBinding
import sushi.hardcore.droidfs.explorers.ExplorerActivity
import sushi.hardcore.droidfs.explorers.ExplorerActivityDrop
import sushi.hardcore.droidfs.explorers.ExplorerActivityPick
import sushi.hardcore.droidfs.explorers.ExplorerRouter
import sushi.hardcore.droidfs.file_operations.FileOperationService
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.WidgetUtil
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog
import java.io.File
import java.util.*
class MainActivity : BaseActivity(), VolumeAdapter.Listener {
companion object {
const val DEFAULT_VOLUME_KEY = "default_volume"
}
private lateinit var binding: ActivityMainBinding
private lateinit var volumeDatabase: VolumeDatabase
private lateinit var volumeAdapter: VolumeAdapter
private var fingerprintProtector: FingerprintProtector? = null
private var usfFingerprint: Boolean = false
private lateinit var volumeOpener: VolumeOpener
private var usfKeepOpen: Boolean = false
private var defaultVolumeName: String? = null
private var addVolume = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
when (result.resultCode) {
AddVolumeActivity.RESULT_VOLUME_ADDED -> onVolumeAdded()
AddVolumeActivity.RESULT_HASH_STORAGE_RESET -> {
volumeAdapter.refresh()
binding.textNoVolumes.visibility = View.GONE
}
if ((explorerRouter.pickMode || explorerRouter.dropMode) && result.resultCode != AddVolumeActivity.RESULT_USER_BACK) {
setResult(result.resultCode, result.data) // forward result
finish()
}
}
private var changePasswordPosition: Int? = null
@ -71,8 +53,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
onDirectoryPicked(uri)
}
private lateinit var fileOperationService: FileOperationService
private var pickMode = false
private var dropMode = false
private lateinit var explorerRouter: ExplorerRouter
private var shouldCloseVolume = true // used when launched to pick file from another volume
override fun onCreate(savedInstanceState: Bundle?) {
@ -98,14 +79,13 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
}
.show()
}
pickMode = intent.action == "pick"
dropMode = (intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE) && intent.extras != null
explorerRouter = ExplorerRouter(this, intent)
volumeDatabase = VolumeDatabase(this)
volumeAdapter = VolumeAdapter(
this,
volumeDatabase,
!pickMode && !dropMode,
!dropMode,
!explorerRouter.pickMode && !explorerRouter.dropMode,
!explorerRouter.dropMode,
this,
)
binding.recyclerViewVolumes.adapter = volumeAdapter
@ -113,24 +93,22 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
if (volumeAdapter.volumes.isEmpty()) {
binding.textNoVolumes.visibility = View.VISIBLE
}
if (pickMode) {
if (explorerRouter.pickMode) {
title = getString(R.string.select_volume)
binding.fab.visibility = View.GONE
} else {
binding.fab.setOnClickListener {
addVolume.launch(Intent(this, AddVolumeActivity::class.java))
}
}
binding.fab.setOnClickListener {
addVolume.launch(Intent(this, AddVolumeActivity::class.java).also {
if (explorerRouter.dropMode || explorerRouter.pickMode) {
IntentUtils.forwardIntent(intent, it)
shouldCloseVolume = false
}
})
}
usfKeepOpen = sharedPrefs.getBoolean("usf_keep_open", false)
usfFingerprint = sharedPrefs.getBoolean("usf_fingerprint", false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fingerprintProtector = FingerprintProtector.new(this, themeValue, volumeDatabase)
}
defaultVolumeName = sharedPrefs.getString(DEFAULT_VOLUME_KEY, null)
defaultVolumeName?.let { name ->
volumeOpener = VolumeOpener(this)
volumeOpener.defaultVolumeName?.let { name ->
try {
val (position, volume) = volumeAdapter.volumes.withIndex().first { it.value.name == name }
openVolume(volume, position)
openVolume(volumeAdapter.volumes.first { it.name == name })
} catch (e: NoSuchElementException) {
unsetDefaultVolume()
}
@ -139,8 +117,9 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
if (volumeAdapter.selectedItems.isNotEmpty()) {
unselectAll()
} else {
if (pickMode)
if (explorerRouter.pickMode) {
shouldCloseVolume = false
}
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
@ -158,10 +137,15 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
override fun onStart() {
super.onStart()
// 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)
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
defaultVolumeName = sharedPrefs.getString(DEFAULT_VOLUME_KEY, null)
volumeOpener.defaultVolumeName = sharedPrefs.getString(DEFAULT_VOLUME_KEY, null)
}
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())
openVolume(volume, position)
openVolume(volume)
else
invalidateOptionsMenu()
}
@ -183,14 +167,6 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
invalidateOptionsMenu()
}
private fun onVolumeAdded() {
volumeAdapter.apply {
volumes = volumeDatabase.getVolumes()
notifyItemInserted(volumes.size)
}
binding.textNoVolumes.visibility = View.GONE
}
private fun unselectAll(notifyChange: Boolean = true) {
volumeAdapter.unSelectAll(notifyChange)
invalidateOptionsMenu()
@ -203,7 +179,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
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 (volumes[i].isHidden) {
if (doDeleteVolumeContent == null) {
@ -258,15 +234,16 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
remove(DEFAULT_VOLUME_KEY)
apply()
}
defaultVolumeName = null
volumeOpener.defaultVolumeName = null
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
if (pickMode || dropMode) {
if (pickMode)
if (explorerRouter.pickMode || explorerRouter.dropMode) {
if (explorerRouter.pickMode) {
shouldCloseVolume = false
}
finish()
} else {
unselectAll()
@ -323,7 +300,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
DocumentFile.fromFile(File(volume.name)),
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 {
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()
menu.findItem(R.id.select_all).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.remove_default_open).isVisible =
onlyOneSelected &&
volumeAdapter.volumes[volumeAdapter.selectedItems.first()].name == defaultVolumeName
volumeAdapter.volumes[volumeAdapter.selectedItems.first()].name == volumeOpener.defaultVolumeName
with(menu.findItem(R.id.copy)) {
isVisible = onlyOneSelected
if (isVisible) {
@ -371,7 +348,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
}
}
menu.findItem(R.id.rename).isVisible = onlyOneAndWriteable
supportActionBar?.setDisplayHomeAsUpEnabled(isSelecting || pickMode || dropMode)
supportActionBar?.setDisplayHomeAsUpEnabled(isSelecting || explorerRouter.pickMode || explorerRouter.dropMode)
return true
}
@ -394,7 +371,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
dstRootDirectory.name?.let { name ->
val path = PathUtils.getFullPathFromTreeUri(dstRootDirectory.uri, this)
if (path == null) null
else SavedVolume(
else VolumeData(
PathUtils.pathJoin(path, name),
false,
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 {
val result = fileOperationService.copyVolume(srcDocumentFile, dstDocumentFile)
when {
@ -417,7 +394,11 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
result.dstRootDirectory?.let {
getResultVolume(it)?.let { 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()
}
}
@ -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 ->
val srcPath = File(volume.getFullPath(filesDir.path))
val dstPath = File(srcPath.parent, newName).canonicalFile
@ -453,12 +434,12 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
if (success) {
volumeDatabase.renameVolume(volume.name, newDBName)
unselect(position)
if (volume.name == defaultVolumeName) {
if (volume.name == volumeOpener.defaultVolumeName) {
with (sharedPrefs.edit()) {
putString(DEFAULT_VOLUME_KEY, newDBName)
apply()
}
defaultVolumeName = newDBName
volumeOpener.defaultVolumeName = newDBName
}
} else {
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: SavedVolume, position: Int) {
if (volume.type == EncryptedVolume.GOCRYPTFS_VOLUME_TYPE && BuildConfig.GOCRYPTFS_DISABLED) {
Toast.makeText(this, R.string.gocryptfs_disabled, Toast.LENGTH_SHORT).show()
return
} else if (volume.type == EncryptedVolume.CRYFS_VOLUME_TYPE && BuildConfig.CRYFS_DISABLED) {
Toast.makeText(this, 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() {
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)
private fun openVolume(volume: VolumeData) {
volumeOpener.openVolume(volume, true, object : VolumeOpener.VolumeOpenerCallbacks {
override fun onHashStorageReset() {
volumeAdapter.refresh()
}
override fun onVolumeOpened(encryptedVolume: EncryptedVolume, volumeShortName: String) {
startActivity(explorerRouter.getExplorerIntent(encryptedVolume, volumeShortName))
if (explorerRouter.pickMode) {
shouldCloseVolume = false
}
if (explorerRouter.dropMode || explorerRouter.pickMode) {
finish()
}
}
}
if (askForPassword)
askForPassword(volume, position)
})
}
private fun onPasswordSubmitted(volume: SavedVolume, position: Int, dialogBinding: DialogOpenVolumeBinding) {
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,
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 onResume() {
super.onResume()
shouldCloseVolume = true
}
override fun onStop() {
super.onStop()
if (pickMode && !usfKeepOpen) {
if (explorerRouter.pickMode && !usfKeepOpen && shouldCloseVolume) {
IntentUtils.getParcelableExtra<EncryptedVolume>(intent, "volume")?.close()
RestrictedFileProvider.wipeAll(this)
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?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
findPreference<ListPreference>("theme")?.setOnPreferenceChangeListener { _, newValue ->
findPreference<ListPreference>(ConstValues.THEME_VALUE_KEY)?.setOnPreferenceChangeListener { _, newValue ->
(activity as BaseActivity).onThemeChanged(newValue as String)
true
}

View File

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

View File

@ -2,6 +2,7 @@ package sushi.hardcore.droidfs
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
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_IV = "iv"
private fun contentValuesFromVolume(volume: SavedVolume): ContentValues {
private fun contentValuesFromVolume(volume: VolumeData): ContentValues {
val contentValues = ContentValues()
contentValues.put(COLUMN_NAME, volume.name)
contentValues.put(COLUMN_HIDDEN, volume.isHidden)
@ -38,7 +39,7 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
"$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) {
@ -49,7 +50,7 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
}, null, null)
// 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(
TABLE_NAME,
arrayOf(COLUMN_NAME),
@ -68,7 +69,7 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
)
).renameTo(
File(
SavedVolume(
VolumeData(
volumeName,
true,
EncryptedVolume.GOCRYPTFS_VOLUME_TYPE
@ -82,37 +83,55 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
}
}
fun isVolumeSaved(volumeName: String, isHidden: Boolean): Boolean {
val cursor = readableDatabase.query(TABLE_NAME,
arrayOf(COLUMN_NAME), "$COLUMN_NAME=? AND $COLUMN_HIDDEN=?",
private fun extractVolumeData(cursor: Cursor): VolumeData {
return VolumeData(
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()),
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
cursor.close()
return result
}
fun saveVolume(volume: SavedVolume): Boolean {
fun saveVolume(volume: VolumeData): Boolean {
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
}
fun getVolumes(): List<SavedVolume> {
val list: MutableList<SavedVolume> = ArrayList()
fun getVolumes(): List<VolumeData> {
val list: MutableList<VolumeData> = ArrayList()
val cursor = readableDatabase.rawQuery("SELECT * FROM $TABLE_NAME", null)
while (cursor.moveToNext()){
list.add(
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))
)
)
list.add(extractVolumeData(cursor))
}
cursor.close()
return list
@ -130,14 +149,14 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
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
}
fun removeHash(volume: SavedVolume): Boolean {
fun removeHash(volume: VolumeData): Boolean {
return writableDatabase.update(
TABLE_NAME, contentValuesFromVolume(
SavedVolume(
VolumeData(
volume.name,
volume.isHidden,
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 androidx.recyclerview.widget.RecyclerView
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.SavedVolume
import sushi.hardcore.droidfs.VolumeData
import sushi.hardcore.droidfs.VolumeDatabase
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
@ -20,9 +20,9 @@ class VolumeAdapter(
private val allowSelection: Boolean,
private val showReadOnly: Boolean,
private val listener: Listener,
) : SelectableAdapter<SavedVolume>(listener::onSelectionChanged) {
) : SelectableAdapter<VolumeData>(listener::onSelectionChanged) {
private val inflater: LayoutInflater = LayoutInflater.from(context)
lateinit var volumes: List<SavedVolume>
lateinit var volumes: List<VolumeData>
init {
reloadVolumes()
@ -30,11 +30,11 @@ class VolumeAdapter(
interface Listener {
fun onSelectionChanged(size: Int)
fun onVolumeItemClick(volume: SavedVolume, position: Int)
fun onVolumeItemClick(volume: VolumeData, position: Int)
fun onVolumeItemLongClick()
}
override fun getItems(): List<SavedVolume> {
override fun getItems(): List<VolumeData> {
return volumes
}

View File

@ -2,31 +2,39 @@ package sushi.hardcore.droidfs.add_volume
import android.os.Bundle
import android.view.MenuItem
import sushi.hardcore.droidfs.BaseActivity
import sushi.hardcore.droidfs.ConstValues
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.*
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
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() {
companion object {
const val RESULT_VOLUME_ADDED = 1
const val RESULT_HASH_STORAGE_RESET = 2
const val RESULT_USER_BACK = 10
}
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?) {
super.onCreate(savedInstanceState)
binding = ActivityAddVolumeBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
usfKeepOpen = sharedPrefs.getBoolean("usf_keep_open", false)
explorerRouter = ExplorerRouter(this, intent)
volumeOpener = VolumeOpener(this)
if (savedInstanceState == null) {
supportFragmentManager
.beginTransaction()
.add(
R.id.fragment_container,
SelectPathFragment.newInstance(themeValue),
SelectPathFragment.newInstance(themeValue, explorerRouter.pickMode),
)
.commit()
}
@ -36,12 +44,21 @@ class AddVolumeActivity: BaseActivity() {
if (item.itemId == android.R.id.home) {
if (supportFragmentManager.backStackEntryCount > 0)
supportFragmentManager.popBackStack()
else
else {
setResult(RESULT_USER_BACK)
shouldCloseVolume = false
finish()
}
}
return super.onOptionsItemSelected(item)
}
override fun onBackPressed() {
setResult(RESULT_USER_BACK)
shouldCloseVolume = false
super.onBackPressed()
}
fun onFragmentLoaded(selectPathFragment: Boolean) {
title = getString(
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()
}
fun onVolumeAdded(hashStorageReset: Boolean) {
setResult(if (hashStorageReset) RESULT_HASH_STORAGE_RESET else RESULT_VOLUME_ADDED)
finish()
fun onVolumeSelected(volume: VolumeData, rememberVolume: Boolean) {
if (rememberVolume) {
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
.beginTransaction()
.replace(
@ -69,6 +97,7 @@ class AddVolumeActivity: BaseActivity() {
themeValue,
volumePath,
isHidden,
rememberVolume,
sharedPrefs.getBoolean(ConstValues.PIN_PASSWORDS_KEY, false),
sharedPrefs.getBoolean("usf_fingerprint", false),
)
@ -76,4 +105,18 @@ class AddVolumeActivity: BaseActivity() {
.addToBackStack(null)
.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 java.io.File
import java.util.*
import kotlin.collections.ArrayList
class CreateVolumeFragment: Fragment() {
companion object {
private const val KEY_THEME_VALUE = "theme"
private const val KEY_VOLUME_PATH = "path"
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_USF_FINGERPRINT = "fingerprint"
@ -37,6 +37,7 @@ class CreateVolumeFragment: Fragment() {
themeValue: String,
volumePath: String,
isHidden: Boolean,
rememberVolume: Boolean,
pinPasswords: Boolean,
usfFingerprint: Boolean,
): CreateVolumeFragment {
@ -45,6 +46,7 @@ class CreateVolumeFragment: Fragment() {
putString(KEY_THEME_VALUE, themeValue)
putString(KEY_VOLUME_PATH, volumePath)
putBoolean(KEY_IS_HIDDEN, isHidden)
putBoolean(KEY_REMEMBER_VOLUME, rememberVolume)
putBoolean(KEY_PIN_PASSWORDS, pinPasswords)
putBoolean(KEY_USF_FINGERPRINT, usfFingerprint)
}
@ -57,6 +59,7 @@ class CreateVolumeFragment: Fragment() {
private val volumeTypes = ArrayList<String>(2)
private lateinit var volumePath: String
private var isHiddenVolume: Boolean = false
private var rememberVolume: Boolean = false
private var usfFingerprint: Boolean = false
private lateinit var volumeDatabase: VolumeDatabase
private var fingerprintProtector: FingerprintProtector? = null
@ -76,6 +79,7 @@ class CreateVolumeFragment: Fragment() {
arguments.getString(KEY_THEME_VALUE)?.let { themeValue = it }
volumePath = arguments.getString(KEY_VOLUME_PATH)!!
isHiddenVolume = arguments.getBoolean(KEY_IS_HIDDEN)
rememberVolume = arguments.getBoolean(KEY_REMEMBER_VOLUME)
usfFingerprint = arguments.getBoolean(KEY_USF_FINGERPRINT)
arguments.getBoolean(KEY_PIN_PASSWORDS)
}
@ -83,7 +87,7 @@ class CreateVolumeFragment: Fragment() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
fingerprintProtector = FingerprintProtector.new(requireActivity(), themeValue, volumeDatabase)
}
if (!usfFingerprint || fingerprintProtector == null) {
if (!rememberVolume || !usfFingerprint || fingerprintProtector == null) {
binding.checkboxSavePassword.visibility = View.GONE
}
if (!BuildConfig.GOCRYPTFS_DISABLED) {
@ -140,21 +144,6 @@ class CreateVolumeFragment: Fragment() {
(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() {
val password = WidgetUtil.encodeEditTextContent(binding.editPassword)
val passwordConfirm = WidgetUtil.encodeEditTextContent(binding.editPasswordConfirm)
@ -169,50 +158,73 @@ class CreateVolumeFragment: Fragment() {
} else {
null
}
object: LoadingTask<SavedVolume?>(requireActivity() as AppCompatActivity, themeValue, R.string.loading_msg_create) {
override suspend fun doTask(): SavedVolume? {
val encryptedVolume = if (rememberVolume) {
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)
if (!volumeFile.exists())
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) {
0 -> 0
1 -> 1
else -> -1
}
saveVolume(GocryptfsVolume.createVolume(
generateResult(GocryptfsVolume.createAndOpenVolume(
volumePath,
password,
false,
xchacha,
GocryptfsVolume.ScryptDefaultLogN,
ConstValues.CREATOR,
returnedHash?.apply {
value = ByteArray(GocryptfsVolume.KeyLen)
}?.value,
encryptedVolume,
), EncryptedVolume.GOCRYPTFS_VOLUME_TYPE)
} else {
saveVolume(CryfsVolume.create(
generateResult(CryfsVolume.create(
volumePath,
CryfsVolume.getLocalStateDir(activity.filesDir.path),
password,
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)
}
Arrays.fill(password, 0)
return volume
return result
}
}.startTask(lifecycleScope) { volume ->
if (volume == null) {
}.startTask(lifecycleScope) { result ->
if (result.compareTo(-1) == 0) {
CustomAlertDialogBuilder(requireContext(), themeValue)
.setTitle(R.string.error)
.setMessage(R.string.create_volume_failed)
.setPositiveButton(R.string.ok, null)
.show()
} 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
if (binding.checkboxSavePassword.isChecked && returnedHash != null) {
if (isVolumeSaved && binding.checkboxSavePassword.isChecked && returnedHash != null) {
fingerprintProtector!!.let {
it.listener = object : FingerprintProtector.Listener {
override fun onHashStorageReset() {
@ -223,24 +235,32 @@ class CreateVolumeFragment: Fragment() {
override fun onPasswordHashDecrypted(hash: ByteArray) {} // shouldn't happen here
override fun onPasswordHashSaved() {
Arrays.fill(returnedHash.value!!, 0)
onVolumeCreated()
onVolumeCreated(encryptedVolume?.value, volume.shortName)
}
override fun onFailed(pending: Boolean) {
if (!pending) {
Arrays.fill(returnedHash.value!!, 0)
onVolumeCreated()
onVolumeCreated(encryptedVolume?.value, volume.shortName)
}
}
}
it.savePasswordHash(volume, returnedHash.value!!)
}
} else onVolumeCreated()
} else {
onVolumeCreated(encryptedVolume?.value, volume.shortName)
}
}
}
}
}
private fun onVolumeCreated() {
(activity as AddVolumeActivity).onVolumeAdded(hashStorageReset)
private fun onVolumeCreated(encryptedVolume: EncryptedVolume?, volumeShortName: String) {
(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.annotation.SuppressLint
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
@ -15,7 +16,11 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
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.FragmentSelectPathBinding
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
@ -26,11 +31,13 @@ import java.io.File
class SelectPathFragment: Fragment() {
companion object {
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 {
arguments = Bundle().apply {
putString(KEY_THEME_VALUE, themeValue)
putBoolean(KEY_PICK_MODE, pickMode)
}
}
}
@ -39,7 +46,7 @@ class SelectPathFragment: Fragment() {
private lateinit var binding: FragmentSelectPathBinding
private val askStoragePermissions = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
if (result[Manifest.permission.READ_EXTERNAL_STORAGE] == true && result[Manifest.permission.WRITE_EXTERNAL_STORAGE] == true)
PathUtils.safePickDirectory(pickDirectory, requireContext(), themeValue)
launchPickDirectory()
else
CustomAlertDialogBuilder(requireContext(), themeValue)
.setTitle(R.string.storage_perm_denied)
@ -54,6 +61,12 @@ class SelectPathFragment: Fragment() {
}
private var themeValue = ConstValues.DEFAULT_THEME_VALUE
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(
inflater: LayoutInflater,
@ -65,15 +78,24 @@ class SelectPathFragment: Fragment() {
}
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.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())
filesDir = requireContext().filesDir.path
binding.containerHiddenVolume.setOnClickListener {
binding.switchHiddenVolume.performClick()
}
binding.switchHiddenVolume.setOnClickListener {
showRightSection()
refreshStatus(binding.editVolumeName.text)
}
binding.buttonPickDirectory.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@ -86,7 +108,7 @@ class SelectPathFragment: Fragment() {
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
)
PathUtils.safePickDirectory(pickDirectory, requireContext(), themeValue)
launchPickDirectory()
else
askStoragePermissions.launch(
arrayOf(
@ -95,35 +117,18 @@ class SelectPathFragment: Fragment() {
)
)
} else
PathUtils.safePickDirectory(pickDirectory, requireContext(), themeValue)
launchPickDirectory()
}
var isVolumeAlreadySaved = false
var volumeAction: Action? = null
binding.editVolumeName.addTextChangedListener(object: TextWatcher {
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
isVolumeAlreadySaved = volumeDatabase.isVolumeSaved(s.toString(), binding.switchHiddenVolume.isChecked)
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
})
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
refreshStatus(s)
}
})
binding.editVolumeName.setOnEditorActionListener { _, _, _ -> onPathSelected(isVolumeAlreadySaved, volumeAction); true }
binding.buttonAction.setOnClickListener { onPathSelected(isVolumeAlreadySaved, volumeAction) }
binding.switchRemember.setOnCheckedChangeListener { _, _ -> refreshButtonText() }
binding.editVolumeName.setOnEditorActionListener { _, _, _ -> onPathSelected(); true }
binding.buttonAction.setOnClickListener { onPathSelected() }
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
@ -132,6 +137,11 @@ class SelectPathFragment: Fragment() {
showRightSection()
}
private fun launchPickDirectory() {
(activity as AddVolumeActivity).shouldCloseVolume = false
PathUtils.safePickDirectory(pickDirectory, requireContext(), themeValue)
}
private fun showRightSection() {
if (binding.switchHiddenVolume.isChecked) {
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) {
val path = PathUtils.getFullPathFromTreeUri(uri, requireContext())
if (path != null)
@ -158,95 +210,105 @@ class SelectPathFragment: Fragment() {
private fun getCurrentVolumePath(): String {
return if (binding.switchHiddenVolume.isChecked)
SavedVolume.getHiddenVolumeFullPath(requireContext().filesDir.path, binding.editVolumeName.text.toString())
VolumeData.getHiddenVolumeFullPath(filesDir, binding.editVolumeName.text.toString())
else
binding.editVolumeName.text.toString()
}
private fun onPathSelected(isVolumeAlreadySaved: Boolean, volumeAction: Action?) {
if (isVolumeAlreadySaved) {
(activity as AddVolumeActivity).onSelectedAlreadySavedVolume()
} else {
if (binding.switchHiddenVolume.isChecked && volumeAction == Action.CREATE) {
private fun onPathSelected() {
if (binding.switchRemember.isChecked != originalRememberVolume) {
with(sharedPrefs.edit()) {
putBoolean(ConstValues.REMEMBER_VOLUME_KEY, binding.switchRemember.isChecked)
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)
.setTitle(R.string.warning)
.setMessage(R.string.hidden_volume_warning)
.setPositiveButton(R.string.ok) { _, _ ->
addVolume(volumeAction)
onNewVolumeSelected(currentVolumeValue, isHidden)
}
.show()
} else {
addVolume(volumeAction)
onNewVolumeSelected(currentVolumeValue, isHidden)
}
} else {
(activity as AddVolumeActivity).onVolumeSelected(currentVolumeData!!, true)
}
}
private fun addVolume(volumeAction: Action?) {
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 {
val volumePath = getCurrentVolumePath()
when (volumeAction!!) {
Action.CREATE -> {
val volumeFile = File(volumePath)
var goodDirectory = false
if (volumeFile.isFile) {
Toast.makeText(requireContext(), R.string.error_is_file, 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()
private fun onNewVolumeSelected(currentVolumeValue: String, isHidden: Boolean) {
val volumePath = getCurrentVolumePath()
when (volumeAction!!) {
Action.CREATE -> {
val volumeFile = File(volumePath)
var goodDirectory = false
if (volumeFile.isFile) {
Toast.makeText(requireContext(), R.string.error_is_file, 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 {
if (File(PathUtils.getParentPath(volumePath)).canWrite())
goodDirectory = true
else
errorDirectoryNotWritable(volumePath)
Toast.makeText(requireContext(), R.string.listdir_null_error_msg, Toast.LENGTH_SHORT).show()
}
} else {
if (File(PathUtils.getParentPath(volumePath)).canWrite()) {
goodDirectory = true
} else {
errorDirectoryNotWritable(volumePath)
}
if (goodDirectory)
(activity as AddVolumeActivity).createVolume(volumePath, isHidden)
}
Action.ADD -> {
val volumeType = EncryptedVolume.getVolumeType(volumePath)
if (volumeType < 0) {
CustomAlertDialogBuilder(requireContext(), themeValue)
.setTitle(R.string.error)
.setMessage(R.string.error_not_a_volume)
.setPositiveButton(R.string.ok, null)
.show()
} else if (!File(volumePath).canWrite()) {
val dialog = CustomAlertDialogBuilder(requireContext(), themeValue)
.setTitle(R.string.warning)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> addVolume(if (isHidden) currentVolumeValue else volumePath, isHidden, volumeType) }
if (PathUtils.isPathOnExternalStorage(volumePath, requireContext()))
dialog.setView(
DialogSdcardErrorBinding.inflate(layoutInflater).apply {
path.text = PathUtils.getPackageDataFolder(requireContext())
footer.text = getString(R.string.sdcard_error_add_footer)
}.root
)
else
dialog.setMessage(R.string.add_cant_write_warning)
dialog.show()
if (goodDirectory) {
(activity as AddVolumeActivity).createVolume(volumePath, isHidden, binding.switchRemember.isChecked)
}
}
Action.ADD -> {
val volumeType = EncryptedVolume.getVolumeType(volumePath)
if (volumeType < 0) {
CustomAlertDialogBuilder(requireContext(), themeValue)
.setTitle(R.string.error)
.setMessage(R.string.error_not_a_volume)
.setPositiveButton(R.string.ok, null)
.show()
} else if (!File(volumePath).canWrite()) {
val dialog = CustomAlertDialogBuilder(requireContext(), themeValue)
.setTitle(R.string.warning)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> addVolume(if (isHidden) currentVolumeValue else volumePath, isHidden, volumeType) }
if (PathUtils.isPathOnExternalStorage(volumePath, requireContext())) {
dialog.setView(
DialogSdcardErrorBinding.inflate(layoutInflater).apply {
path.text = PathUtils.getPackageDataFolder(requireContext())
footer.text = getString(R.string.sdcard_error_add_footer)
}.root
)
} 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) {
volumeDatabase.saveVolume(SavedVolume(volumeName, isHidden, volumeType))
(activity as AddVolumeActivity).onVolumeAdded(false)
val volumeData = VolumeData(volumeName, isHidden, volumeType)
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.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog
@ -82,7 +83,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
usf_open = sharedPrefs.getBoolean("usf_open", false)
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
volumeName = intent.getStringExtra("volume_name") ?: ""
encryptedVolume = getParcelableExtra(intent, "volume")!!
encryptedVolume = IntentUtils.getParcelableExtra(intent, "volume")!!
sortOrderEntries = resources.getStringArray(R.array.sort_orders_entries)
sortOrderValues = resources.getStringArray(R.array.sort_orders_values)
foldersFirst = sharedPrefs.getBoolean("folders_first", true)
@ -156,7 +157,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
}
protected open fun init() {
setContentView(R.layout.activity_explorer_base)
setContentView(R.layout.activity_explorer)
}
protected open fun bindFileOperationService(){

View File

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

View File

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

View File

@ -6,13 +6,14 @@ import android.view.Menu
import android.view.MenuItem
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils
class ExplorerActivityPick : BaseExplorerActivity() {
private var resultIntent = Intent()
private var isFinishingIntentionally = false
override fun init() {
super.init()
setContentView(R.layout.activity_explorer_pick)
resultIntent.putExtra("volume", encryptedVolume)
}
@ -83,7 +84,7 @@ class ExplorerActivityPick : BaseExplorerActivity() {
override fun closeVolumeOnDestroy() {
if (!isFinishingIntentionally && !usf_keep_open){
getParcelableExtra<EncryptedVolume>(intent, "destinationVolume")?.close()
IntentUtils.getParcelableExtra<EncryptedVolume>(intent, "destinationVolume")?.close()
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.explorers.ExplorerElement
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
@ -34,7 +35,7 @@ abstract class FileViewerActivity: BaseActivity() {
super.onCreate(savedInstanceState)
filePath = intent.getStringExtra("path")!!
originalParentPath = PathUtils.getParentPath(filePath)
encryptedVolume = getParcelableExtra(intent, "volume")!!
encryptedVolume = IntentUtils.getParcelableExtra(intent, "volume")!!
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
foldersFirst = sharedPrefs.getBoolean("folders_first", true)
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 {
return init(baseDir, localStateDir, password, null, returnedHash, true, cipher)?.also { it.close() } != null
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 {
if (volume == null) {
it.close()
} else {
volume.value = it
}
} != null
}
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.Parcelable
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.util.ObjRef
import sushi.hardcore.droidfs.util.PathUtils
@ -42,7 +42,7 @@ abstract class EncryptedVolume: Parcelable {
}
fun init(
volume: SavedVolume,
volume: VolumeData,
filesDir: String,
password: ByteArray?,
givenHash: ByteArray?,

View File

@ -1,7 +1,9 @@
package sushi.hardcore.droidfs.filesystems
import android.os.Parcel
import android.util.Log
import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.util.ObjRef
import kotlin.math.min
class GocryptfsVolume(private val sessionID: Int): EncryptedVolume() {
@ -22,10 +24,20 @@ class GocryptfsVolume(private val sessionID: Int): EncryptedVolume() {
companion object {
const val KeyLen = 32
const val ScryptDefaultLogN = 16
const val MAX_KERNEL_WRITE = 128*1024
private const val ScryptDefaultLogN = 16
private const val VOLUME_CREATOR = "DroidFS"
private const val MAX_KERNEL_WRITE = 128*1024
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
external fun changePassword(
root_cipher_dir: String,
@ -35,6 +47,29 @@ class GocryptfsVolume(private val sessionID: Int): EncryptedVolume() {
returnedHash: ByteArray?
): 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? {
val sessionId = nativeInit(root_cipher_dir, password, givenHash, returnedHash)
return if (sessionId == -1) {
@ -52,7 +87,7 @@ class GocryptfsVolume(private val sessionID: Int): EncryptedVolume() {
constructor(parcel: Parcel) : this(parcel.readInt())
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 {
@ -81,7 +116,7 @@ class GocryptfsVolume(private val sessionID: Int): EncryptedVolume() {
}
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 {

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 "libgocryptfs.h"
JNIEXPORT jboolean JNICALL
Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_00024Companion_createVolume(JNIEnv *env, jclass clazz,
const int KeyLen = 32;
JNIEXPORT jint JNICALL
Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_00024Companion_nativeCreateVolume(JNIEnv *env, jclass clazz,
jstring jroot_cipher_dir,
jbyteArray jpassword,
jboolean plainTextNames,
jint xchacha,
jint logN,
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* creator = (*env)->GetStringUTFChars(env, jcreator, NULL);
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};
size_t returned_hash_len;
jbyte* returned_hash;
GoSlice go_returned_hash = {NULL, 0, 0};
GoSlice go_returned_hash;
if (!(*env)->IsSameObject(env, jreturned_hash, NULL)) {
returned_hash_len = (*env)->GetArrayLength(env, jreturned_hash);
returned_hash = (*env)->GetByteArrayElements(env, jreturned_hash, NULL);
go_returned_hash.data = returned_hash;
go_returned_hash.len = returned_hash_len;
go_returned_hash.cap = returned_hash_len;
go_returned_hash.data = (*env)->GetByteArrayElements(env, jreturned_hash, NULL);
} else if (open_after_creation) {
returned_hash_len = KeyLen;
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);
(*env)->ReleaseStringUTFChars(env, jroot_cipher_dir, root_cipher_dir);
(*env)->ReleaseStringUTFChars(env, jcreator, creator);
(*env)->ReleaseByteArrayElements(env, jpassword, password, 0);
(*env)->ReleaseStringUTFChars(env, jcreator, creator);
if (!(*env)->IsSameObject(env, jreturned_hash, NULL)) {
(*env)->ReleaseByteArrayElements(env, jreturned_hash, returned_hash, 0);
GoInt sessionID = -2;
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

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: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
android:id="@+id/button_action"
android:layout_width="match_parent"
@ -108,6 +116,6 @@
android:layout_gravity="center"
android:layout_marginHorizontal="@dimen/volume_operation_button_horizontal_margin"
android:layout_marginTop="@dimen/volume_operation_vertical_gap"
android:text="@string/add_volume" />
android:text="@string/create_volume" />
</LinearLayout>

View File

@ -250,4 +250,7 @@
<string name="volume_type_read_only">(%s, read-only)</string>
<string name="io_error">I/O Error.</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>