forked from hardcoresushi/DroidFS
Allow to open & create volumes without remembering
This commit is contained in:
parent
4df1086734
commit
7c2f87109a
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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))
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
@ -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,
|
||||
|
201
app/src/main/java/sushi/hardcore/droidfs/VolumeOpener.kt
Normal file
201
app/src/main/java/sushi/hardcore/droidfs/VolumeOpener.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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(){
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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? {
|
||||
|
@ -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?,
|
||||
|
@ -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 {
|
||||
|
21
app/src/main/java/sushi/hardcore/droidfs/util/IntentUtils.kt
Normal file
21
app/src/main/java/sushi/hardcore/droidfs/util/IntentUtils.kt
Normal 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) }
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user