SavedVolume database & Hidden volume feature

This commit is contained in:
Matéo Duparc 2020-11-04 18:42:04 +01:00
parent 0b509c2f98
commit a4df9d3ffa
14 changed files with 439 additions and 231 deletions

View File

@ -56,6 +56,6 @@ dependencies {
implementation "com.github.bumptech.glide:glide:4.11.0" implementation "com.github.bumptech.glide:glide:4.11.0"
implementation "com.google.android.exoplayer:exoplayer-core:2.11.7" implementation "com.google.android.exoplayer:exoplayer-core:2.11.7"
implementation "com.google.android.exoplayer:exoplayer-ui:2.11.7" implementation "com.google.android.exoplayer:exoplayer-ui:2.11.7"
implementation "com.otaliastudios:cameraview:2.6.3" implementation "com.otaliastudios:cameraview:2.6.4"
implementation "androidx.biometric:biometric:1.0.1" implementation "androidx.biometric:biometric:1.0.1"
} }

View File

@ -29,25 +29,34 @@ class ChangePasswordActivity : VolumeActionActivity() {
setContentView(R.layout.activity_change_password) setContentView(R.layout.activity_change_password)
setupActionBar() setupActionBar()
setupFingerprintStuff() setupFingerprintStuff()
savedVolumesAdapter = SavedVolumesAdapter(this, sharedPrefs) savedVolumesAdapter = SavedVolumesAdapter(this, volumeDatabase)
if (savedVolumesAdapter.count > 0){ if (savedVolumesAdapter.count > 0){
saved_path_listview.adapter = savedVolumesAdapter saved_path_listview.adapter = savedVolumesAdapter
saved_path_listview.onItemClickListener = OnItemClickListener { _, _, position, _ -> saved_path_listview.onItemClickListener = OnItemClickListener { _, _, position, _ ->
edit_volume_path.setText(savedVolumesAdapter.getItem(position)) val volume = savedVolumesAdapter.getItem(position)
currentVolumeName = volume.name
if (volume.isHidden){
switch_hidden_volume.isChecked = true
edit_volume_name.setText(currentVolumeName)
} else {
switch_hidden_volume.isChecked = false
edit_volume_path.setText(currentVolumeName)
}
onClickSwitchHiddenVolume(switch_hidden_volume)
} }
} else { } else {
WidgetUtil.hide(saved_path_listview) WidgetUtil.hideWithPadding(saved_path_listview)
} }
edit_volume_path.addTextChangedListener(object: TextWatcher{ val textWatcher = object: TextWatcher{
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
} }
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
} }
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (savedVolumesAdapter.isPathSaved(s.toString())){ if (volumeDatabase.isVolumeSaved(s.toString())){
checkbox_remember_path.isEnabled = false checkbox_remember_path.isEnabled = false
checkbox_remember_path.isChecked = false checkbox_remember_path.isChecked = false
if (sharedPrefs.getString(s.toString(), null) != null){ if (volumeDatabase.isHashSaved(s.toString())){
edit_old_password.text = null edit_old_password.text = null
edit_old_password.hint = getString(R.string.hash_saved_hint) edit_old_password.hint = getString(R.string.hash_saved_hint)
edit_old_password.isEnabled = false edit_old_password.isEnabled = false
@ -61,7 +70,9 @@ class ChangePasswordActivity : VolumeActionActivity() {
edit_old_password.isEnabled = true edit_old_password.isEnabled = true
} }
} }
}) }
edit_volume_path.addTextChangedListener(textWatcher)
edit_volume_name.addTextChangedListener(textWatcher)
edit_new_password_confirm.setOnEditorActionListener { v, _, _ -> edit_new_password_confirm.setOnEditorActionListener { v, _, _ ->
onClickChangePassword(v) onClickChangePassword(v)
true true
@ -102,30 +113,27 @@ class ChangePasswordActivity : VolumeActionActivity() {
} }
fun onClickChangePassword(view: View?) { fun onClickChangePassword(view: View?) {
rootCipherDir = edit_volume_path.text.toString() loadVolumePath {
if (rootCipherDir.isEmpty()) { val volumeFile = File(currentVolumePath)
Toast.makeText(this, R.string.enter_volume_path, Toast.LENGTH_SHORT).show() if (!GocryptfsVolume.isGocryptfsVolume(volumeFile)){
} else {
val rootCipherDirFile = File(rootCipherDir)
if (!GocryptfsVolume.isGocryptfsVolume(rootCipherDirFile)){
ColoredAlertDialogBuilder(this) ColoredAlertDialogBuilder(this)
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(R.string.error_not_a_volume) .setMessage(R.string.error_not_a_volume)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} else if (!rootCipherDirFile.canWrite()){ } else if (!volumeFile.canWrite()){
ColoredAlertDialogBuilder(this) ColoredAlertDialogBuilder(this)
.setTitle(R.string.warning) .setTitle(R.string.warning)
.setMessage(R.string.change_pwd_cant_write_error_msg) .setMessage(R.string.change_pwd_cant_write_error_msg)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} else { } else {
changePassword(null) changePassword()
} }
} }
} }
private fun changePassword(givenHash: ByteArray?){ private fun changePassword(givenHash: ByteArray? = null){
val newPassword = edit_new_password.text.toString().toCharArray() val newPassword = edit_new_password.text.toString().toCharArray()
val newPasswordConfirm = edit_new_password_confirm.text.toString().toCharArray() val newPasswordConfirm = edit_new_password_confirm.text.toString().toCharArray()
if (!newPassword.contentEquals(newPasswordConfirm)) { if (!newPassword.contentEquals(newPasswordConfirm)) {
@ -140,30 +148,38 @@ class ChangePasswordActivity : VolumeActionActivity() {
} }
var changePasswordImmediately = true var changePasswordImmediately = true
if (givenHash == null) { if (givenHash == null) {
val cipherText = sharedPrefs.getString(rootCipherDir, null) var volume: Volume? = null
if (cipherText != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { //password hash saved volumeDatabase.getVolumes().forEach { testVolume ->
stopTask { if (testVolume.name == currentVolumeName){
loadPasswordHash(cipherText, ::changePassword) volume = testVolume
}
}
volume?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
it.hash?.let { hash ->
it.iv?.let { iv ->
currentVolumePath = if (it.isHidden){
PathUtils.pathJoin(filesDir.path, it.name)
} else {
it.name
}
stopTask {
loadPasswordHash(hash, iv, ::changePassword)
}
changePasswordImmediately = false
}
}
} }
changePasswordImmediately = false
} }
} }
if (changePasswordImmediately) { if (changePasswordImmediately) {
if (GocryptfsVolume.changePassword( if (GocryptfsVolume.changePassword(currentVolumePath, oldPassword, givenHash, newPassword, returnedHash)) {
rootCipherDir, val volume = Volume(currentVolumeName, switch_hidden_volume.isChecked)
oldPassword, if (volumeDatabase.isHashSaved(currentVolumeName)) {
givenHash, volumeDatabase.removeHash(volume)
newPassword,
returnedHash
)
) {
if (sharedPrefs.getString(rootCipherDir, null) != null) {
val editor = sharedPrefs.edit()
editor.remove(rootCipherDir)
editor.apply()
} }
if (checkbox_remember_path.isChecked) { if (checkbox_remember_path.isChecked) {
savedVolumesAdapter.addVolumePath(rootCipherDir) volumeDatabase.saveVolume(volume)
} }
if (checkbox_save_password.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ if (checkbox_save_password.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
stopTask { stopTask {

View File

@ -7,7 +7,7 @@ class ConstValues {
companion object { companion object {
const val creator = "DroidFS" const val creator = "DroidFS"
const val gocryptfsConfFilename = "gocryptfs.conf" const val gocryptfsConfFilename = "gocryptfs.conf"
const val saved_volumes_key = "saved_volumes" const val volumeDatabaseName = "SavedVolumes"
const val sort_order_key = "sort_order" const val sort_order_key = "sort_order"
val fakeUri: Uri = Uri.parse("fakeuri://droidfs") val fakeUri: Uri = Uri.parse("fakeuri://droidfs")
const val MAX_KERNEL_WRITE = 128*1024 const val MAX_KERNEL_WRITE = 128*1024

View File

@ -11,10 +11,7 @@ import kotlinx.android.synthetic.main.activity_create.*
import kotlinx.android.synthetic.main.checkboxes_section.* import kotlinx.android.synthetic.main.checkboxes_section.*
import kotlinx.android.synthetic.main.volume_path_section.* import kotlinx.android.synthetic.main.volume_path_section.*
import sushi.hardcore.droidfs.explorers.ExplorerActivity import sushi.hardcore.droidfs.explorers.ExplorerActivity
import sushi.hardcore.droidfs.util.GocryptfsVolume import sushi.hardcore.droidfs.util.*
import sushi.hardcore.droidfs.util.LoadingTask
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.Wiper
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
import java.io.File import java.io.File
import java.util.* import java.util.*
@ -69,10 +66,7 @@ class CreateActivity : VolumeActionActivity() {
} }
fun onClickCreate(view: View?) { fun onClickCreate(view: View?) {
rootCipherDir = edit_volume_path.text.toString() loadVolumePath {
if (rootCipherDir.isEmpty()) {
Toast.makeText(this, R.string.enter_volume_path, Toast.LENGTH_SHORT).show()
} else {
val password = edit_password.text.toString().toCharArray() val password = edit_password.text.toString().toCharArray()
val passwordConfirm = edit_password_confirm.text.toString().toCharArray() val passwordConfirm = edit_password_confirm.text.toString().toCharArray()
if (!password.contentEquals(passwordConfirm)) { if (!password.contentEquals(passwordConfirm)) {
@ -80,10 +74,10 @@ class CreateActivity : VolumeActionActivity() {
} else { } else {
object: LoadingTask(this, R.string.loading_msg_create){ object: LoadingTask(this, R.string.loading_msg_create){
override fun doTask(activity: AppCompatActivity) { override fun doTask(activity: AppCompatActivity) {
val volumePathFile = File(rootCipherDir) val volumeFile = File(currentVolumePath)
var goodDirectory = false var goodDirectory = false
if (!volumePathFile.isDirectory) { if (!volumeFile.isDirectory) {
if (volumePathFile.mkdirs()) { if (volumeFile.mkdirs()) {
goodDirectory = true goodDirectory = true
} else { } else {
stopTask { stopTask {
@ -95,10 +89,10 @@ class CreateActivity : VolumeActionActivity() {
} }
} }
} else { } else {
val dirContent = volumePathFile.list() val dirContent = volumeFile.list()
if (dirContent != null){ if (dirContent != null){
if (dirContent.isEmpty()) { if (dirContent.isEmpty()) {
if (volumePathFile.canWrite()){ if (volumeFile.canWrite()){
goodDirectory = true goodDirectory = true
} else { } else {
stopTask { stopTask {
@ -117,26 +111,18 @@ class CreateActivity : VolumeActionActivity() {
} }
} }
if (goodDirectory) { if (goodDirectory) {
if (GocryptfsVolume.createVolume(rootCipherDir, password, GocryptfsVolume.ScryptDefaultLogN, ConstValues.creator)) { if (GocryptfsVolume.createVolume(currentVolumePath, password, GocryptfsVolume.ScryptDefaultLogN, ConstValues.creator)) {
var returnedHash: ByteArray? = null var returnedHash: ByteArray? = null
if (checkbox_save_password.isChecked){ if (checkbox_save_password.isChecked){
returnedHash = ByteArray(GocryptfsVolume.KeyLen) returnedHash = ByteArray(GocryptfsVolume.KeyLen)
} }
sessionID = GocryptfsVolume.init(rootCipherDir, password, null, returnedHash) sessionID = GocryptfsVolume.init(currentVolumePath, password, null, returnedHash)
if (sessionID != -1) { if (sessionID != -1) {
if (checkbox_remember_path.isChecked) { if (checkbox_remember_path.isChecked) {
val oldSavedVolumesPaths = sharedPrefs.getStringSet(ConstValues.saved_volumes_key, HashSet()) as Set<String> if (volumeDatabase.isVolumeSaved(currentVolumeName)) {
val editor = sharedPrefs.edit() volumeDatabase.removeVolume(Volume(currentVolumeName))
val newSavedVolumesPaths = oldSavedVolumesPaths.toMutableList()
if (oldSavedVolumesPaths.contains(rootCipherDir)) {
if (sharedPrefs.getString(rootCipherDir, null) != null){
editor.remove(rootCipherDir)
}
} else {
newSavedVolumesPaths.add(rootCipherDir)
editor.putStringSet(ConstValues.saved_volumes_key, newSavedVolumesPaths.toSet())
} }
editor.apply() volumeDatabase.saveVolume(Volume(currentVolumeName, switch_hidden_volume.isChecked))
} }
if (checkbox_save_password.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ if (checkbox_save_password.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
stopTask { stopTask {
@ -178,7 +164,7 @@ class CreateActivity : VolumeActionActivity() {
.setPositiveButton(R.string.ok) { _, _ -> .setPositiveButton(R.string.ok) { _, _ ->
val intent = Intent(applicationContext, ExplorerActivity::class.java) val intent = Intent(applicationContext, ExplorerActivity::class.java)
intent.putExtra("sessionID", sessionID) intent.putExtra("sessionID", sessionID)
intent.putExtra("volume_name", File(rootCipherDir).name) intent.putExtra("volume_name", File(currentVolumeName).name)
startActivity(intent) startActivity(intent)
finish() finish()
} }

View File

@ -9,7 +9,6 @@ import android.text.TextWatcher
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.AdapterView.OnItemClickListener import android.widget.AdapterView.OnItemClickListener
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_open.* import kotlinx.android.synthetic.main.activity_open.*
import kotlinx.android.synthetic.main.checkboxes_section.* import kotlinx.android.synthetic.main.checkboxes_section.*
@ -37,30 +36,46 @@ class OpenActivity : VolumeActionActivity() {
setContentView(R.layout.activity_open) setContentView(R.layout.activity_open)
setupActionBar() setupActionBar()
setupFingerprintStuff() setupFingerprintStuff()
savedVolumesAdapter = SavedVolumesAdapter(this, sharedPrefs) savedVolumesAdapter = SavedVolumesAdapter(this, volumeDatabase)
if (savedVolumesAdapter.count > 0){ if (savedVolumesAdapter.count > 0){
saved_path_listview.adapter = savedVolumesAdapter saved_path_listview.adapter = savedVolumesAdapter
saved_path_listview.onItemClickListener = OnItemClickListener { _, _, position, _ -> saved_path_listview.onItemClickListener = OnItemClickListener { _, _, position, _ ->
rootCipherDir = savedVolumesAdapter.getItem(position) val volume = savedVolumesAdapter.getItem(position)
edit_volume_path.setText(rootCipherDir) currentVolumeName = volume.name
val cipherText = sharedPrefs.getString(rootCipherDir, null) if (volume.isHidden){
if (cipherText != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ //password hash saved switch_hidden_volume.isChecked = true
loadPasswordHash(cipherText, ::openUsingPasswordHash) edit_volume_name.setText(currentVolumeName)
} else {
switch_hidden_volume.isChecked = false
edit_volume_path.setText(currentVolumeName)
}
onClickSwitchHiddenVolume(switch_hidden_volume)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
volume.hash?.let { hash ->
volume.iv?.let { iv ->
currentVolumePath = if (volume.isHidden){
PathUtils.pathJoin(filesDir.path, volume.name)
} else {
volume.name
}
loadPasswordHash(hash, iv, ::openUsingPasswordHash)
}
}
} }
} }
} else { } else {
WidgetUtil.hide(saved_path_listview) WidgetUtil.hideWithPadding(saved_path_listview)
} }
edit_volume_path.addTextChangedListener(object: TextWatcher { val textWatcher = object: TextWatcher {
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
} }
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
} }
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (savedVolumesAdapter.isPathSaved(s.toString())){ if (volumeDatabase.isVolumeSaved(s.toString())){
checkbox_remember_path.isEnabled = false checkbox_remember_path.isEnabled = false
checkbox_remember_path.isChecked = false checkbox_remember_path.isChecked = false
if (sharedPrefs.getString(s.toString(), null) != null){ if (volumeDatabase.isHashSaved(s.toString())){
checkbox_save_password.isEnabled = false checkbox_save_password.isEnabled = false
checkbox_save_password.isChecked = false checkbox_save_password.isChecked = false
} else { } else {
@ -71,7 +86,9 @@ class OpenActivity : VolumeActionActivity() {
checkbox_save_password.isEnabled = true checkbox_save_password.isEnabled = true
} }
} }
}) }
edit_volume_path.addTextChangedListener(textWatcher)
edit_volume_name.addTextChangedListener(textWatcher)
edit_password.setOnEditorActionListener { v, _, _ -> edit_password.setOnEditorActionListener { v, _, _ ->
onClickOpen(v) onClickOpen(v)
true true
@ -116,24 +133,15 @@ class OpenActivity : VolumeActionActivity() {
} }
fun onClickOpen(view: View?) { fun onClickOpen(view: View?) {
rootCipherDir = edit_volume_path.text.toString() loadVolumePath {
if (rootCipherDir.isEmpty()) { val volumeFile = File(currentVolumePath)
Toast.makeText(this, R.string.enter_volume_path, Toast.LENGTH_SHORT).show() if (!GocryptfsVolume.isGocryptfsVolume(volumeFile)){
} else {
val rootCipherDirFile = File(rootCipherDir)
if (!rootCipherDirFile.canRead()) {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(R.string.open_cant_read_error)
.setPositiveButton(R.string.ok, null)
.show()
} else if (!GocryptfsVolume.isGocryptfsVolume(rootCipherDirFile)){
ColoredAlertDialogBuilder(this) ColoredAlertDialogBuilder(this)
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(R.string.error_not_a_volume) .setMessage(R.string.error_not_a_volume)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} else if (!rootCipherDirFile.canWrite()) { } else if (!volumeFile.canWrite()) {
if ((intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE) && intent.extras != null) { //import via android share menu if ((intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE) && intent.extras != null) { //import via android share menu
ColoredAlertDialogBuilder(this) ColoredAlertDialogBuilder(this)
.setTitle(R.string.error) .setTitle(R.string.error)
@ -145,7 +153,7 @@ class OpenActivity : VolumeActionActivity() {
.setTitle(R.string.warning) .setTitle(R.string.warning)
.setCancelable(false) .setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> openVolume() } .setPositiveButton(R.string.ok) { _, _ -> openVolume() }
if (PathUtils.isPathOnExternalStorage(rootCipherDir, this)){ if (PathUtils.isPathOnExternalStorage(currentVolumeName, this)){
dialog.setMessage(R.string.open_on_sdcard_warning) dialog.setMessage(R.string.open_on_sdcard_warning)
} else { } else {
dialog.setMessage(R.string.open_cant_write_warning) dialog.setMessage(R.string.open_cant_write_warning)
@ -166,10 +174,10 @@ class OpenActivity : VolumeActionActivity() {
if (checkbox_save_password.isChecked){ if (checkbox_save_password.isChecked){
returnedHash = ByteArray(GocryptfsVolume.KeyLen) returnedHash = ByteArray(GocryptfsVolume.KeyLen)
} }
sessionID = GocryptfsVolume.init(rootCipherDir, password, null, returnedHash) sessionID = GocryptfsVolume.init(currentVolumePath, password, null, returnedHash)
if (sessionID != -1) { if (sessionID != -1) {
if (checkbox_remember_path.isChecked) { if (checkbox_remember_path.isChecked) {
savedVolumesAdapter.addVolumePath(rootCipherDir) volumeDatabase.saveVolume(Volume(currentVolumeName, switch_hidden_volume.isChecked))
} }
if (checkbox_save_password.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ if (checkbox_save_password.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
stopTask { stopTask {
@ -201,7 +209,7 @@ class OpenActivity : VolumeActionActivity() {
private fun openUsingPasswordHash(passwordHash: ByteArray){ private fun openUsingPasswordHash(passwordHash: ByteArray){
object : LoadingTask(this, R.string.loading_msg_open){ object : LoadingTask(this, R.string.loading_msg_open){
override fun doTask(activity: AppCompatActivity) { override fun doTask(activity: AppCompatActivity) {
sessionID = GocryptfsVolume.init(rootCipherDir, null, passwordHash, null) sessionID = GocryptfsVolume.init(currentVolumePath, null, passwordHash, null)
if (sessionID != -1){ if (sessionID != -1){
stopTask { startExplorer() } stopTask { startExplorer() }
} else { } else {
@ -236,7 +244,7 @@ class OpenActivity : VolumeActionActivity() {
explorerIntent = Intent(this, ExplorerActivity::class.java) //default opening explorerIntent = Intent(this, ExplorerActivity::class.java) //default opening
} }
explorerIntent.putExtra("sessionID", sessionID) explorerIntent.putExtra("sessionID", sessionID)
explorerIntent.putExtra("volume_name", File(rootCipherDir).name) explorerIntent.putExtra("volume_name", File(currentVolumeName).name)
startActivity(explorerIntent) startActivity(explorerIntent)
isFinishingIntentionally = true isFinishingIntentionally = true
finish() finish()

View File

@ -0,0 +1,3 @@
package sushi.hardcore.droidfs
class Volume(val name: String, val isHidden: Boolean = false, var hash: ByteArray? = null, var iv: ByteArray? = null)

View File

@ -6,8 +6,8 @@ import android.os.Build
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Base64
import android.view.View import android.view.View
import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager
@ -15,6 +15,8 @@ import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.checkboxes_section.* import kotlinx.android.synthetic.main.checkboxes_section.*
import kotlinx.android.synthetic.main.toolbar.* import kotlinx.android.synthetic.main.toolbar.*
import kotlinx.android.synthetic.main.volume_path_section.*
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.WidgetUtil import sushi.hardcore.droidfs.util.WidgetUtil
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
import java.security.KeyStore import java.security.KeyStore
@ -22,7 +24,9 @@ import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.GCMParameterSpec
open class VolumeActionActivity : BaseActivity() { open class VolumeActionActivity : BaseActivity() {
protected lateinit var rootCipherDir: String protected lateinit var currentVolumeName: String
protected lateinit var currentVolumePath: String
protected lateinit var volumeDatabase: VolumeDatabase
private var usf_fingerprint = false private var usf_fingerprint = false
private var biometricCanAuthenticateCode: Int = -1 private var biometricCanAuthenticateCode: Int = -1
private lateinit var biometricManager: BiometricManager private lateinit var biometricManager: BiometricManager
@ -30,10 +34,13 @@ open class VolumeActionActivity : BaseActivity() {
private lateinit var keyStore: KeyStore private lateinit var keyStore: KeyStore
private lateinit var key: SecretKey private lateinit var key: SecretKey
private lateinit var cipher: Cipher private lateinit var cipher: Cipher
private var isCipherReady = false
private var actionMode: Int? = null private var actionMode: Int? = null
private lateinit var onAuthenticationResult: (success: Boolean) -> Unit private lateinit var onAuthenticationResult: (success: Boolean) -> Unit
private lateinit var onPasswordDecrypted: (password: ByteArray) -> Unit private lateinit var onPasswordDecrypted: (password: ByteArray) -> Unit
private lateinit var dataToProcess: ByteArray private lateinit var dataToProcess: ByteArray
private lateinit var originalHiddenVolumeSectionLayoutParams: LinearLayout.LayoutParams
private lateinit var originalNormalVolumeSectionLayoutParams: LinearLayout.LayoutParams
companion object { companion object {
private const val ANDROID_KEY_STORE = "AndroidKeyStore" private const val ANDROID_KEY_STORE = "AndroidKeyStore"
private const val KEY_ALIAS = "Hash Key" private const val KEY_ALIAS = "Hash Key"
@ -42,6 +49,10 @@ open class VolumeActionActivity : BaseActivity() {
} }
protected fun setupFingerprintStuff(){ protected fun setupFingerprintStuff(){
originalHiddenVolumeSectionLayoutParams = hidden_volume_section.layoutParams as LinearLayout.LayoutParams
originalNormalVolumeSectionLayoutParams = normal_volume_section.layoutParams as LinearLayout.LayoutParams
WidgetUtil.hide(hidden_volume_section)
volumeDatabase = VolumeDatabase(this)
usf_fingerprint = sharedPrefs.getBoolean("usf_fingerprint", false) usf_fingerprint = sharedPrefs.getBoolean("usf_fingerprint", false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && usf_fingerprint) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && usf_fingerprint) {
biometricManager = BiometricManager.from(this) biometricManager = BiometricManager.from(this)
@ -73,12 +84,7 @@ open class VolumeActionActivity : BaseActivity() {
when (actionMode) { when (actionMode) {
Cipher.ENCRYPT_MODE -> { Cipher.ENCRYPT_MODE -> {
val cipherText = cipherObject.doFinal(dataToProcess) val cipherText = cipherObject.doFinal(dataToProcess)
val encodedCipherText = Base64.encodeToString(cipherText, 0) success = volumeDatabase.addHash(Volume(currentVolumeName, switch_hidden_volume.isChecked, cipherText, cipherObject.iv))
val encodedIv = Base64.encodeToString(cipherObject.iv, 0)
val sharedPrefsEditor = sharedPrefs.edit()
sharedPrefsEditor.putString(rootCipherDir, "$encodedIv:$encodedCipherText")
sharedPrefsEditor.apply()
success = true
} }
Cipher.DECRYPT_MODE -> { Cipher.DECRYPT_MODE -> {
try { try {
@ -117,7 +123,7 @@ open class VolumeActionActivity : BaseActivity() {
biometricPrompt = BiometricPrompt(this, executor, callback) biometricPrompt = BiometricPrompt(this, executor, callback)
} }
} else { } else {
WidgetUtil.hide(checkbox_save_password) WidgetUtil.hideWithPadding(checkbox_save_password)
} }
} }
@ -182,6 +188,7 @@ open class VolumeActionActivity : BaseActivity() {
keyGenerator.generateKey() keyGenerator.generateKey()
} }
cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES+"/"+KeyProperties.BLOCK_MODE_GCM+"/"+KeyProperties.ENCRYPTION_PADDING_NONE) cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES+"/"+KeyProperties.BLOCK_MODE_GCM+"/"+KeyProperties.ENCRYPTION_PADDING_NONE)
isCipherReady = true
} }
private fun alertKeyPermanentlyInvalidatedException(){ private fun alertKeyPermanentlyInvalidatedException(){
@ -198,14 +205,14 @@ open class VolumeActionActivity : BaseActivity() {
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
protected fun savePasswordHash(plainText: ByteArray, onAuthenticationResult: (success: Boolean) -> Unit){ protected fun savePasswordHash(plainText: ByteArray, onAuthenticationResult: (success: Boolean) -> Unit){
val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder() val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(rootCipherDir) .setTitle(currentVolumeName)
.setSubtitle(getString(R.string.encrypt_action_description)) .setSubtitle(getString(R.string.encrypt_action_description))
.setDescription(getString(R.string.fingerprint_instruction)) .setDescription(getString(R.string.fingerprint_instruction))
.setNegativeButtonText(getString(R.string.cancel)) .setNegativeButtonText(getString(R.string.cancel))
.setDeviceCredentialAllowed(false) .setDeviceCredentialAllowed(false)
.setConfirmationRequired(false) .setConfirmationRequired(false)
.build() .build()
if (!::cipher.isInitialized){ if (!isCipherReady){
prepareCipher() prepareCipher()
} }
actionMode = Cipher.ENCRYPT_MODE actionMode = Cipher.ENCRYPT_MODE
@ -220,9 +227,9 @@ open class VolumeActionActivity : BaseActivity() {
} }
@RequiresApi(Build.VERSION_CODES.M) @RequiresApi(Build.VERSION_CODES.M)
protected fun loadPasswordHash(cipherText: String, onPasswordDecrypted: (password: ByteArray) -> Unit){ protected fun loadPasswordHash(cipherText: ByteArray, iv: ByteArray, onPasswordDecrypted: (password: ByteArray) -> Unit){
val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder() val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(rootCipherDir) .setTitle(currentVolumeName)
.setSubtitle(getString(R.string.decrypt_action_description)) .setSubtitle(getString(R.string.decrypt_action_description))
.setDescription(getString(R.string.fingerprint_instruction)) .setDescription(getString(R.string.fingerprint_instruction))
.setNegativeButtonText(getString(R.string.cancel)) .setNegativeButtonText(getString(R.string.cancel))
@ -231,12 +238,10 @@ open class VolumeActionActivity : BaseActivity() {
.build() .build()
this.onPasswordDecrypted = onPasswordDecrypted this.onPasswordDecrypted = onPasswordDecrypted
actionMode = Cipher.DECRYPT_MODE actionMode = Cipher.DECRYPT_MODE
if (!::cipher.isInitialized){ if (!isCipherReady){
prepareCipher() prepareCipher()
} }
val encodedElements = cipherText.split(":") dataToProcess = cipherText
dataToProcess = Base64.decode(encodedElements[1], 0)
val iv = Base64.decode(encodedElements[0], 0)
val gcmSpec = GCMParameterSpec(GCM_TAG_LEN, iv) val gcmSpec = GCMParameterSpec(GCM_TAG_LEN, iv)
try { try {
cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec) cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec)
@ -248,15 +253,40 @@ open class VolumeActionActivity : BaseActivity() {
private fun resetHashStorage() { private fun resetHashStorage() {
keyStore.deleteEntry(KEY_ALIAS) keyStore.deleteEntry(KEY_ALIAS)
val savedVolumePaths = sharedPrefs.getStringSet(ConstValues.saved_volumes_key, HashSet<String>()) as Set<String> volumeDatabase.getVolumes().forEach { volume ->
val sharedPrefsEditor = sharedPrefs.edit() volumeDatabase.removeHash(volume)
for (path in savedVolumePaths){
val savedHash = sharedPrefs.getString(path, null)
if (savedHash != null){
sharedPrefsEditor.remove(path)
}
} }
sharedPrefsEditor.apply() isCipherReady = false
Toast.makeText(this, R.string.hash_storage_reset, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.hash_storage_reset, Toast.LENGTH_SHORT).show()
} }
protected fun loadVolumePath(callback: () -> Unit){
currentVolumeName = if (switch_hidden_volume.isChecked){
edit_volume_name.text.toString()
} else {
edit_volume_path.text.toString()
}
if (currentVolumeName.isEmpty()) {
Toast.makeText(this, if (switch_hidden_volume.isChecked) {R.string.enter_volume_name} else {R.string.enter_volume_path}, Toast.LENGTH_SHORT).show()
} else if (switch_hidden_volume.isChecked && currentVolumeName.contains("/")){
Toast.makeText(this, R.string.error_slash_in_name, Toast.LENGTH_SHORT).show()
} else {
currentVolumePath = if (switch_hidden_volume.isChecked) {
PathUtils.pathJoin(filesDir.path, currentVolumeName)
} else {
currentVolumeName
}
callback()
}
}
fun onClickSwitchHiddenVolume(view: View){
if (switch_hidden_volume.isChecked){
WidgetUtil.show(hidden_volume_section, originalHiddenVolumeSectionLayoutParams)
WidgetUtil.hide(normal_volume_section)
} else {
WidgetUtil.show(normal_volume_section, originalNormalVolumeSectionLayoutParams)
WidgetUtil.hide(hidden_volume_section)
}
}
} }

View File

@ -0,0 +1,96 @@
package sushi.hardcore.droidfs
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
class VolumeDatabase(context: Context): SQLiteOpenHelper(context,
ConstValues.volumeDatabaseName, null, 3) {
companion object {
const val TABLE_NAME = "Volumes"
const val COLUMN_NAME = "name"
const val COLUMN_HIDDEN = "hidden"
const val COLUMN_HASH = "hash"
const val COLUMN_IV = "iv"
private fun contentValuesFromVolume(volume: Volume): ContentValues {
val contentValues = ContentValues()
contentValues.put(COLUMN_NAME, volume.name)
contentValues.put(COLUMN_HIDDEN, volume.isHidden)
contentValues.put(COLUMN_HASH, volume.hash)
contentValues.put(COLUMN_IV, volume.iv)
return contentValues
}
}
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(
"CREATE TABLE IF NOT EXISTS $TABLE_NAME ($COLUMN_NAME TEXT PRIMARY KEY, $COLUMN_HIDDEN SHORT, $COLUMN_HASH BLOB, $COLUMN_IV BLOB);"
)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {}
fun isVolumeSaved(volumeName: String): Boolean {
val cursor = readableDatabase.query(TABLE_NAME, arrayOf(COLUMN_NAME), "$COLUMN_NAME=?", arrayOf(volumeName), null, null, null)
val result = cursor.count > 0
cursor.close()
return result
}
fun saveVolume(volume: Volume): Boolean {
if (!isVolumeSaved(volume.name)){
return (writableDatabase.insert(TABLE_NAME, null, contentValuesFromVolume(volume)) == 0.toLong())
}
return false
}
fun getVolumes(): List<Volume> {
val list: MutableList<Volume> = ArrayList()
val cursor = readableDatabase.rawQuery("SELECT * FROM $TABLE_NAME", null)
while (cursor.moveToNext()){
list.add(
Volume(
cursor.getString(cursor.getColumnIndex(COLUMN_NAME)),
cursor.getShort(cursor.getColumnIndex(COLUMN_HIDDEN)) == 1.toShort(),
cursor.getBlob(cursor.getColumnIndex(COLUMN_HASH)),
cursor.getBlob(cursor.getColumnIndex(COLUMN_IV))
)
)
}
cursor.close()
return list
}
fun isHashSaved(volumeName: String): Boolean {
val cursor = readableDatabase.query(TABLE_NAME, arrayOf(COLUMN_NAME, COLUMN_HASH), "$COLUMN_NAME=?", arrayOf(volumeName), null, null, null)
var isHashSaved = false
if (cursor.moveToNext()){
if (cursor.getBlob(cursor.getColumnIndex(COLUMN_HASH)) != null){
isHashSaved = true
}
}
cursor.close()
return isHashSaved
}
fun addHash(volume: Volume): Boolean {
return writableDatabase.update(TABLE_NAME, contentValuesFromVolume(volume), "$COLUMN_NAME=?", arrayOf(volume.name)) > 0
}
fun removeHash(volume: Volume): Boolean {
return writableDatabase.update(
TABLE_NAME, contentValuesFromVolume(
Volume(
volume.name,
volume.isHidden,
null,
null
)
), "$COLUMN_NAME=?", arrayOf(volume.name)) > 0
}
fun removeVolume(volume: Volume): Boolean {
return writableDatabase.delete(TABLE_NAME, "$COLUMN_NAME=?", arrayOf(volume.name)) > 0
}
}

View File

@ -1,86 +1,99 @@
package sushi.hardcore.droidfs.adapters package sushi.hardcore.droidfs.adapters
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.content.SharedPreferences.Editor
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.BaseAdapter import android.widget.BaseAdapter
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import sushi.hardcore.droidfs.ConstValues
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.Volume
import sushi.hardcore.droidfs.VolumeDatabase
import sushi.hardcore.droidfs.util.WidgetUtil import sushi.hardcore.droidfs.util.WidgetUtil
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.NonScrollableColoredBorderListView import sushi.hardcore.droidfs.widgets.NonScrollableColoredBorderListView
import java.util.* import java.io.File
class SavedVolumesAdapter(val context: Context, private val sharedPrefs: SharedPreferences) : BaseAdapter() { class SavedVolumesAdapter(private val context: Context, private val volumeDatabase: VolumeDatabase) : BaseAdapter() {
private val inflater: LayoutInflater = LayoutInflater.from(context) private val inflater: LayoutInflater = LayoutInflater.from(context)
private lateinit var nonScrollableColoredBorderListView: NonScrollableColoredBorderListView private lateinit var nonScrollableColoredBorderListView: NonScrollableColoredBorderListView
private val savedVolumesPaths: MutableList<String> = ArrayList()
private val sharedPrefsEditor: Editor = sharedPrefs.edit()
init {
val savedVolumesPathsSet = sharedPrefs.getStringSet(ConstValues.saved_volumes_key, HashSet()) as Set<String>
for (volume_path in savedVolumesPathsSet) {
savedVolumesPaths.add(volume_path)
}
}
private fun updateSharedPrefs() {
val savedVolumesPathsSet = savedVolumesPaths.toSet()
sharedPrefsEditor.remove(ConstValues.saved_volumes_key)
sharedPrefsEditor.putStringSet(ConstValues.saved_volumes_key, savedVolumesPathsSet)
sharedPrefsEditor.apply()
}
override fun getCount(): Int { override fun getCount(): Int {
return savedVolumesPaths.size return volumeDatabase.getVolumes().size
} }
override fun getItem(position: Int): String { override fun getItem(position: Int): Volume {
return savedVolumesPaths[position] return volumeDatabase.getVolumes()[position]
} }
override fun getItemId(position: Int): Long { override fun getItemId(position: Int): Long {
return 0 return 0
} }
private fun deletePasswordHash(volume: Volume){
volumeDatabase.removeHash(volume)
volume.hash = null
volume.iv = null
}
private fun deleteVolumeData(volume: Volume, parent: ViewGroup){
volumeDatabase.removeVolume(volume)
refresh(parent)
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
if (!::nonScrollableColoredBorderListView.isInitialized){ if (!::nonScrollableColoredBorderListView.isInitialized){
nonScrollableColoredBorderListView = parent as NonScrollableColoredBorderListView nonScrollableColoredBorderListView = parent as NonScrollableColoredBorderListView
} }
val view: View = convertView ?: inflater.inflate(R.layout.adapter_saved_volume, parent, false) val view: View = convertView ?: inflater.inflate(R.layout.adapter_saved_volume, parent, false)
val volumeNameTextview = view.findViewById<TextView>(R.id.volume_name_textview) val volumeNameTextView = view.findViewById<TextView>(R.id.volume_name_textview)
val currentVolume = getItem(position) val currentVolume = getItem(position)
volumeNameTextview.text = currentVolume volumeNameTextView.text = currentVolume.name
val deleteImageview = view.findViewById<ImageView>(R.id.delete_imageview) val deleteImageView = view.findViewById<ImageView>(R.id.delete_imageview)
deleteImageview.setOnClickListener { deleteImageView.setOnClickListener {
val volumePath = savedVolumesPaths[position]
val dialog = ColoredAlertDialogBuilder(context) val dialog = ColoredAlertDialogBuilder(context)
dialog.setTitle(R.string.warning) dialog.setTitle(R.string.warning)
if (sharedPrefs.getString(volumePath, null) != null){ if (currentVolume.isHidden){
dialog.setMessage(context.getString(R.string.delete_hash_or_all)) if (currentVolume.hash != null) {
dialog.setPositiveButton(context.getString(R.string.delete_all)) { _, _ -> dialog.setMessage(R.string.hidden_volume_delete_question_hash)
savedVolumesPaths.removeAt(position) dialog.setPositiveButton(R.string.password_hash){ _, _ ->
sharedPrefsEditor.remove(volumePath) deletePasswordHash(currentVolume)
updateSharedPrefs() }
refresh(parent) dialog.setNegativeButton(R.string.password_hash_and_path){ _, _ ->
} deleteVolumeData(currentVolume, parent)
dialog.setNegativeButton(context.getString(R.string.delete_hash)) { _, _ -> }
sharedPrefsEditor.remove(volumePath) dialog.setNeutralButton(R.string.whole_volume){ _, _ ->
sharedPrefsEditor.apply() PathUtils.recursiveRemoveDirectory(File(PathUtils.pathJoin(context.filesDir.path, currentVolume.name)))
deleteVolumeData(currentVolume, parent)
}
} else {
dialog.setMessage(R.string.hidden_volume_delete_question)
dialog.setPositiveButton(R.string.path_only){ _, _ ->
deleteVolumeData(currentVolume, parent)
}
dialog.setNegativeButton(R.string.whole_volume){ _, _ ->
PathUtils.recursiveRemoveDirectory(File(PathUtils.pathJoin(context.filesDir.path, currentVolume.name)))
deleteVolumeData(currentVolume, parent)
}
} }
} else { } else {
dialog.setMessage(context.getString(R.string.ask_delete_volume_path)) if (currentVolume.hash != null) {
dialog.setPositiveButton(R.string.ok) {_, _ -> dialog.setMessage(R.string.delete_hash_or_all)
savedVolumesPaths.removeAt(position) dialog.setNegativeButton(R.string.password_hash_and_path) { _, _ ->
updateSharedPrefs() deleteVolumeData(currentVolume, parent)
refresh(parent) }
dialog.setPositiveButton(R.string.password_hash) { _, _ ->
deletePasswordHash(currentVolume)
}
} else {
dialog.setMessage(R.string.ask_delete_volume_path)
dialog.setPositiveButton(R.string.ok) {_, _ ->
deleteVolumeData(currentVolume, parent)
}
dialog.setNegativeButton(R.string.cancel, null)
} }
dialog.setNegativeButton(R.string.cancel, null)
} }
dialog.show() dialog.show()
} }
@ -90,20 +103,9 @@ class SavedVolumesAdapter(val context: Context, private val sharedPrefs: SharedP
private fun refresh(parent: ViewGroup) { private fun refresh(parent: ViewGroup) {
notifyDataSetChanged() notifyDataSetChanged()
if (count == 0){ if (count == 0){
WidgetUtil.hide(parent) WidgetUtil.hideWithPadding(parent)
} else { } else {
nonScrollableColoredBorderListView.layoutParams.height = nonScrollableColoredBorderListView.computeHeight() nonScrollableColoredBorderListView.layoutParams.height = nonScrollableColoredBorderListView.computeHeight()
} }
} }
fun isPathSaved(volume_path: String): Boolean {
return savedVolumesPaths.contains(volume_path)
}
fun addVolumePath(volume_path: String) {
if (!isPathSaved(volume_path)) {
savedVolumesPaths.add(volume_path)
updateSharedPrefs()
}
}
} }

View File

@ -160,4 +160,19 @@ object PathUtils {
val split: Array<String?> = docId.split(":").toTypedArray() val split: Array<String?> = docId.split(":").toTypedArray()
return if (split.size >= 2 && split[1] != null) split[1] else File.separator return if (split.size >= 2 && split[1] != null) split[1] else File.separator
} }
fun recursiveRemoveDirectory(rootDirectory: File): Boolean {
rootDirectory.listFiles()?.forEach { item ->
if (item.isDirectory) {
if (!recursiveRemoveDirectory(item)){
return false
}
} else {
if (!item.delete()) {
return false
}
}
}
return rootDirectory.delete()
}
} }

View File

@ -1,13 +1,20 @@
package sushi.hardcore.droidfs.util package sushi.hardcore.droidfs.util
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
object WidgetUtil { object WidgetUtil {
fun hide(view: View){ fun hideWithPadding(view: View){
view.visibility = View.INVISIBLE view.visibility = View.INVISIBLE
view.setPadding(0, 0, 0, 0) view.setPadding(0, 0, 0, 0)
view.layoutParams = LinearLayout.LayoutParams(0, 0) view.layoutParams = LinearLayout.LayoutParams(0, 0)
} }
fun hide(view: View){
view.visibility = View.INVISIBLE
view.layoutParams = LinearLayout.LayoutParams(0, 0)
}
fun show(view: View, layoutParams: LinearLayout.LayoutParams){
view.visibility = View.VISIBLE
view.layoutParams = layoutParams
}
} }

View File

@ -1,29 +1,69 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<TextView <androidx.appcompat.widget.SwitchCompat
android:layout_width="@dimen/create_activity_label_width" android:id="@+id/switch_hidden_volume"
android:layout_height="match_parent" android:layout_width="wrap_content"
android:gravity="center_vertical"
android:text="@string/volume_path"
android:textSize="@dimen/edit_text_label_text_size" />
<sushi.hardcore.droidfs.widgets.ColoredEditText
android:id="@+id/edit_volume_path"
android:layout_width="0dp"
android:layout_weight="0.5"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="text" android:text="@string/hidden_volume"
android:maxLines="1"/> app:switchPadding="10dp"
android:layout_gravity="center_horizontal"
android:onClick="onClickSwitchHiddenVolume"/>
<sushi.hardcore.droidfs.widgets.ColoredImageButton <LinearLayout
android:layout_width="@dimen/image_button_size" android:id="@+id/normal_volume_section"
android:layout_height="@dimen/image_button_size" android:layout_width="match_parent"
android:onClick="pickDirectory" android:layout_height="match_parent">
android:scaleType="fitCenter"
android:background="#00000000" <TextView
android:src="@drawable/icon_folder" /> android:layout_width="@dimen/create_activity_label_width"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/volume_path"
android:textSize="@dimen/edit_text_label_text_size" />
<sushi.hardcore.droidfs.widgets.ColoredEditText
android:id="@+id/edit_volume_path"
android:layout_width="0dp"
android:layout_weight="0.5"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1"/>
<sushi.hardcore.droidfs.widgets.ColoredImageButton
android:layout_width="@dimen/image_button_size"
android:layout_height="@dimen/image_button_size"
android:onClick="pickDirectory"
android:scaleType="fitCenter"
android:background="#00000000"
android:src="@drawable/icon_folder" />
</LinearLayout>
<LinearLayout
android:id="@+id/hidden_volume_section"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="@dimen/create_activity_label_width"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/volume_name"
android:textSize="@dimen/edit_text_label_text_size" />
<sushi.hardcore.droidfs.widgets.ColoredEditText
android:id="@+id/edit_volume_name"
android:layout_width="0dp"
android:layout_weight="0.5"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1"/>
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -9,6 +9,7 @@
<string name="password">Password:</string> <string name="password">Password:</string>
<string name="password_confirm">Password (confirmation):</string> <string name="password_confirm">Password (confirmation):</string>
<string name="volume_path">Volume Path:</string> <string name="volume_path">Volume Path:</string>
<string name="volume_name">Volume Name:</string>
<string name="import_files">Import/Encrypt files</string> <string name="import_files">Import/Encrypt files</string>
<string name="mkdir">Create folder</string> <string name="mkdir">Create folder</string>
<string name="dir_empty">Directory Empty</string> <string name="dir_empty">Directory Empty</string>
@ -42,6 +43,7 @@
<string name="get_size_failed">Failed to retrieve file size.</string> <string name="get_size_failed">Failed to retrieve file size.</string>
<string name="parent_folder">Parent Folder</string> <string name="parent_folder">Parent Folder</string>
<string name="enter_volume_path">Please enter the volume path</string> <string name="enter_volume_path">Please enter the volume path</string>
<string name="enter_volume_name">Please enter the volume name</string>
<string name="external_open">Open with external app</string> <string name="external_open">Open with external app</string>
<string name="single_delete_confirm">Are you sure you want to delete %s ?</string> <string name="single_delete_confirm">Are you sure you want to delete %s ?</string>
<string name="multiple_delete_confirm">Are you sure you want to delete these %s items ?</string> <string name="multiple_delete_confirm">Are you sure you want to delete these %s items ?</string>
@ -76,10 +78,14 @@
<string name="fingerprint_instruction">Please touch the fingerprint sensor</string> <string name="fingerprint_instruction">Please touch the fingerprint sensor</string>
<string name="open_failed_hash_msg">Failed to open the volume. The password may have changed.</string> <string name="open_failed_hash_msg">Failed to open the volume. The password may have changed.</string>
<string name="authentication_failed">Authentication failed</string> <string name="authentication_failed">Authentication failed</string>
<string name="delete_hash_or_all">Delete only password hash or all saved volume data ? (This won\'t delete the volume itself)</string> <string name="delete_hash_or_all">Delete only the password hash or the password hash and the saved path ? (This won\'t delete the volume itself)</string>
<string name="delete_all">Delete all</string> <string name="password_hash_and_path">Password hash and path</string>
<string name="delete_hash">Delete password hash</string> <string name="password_hash">Password hash</string>
<string name="ask_delete_volume_path">Are you sure you want to forget this volume path ? (This won\'t delete the volume itself)</string> <string name="ask_delete_volume_path">Are you sure you want to forget this volume path ? (This won\'t delete the volume itself)</string>
<string name="hidden_volume_delete_question_hash">Delete only the password hash, the password hash and the saved path, or the WHOLE volume (including its content) ?</string>
<string name="whole_volume">Whole volume</string>
<string name="hidden_volume_delete_question">Delete only the path or the whole volume ITSELF (including its content) ?</string>
<string name="path_only">Path only</string>
<string name="illegal_block_size_exception">IllegalBlockSizeException</string> <string name="illegal_block_size_exception">IllegalBlockSizeException</string>
<string name="illegal_block_size_exception_msg">This can happen if you have added a new fingerprint. Resetting hash storage can solve this problem.</string> <string name="illegal_block_size_exception_msg">This can happen if you have added a new fingerprint. Resetting hash storage can solve this problem.</string>
<string name="reset_hash_storage">Reset hash storage</string> <string name="reset_hash_storage">Reset hash storage</string>
@ -167,7 +173,6 @@
<string name="open_on_sdcard_warning">DroidFS can\'t write on removable SD cards. Opening volume with read-only access.</string> <string name="open_on_sdcard_warning">DroidFS can\'t write on removable SD cards. Opening volume with read-only access.</string>
<string name="open_cant_write_warning">DroidFS doesn\'t have write access to this path. Opening volume with read-only access.</string> <string name="open_cant_write_warning">DroidFS doesn\'t have write access to this path. Opening volume with read-only access.</string>
<string name="open_cant_write_error_msg">DroidFS doesn\'t have write access to this path. Please try another volume.</string> <string name="open_cant_write_error_msg">DroidFS doesn\'t have write access to this path. Please try another volume.</string>
<string name="open_cant_read_error">DroidFS doesn\'t have read access to this path. You can try to move the volume to a readable location.</string>
<string name="change_pwd_cant_write_error_msg">DroidFS doesn\'t have write access to this path. You can try to move the volume to a writable location.</string> <string name="change_pwd_cant_write_error_msg">DroidFS doesn\'t have write access to this path. You can try to move the volume to a writable location.</string>
<string name="change_pwd_on_sdcard_error_msg">DroidFS can\'t write on removable SD cards, please move the volume to internal storage.</string> <string name="change_pwd_on_sdcard_error_msg">DroidFS can\'t write on removable SD cards, please move the volume to internal storage.</string>
<string name="slideshow_stopped">Slideshow stopped</string> <string name="slideshow_stopped">Slideshow stopped</string>
@ -185,4 +190,6 @@
<string name="usf_read_doc">You should read it carefully before enabling any of these options.</string> <string name="usf_read_doc">You should read it carefully before enabling any of these options.</string>
<string name="usf_doc">Unsafe features documentation</string> <string name="usf_doc">Unsafe features documentation</string>
<string name="error_retrieving_filename">Unable to retrieve file name for URI: %s</string> <string name="error_retrieving_filename">Unable to retrieve file name for URI: %s</string>
<string name="hidden_volume">Hidden Volume</string>
<string name="error_slash_in_name">Volume name cannot contain slashes</string>
</resources> </resources>

View File

@ -1,63 +1,61 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory <PreferenceCategory android:title="@string/ui">
android:title="@string/ui">
<SwitchPreference <SwitchPreference
android:defaultValue="false"
android:icon="@drawable/icon_screenshot" android:icon="@drawable/icon_screenshot"
android:key="usf_screenshot" android:key="usf_screenshot"
android:title="@string/usf_screenshot" android:title="@string/usf_screenshot" />
android:defaultValue="false"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory android:title="@string/explorer">
android:title="@string/explorer">
<SwitchPreference <SwitchPreference
android:icon="@drawable/icon_open_in_new" android:defaultValue="false"
android:key="usf_open"
android:title="@string/usf_open"
android:defaultValue="false"/>
<SwitchPreference
android:icon="@drawable/icon_decrypt" android:icon="@drawable/icon_decrypt"
android:key="usf_decrypt" android:key="usf_decrypt"
android:title="@string/usf_decrypt" android:title="@string/usf_decrypt" />
android:defaultValue="false"/>
<SwitchPreference <SwitchPreference
android:defaultValue="false"
android:icon="@drawable/icon_open_in_new"
android:key="usf_open"
android:title="@string/usf_open" />
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/icon_share" android:icon="@drawable/icon_share"
android:key="usf_share" android:key="usf_share"
android:title="@string/usf_share" android:title="@string/usf_share" />
android:defaultValue="false"/>
<SwitchPreference <SwitchPreference
android:defaultValue="false"
android:icon="@drawable/icon_decrypt" android:icon="@drawable/icon_decrypt"
android:key="usf_keep_open" android:key="usf_keep_open"
android:title="@string/usf_keep_open" android:title="@string/usf_keep_open" />
android:defaultValue="false"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory android:title="@string/usf_volume_management">
android:title="@string/usf_volume_management">
<SwitchPreference <SwitchPreference
android:defaultValue="false"
android:icon="@drawable/icon_fingerprint" android:icon="@drawable/icon_fingerprint"
android:key="usf_fingerprint" android:key="usf_fingerprint"
android:title="@string/usf_fingerprint" android:title="@string/usf_fingerprint" />
android:defaultValue="false"/>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory android:title="@string/about">
android:title="@string/about">
<Preference <Preference
android:title="@string/usf_doc" android:icon="@drawable/icon_notes"
android:summary="@string/usf_read_doc" android:summary="@string/usf_read_doc"
android:icon="@drawable/icon_notes"> android:title="@string/usf_doc">
<intent android:action="android.intent.action.VIEW" android:data="https://github.com/hardcore-sushi/DroidFS#unsafe-features"/> <intent
android:action="android.intent.action.VIEW"
android:data="https://github.com/hardcore-sushi/DroidFS#unsafe-features" />
</Preference> </Preference>
</PreferenceCategory> </PreferenceCategory>