SavedVolume database & Hidden volume feature
This commit is contained in:
parent
0b509c2f98
commit
a4df9d3ffa
@ -56,6 +56,6 @@ dependencies {
|
||||
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-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"
|
||||
}
|
||||
|
@ -29,25 +29,34 @@ class ChangePasswordActivity : VolumeActionActivity() {
|
||||
setContentView(R.layout.activity_change_password)
|
||||
setupActionBar()
|
||||
setupFingerprintStuff()
|
||||
savedVolumesAdapter = SavedVolumesAdapter(this, sharedPrefs)
|
||||
savedVolumesAdapter = SavedVolumesAdapter(this, volumeDatabase)
|
||||
if (savedVolumesAdapter.count > 0){
|
||||
saved_path_listview.adapter = savedVolumesAdapter
|
||||
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 {
|
||||
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 beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: 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.isChecked = false
|
||||
if (sharedPrefs.getString(s.toString(), null) != null){
|
||||
if (volumeDatabase.isHashSaved(s.toString())){
|
||||
edit_old_password.text = null
|
||||
edit_old_password.hint = getString(R.string.hash_saved_hint)
|
||||
edit_old_password.isEnabled = false
|
||||
@ -61,7 +70,9 @@ class ChangePasswordActivity : VolumeActionActivity() {
|
||||
edit_old_password.isEnabled = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
edit_volume_path.addTextChangedListener(textWatcher)
|
||||
edit_volume_name.addTextChangedListener(textWatcher)
|
||||
edit_new_password_confirm.setOnEditorActionListener { v, _, _ ->
|
||||
onClickChangePassword(v)
|
||||
true
|
||||
@ -102,30 +113,27 @@ class ChangePasswordActivity : VolumeActionActivity() {
|
||||
}
|
||||
|
||||
fun onClickChangePassword(view: View?) {
|
||||
rootCipherDir = edit_volume_path.text.toString()
|
||||
if (rootCipherDir.isEmpty()) {
|
||||
Toast.makeText(this, R.string.enter_volume_path, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
val rootCipherDirFile = File(rootCipherDir)
|
||||
if (!GocryptfsVolume.isGocryptfsVolume(rootCipherDirFile)){
|
||||
loadVolumePath {
|
||||
val volumeFile = File(currentVolumePath)
|
||||
if (!GocryptfsVolume.isGocryptfsVolume(volumeFile)){
|
||||
ColoredAlertDialogBuilder(this)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.error_not_a_volume)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
} else if (!rootCipherDirFile.canWrite()){
|
||||
} else if (!volumeFile.canWrite()){
|
||||
ColoredAlertDialogBuilder(this)
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(R.string.change_pwd_cant_write_error_msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
} else {
|
||||
changePassword(null)
|
||||
changePassword()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun changePassword(givenHash: ByteArray?){
|
||||
private fun changePassword(givenHash: ByteArray? = null){
|
||||
val newPassword = edit_new_password.text.toString().toCharArray()
|
||||
val newPasswordConfirm = edit_new_password_confirm.text.toString().toCharArray()
|
||||
if (!newPassword.contentEquals(newPasswordConfirm)) {
|
||||
@ -140,30 +148,38 @@ class ChangePasswordActivity : VolumeActionActivity() {
|
||||
}
|
||||
var changePasswordImmediately = true
|
||||
if (givenHash == null) {
|
||||
val cipherText = sharedPrefs.getString(rootCipherDir, null)
|
||||
if (cipherText != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { //password hash saved
|
||||
stopTask {
|
||||
loadPasswordHash(cipherText, ::changePassword)
|
||||
var volume: Volume? = null
|
||||
volumeDatabase.getVolumes().forEach { testVolume ->
|
||||
if (testVolume.name == currentVolumeName){
|
||||
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 (GocryptfsVolume.changePassword(
|
||||
rootCipherDir,
|
||||
oldPassword,
|
||||
givenHash,
|
||||
newPassword,
|
||||
returnedHash
|
||||
)
|
||||
) {
|
||||
if (sharedPrefs.getString(rootCipherDir, null) != null) {
|
||||
val editor = sharedPrefs.edit()
|
||||
editor.remove(rootCipherDir)
|
||||
editor.apply()
|
||||
if (GocryptfsVolume.changePassword(currentVolumePath, oldPassword, givenHash, newPassword, returnedHash)) {
|
||||
val volume = Volume(currentVolumeName, switch_hidden_volume.isChecked)
|
||||
if (volumeDatabase.isHashSaved(currentVolumeName)) {
|
||||
volumeDatabase.removeHash(volume)
|
||||
}
|
||||
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){
|
||||
stopTask {
|
||||
|
@ -7,7 +7,7 @@ class ConstValues {
|
||||
companion object {
|
||||
const val creator = "DroidFS"
|
||||
const val gocryptfsConfFilename = "gocryptfs.conf"
|
||||
const val saved_volumes_key = "saved_volumes"
|
||||
const val volumeDatabaseName = "SavedVolumes"
|
||||
const val sort_order_key = "sort_order"
|
||||
val fakeUri: Uri = Uri.parse("fakeuri://droidfs")
|
||||
const val MAX_KERNEL_WRITE = 128*1024
|
||||
|
@ -11,10 +11,7 @@ import kotlinx.android.synthetic.main.activity_create.*
|
||||
import kotlinx.android.synthetic.main.checkboxes_section.*
|
||||
import kotlinx.android.synthetic.main.volume_path_section.*
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerActivity
|
||||
import sushi.hardcore.droidfs.util.GocryptfsVolume
|
||||
import sushi.hardcore.droidfs.util.LoadingTask
|
||||
import sushi.hardcore.droidfs.util.PathUtils
|
||||
import sushi.hardcore.droidfs.util.Wiper
|
||||
import sushi.hardcore.droidfs.util.*
|
||||
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
@ -69,10 +66,7 @@ class CreateActivity : VolumeActionActivity() {
|
||||
}
|
||||
|
||||
fun onClickCreate(view: View?) {
|
||||
rootCipherDir = edit_volume_path.text.toString()
|
||||
if (rootCipherDir.isEmpty()) {
|
||||
Toast.makeText(this, R.string.enter_volume_path, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
loadVolumePath {
|
||||
val password = edit_password.text.toString().toCharArray()
|
||||
val passwordConfirm = edit_password_confirm.text.toString().toCharArray()
|
||||
if (!password.contentEquals(passwordConfirm)) {
|
||||
@ -80,10 +74,10 @@ class CreateActivity : VolumeActionActivity() {
|
||||
} else {
|
||||
object: LoadingTask(this, R.string.loading_msg_create){
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
val volumePathFile = File(rootCipherDir)
|
||||
val volumeFile = File(currentVolumePath)
|
||||
var goodDirectory = false
|
||||
if (!volumePathFile.isDirectory) {
|
||||
if (volumePathFile.mkdirs()) {
|
||||
if (!volumeFile.isDirectory) {
|
||||
if (volumeFile.mkdirs()) {
|
||||
goodDirectory = true
|
||||
} else {
|
||||
stopTask {
|
||||
@ -95,10 +89,10 @@ class CreateActivity : VolumeActionActivity() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val dirContent = volumePathFile.list()
|
||||
val dirContent = volumeFile.list()
|
||||
if (dirContent != null){
|
||||
if (dirContent.isEmpty()) {
|
||||
if (volumePathFile.canWrite()){
|
||||
if (volumeFile.canWrite()){
|
||||
goodDirectory = true
|
||||
} else {
|
||||
stopTask {
|
||||
@ -117,26 +111,18 @@ class CreateActivity : VolumeActionActivity() {
|
||||
}
|
||||
}
|
||||
if (goodDirectory) {
|
||||
if (GocryptfsVolume.createVolume(rootCipherDir, password, GocryptfsVolume.ScryptDefaultLogN, ConstValues.creator)) {
|
||||
if (GocryptfsVolume.createVolume(currentVolumePath, password, GocryptfsVolume.ScryptDefaultLogN, ConstValues.creator)) {
|
||||
var returnedHash: ByteArray? = null
|
||||
if (checkbox_save_password.isChecked){
|
||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||
}
|
||||
sessionID = GocryptfsVolume.init(rootCipherDir, password, null, returnedHash)
|
||||
sessionID = GocryptfsVolume.init(currentVolumePath, password, null, returnedHash)
|
||||
if (sessionID != -1) {
|
||||
if (checkbox_remember_path.isChecked) {
|
||||
val oldSavedVolumesPaths = sharedPrefs.getStringSet(ConstValues.saved_volumes_key, HashSet()) as Set<String>
|
||||
val editor = sharedPrefs.edit()
|
||||
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())
|
||||
if (volumeDatabase.isVolumeSaved(currentVolumeName)) {
|
||||
volumeDatabase.removeVolume(Volume(currentVolumeName))
|
||||
}
|
||||
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){
|
||||
stopTask {
|
||||
@ -178,7 +164,7 @@ class CreateActivity : VolumeActionActivity() {
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
val intent = Intent(applicationContext, ExplorerActivity::class.java)
|
||||
intent.putExtra("sessionID", sessionID)
|
||||
intent.putExtra("volume_name", File(rootCipherDir).name)
|
||||
intent.putExtra("volume_name", File(currentVolumeName).name)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import android.text.TextWatcher
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.AdapterView.OnItemClickListener
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import kotlinx.android.synthetic.main.activity_open.*
|
||||
import kotlinx.android.synthetic.main.checkboxes_section.*
|
||||
@ -37,30 +36,46 @@ class OpenActivity : VolumeActionActivity() {
|
||||
setContentView(R.layout.activity_open)
|
||||
setupActionBar()
|
||||
setupFingerprintStuff()
|
||||
savedVolumesAdapter = SavedVolumesAdapter(this, sharedPrefs)
|
||||
savedVolumesAdapter = SavedVolumesAdapter(this, volumeDatabase)
|
||||
if (savedVolumesAdapter.count > 0){
|
||||
saved_path_listview.adapter = savedVolumesAdapter
|
||||
saved_path_listview.onItemClickListener = OnItemClickListener { _, _, position, _ ->
|
||||
rootCipherDir = savedVolumesAdapter.getItem(position)
|
||||
edit_volume_path.setText(rootCipherDir)
|
||||
val cipherText = sharedPrefs.getString(rootCipherDir, null)
|
||||
if (cipherText != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){ //password hash saved
|
||||
loadPasswordHash(cipherText, ::openUsingPasswordHash)
|
||||
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)
|
||||
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 {
|
||||
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 beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: 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.isChecked = false
|
||||
if (sharedPrefs.getString(s.toString(), null) != null){
|
||||
if (volumeDatabase.isHashSaved(s.toString())){
|
||||
checkbox_save_password.isEnabled = false
|
||||
checkbox_save_password.isChecked = false
|
||||
} else {
|
||||
@ -71,7 +86,9 @@ class OpenActivity : VolumeActionActivity() {
|
||||
checkbox_save_password.isEnabled = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
edit_volume_path.addTextChangedListener(textWatcher)
|
||||
edit_volume_name.addTextChangedListener(textWatcher)
|
||||
edit_password.setOnEditorActionListener { v, _, _ ->
|
||||
onClickOpen(v)
|
||||
true
|
||||
@ -116,24 +133,15 @@ class OpenActivity : VolumeActionActivity() {
|
||||
}
|
||||
|
||||
fun onClickOpen(view: View?) {
|
||||
rootCipherDir = edit_volume_path.text.toString()
|
||||
if (rootCipherDir.isEmpty()) {
|
||||
Toast.makeText(this, R.string.enter_volume_path, Toast.LENGTH_SHORT).show()
|
||||
} 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)){
|
||||
loadVolumePath {
|
||||
val volumeFile = File(currentVolumePath)
|
||||
if (!GocryptfsVolume.isGocryptfsVolume(volumeFile)){
|
||||
ColoredAlertDialogBuilder(this)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.error_not_a_volume)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.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
|
||||
ColoredAlertDialogBuilder(this)
|
||||
.setTitle(R.string.error)
|
||||
@ -145,7 +153,7 @@ class OpenActivity : VolumeActionActivity() {
|
||||
.setTitle(R.string.warning)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { _, _ -> openVolume() }
|
||||
if (PathUtils.isPathOnExternalStorage(rootCipherDir, this)){
|
||||
if (PathUtils.isPathOnExternalStorage(currentVolumeName, this)){
|
||||
dialog.setMessage(R.string.open_on_sdcard_warning)
|
||||
} else {
|
||||
dialog.setMessage(R.string.open_cant_write_warning)
|
||||
@ -166,10 +174,10 @@ class OpenActivity : VolumeActionActivity() {
|
||||
if (checkbox_save_password.isChecked){
|
||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||
}
|
||||
sessionID = GocryptfsVolume.init(rootCipherDir, password, null, returnedHash)
|
||||
sessionID = GocryptfsVolume.init(currentVolumePath, password, null, returnedHash)
|
||||
if (sessionID != -1) {
|
||||
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){
|
||||
stopTask {
|
||||
@ -201,7 +209,7 @@ class OpenActivity : VolumeActionActivity() {
|
||||
private fun openUsingPasswordHash(passwordHash: ByteArray){
|
||||
object : LoadingTask(this, R.string.loading_msg_open){
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
sessionID = GocryptfsVolume.init(rootCipherDir, null, passwordHash, null)
|
||||
sessionID = GocryptfsVolume.init(currentVolumePath, null, passwordHash, null)
|
||||
if (sessionID != -1){
|
||||
stopTask { startExplorer() }
|
||||
} else {
|
||||
@ -236,7 +244,7 @@ class OpenActivity : VolumeActionActivity() {
|
||||
explorerIntent = Intent(this, ExplorerActivity::class.java) //default opening
|
||||
}
|
||||
explorerIntent.putExtra("sessionID", sessionID)
|
||||
explorerIntent.putExtra("volume_name", File(rootCipherDir).name)
|
||||
explorerIntent.putExtra("volume_name", File(currentVolumeName).name)
|
||||
startActivity(explorerIntent)
|
||||
isFinishingIntentionally = true
|
||||
finish()
|
||||
|
3
app/src/main/java/sushi/hardcore/droidfs/Volume.kt
Normal file
3
app/src/main/java/sushi/hardcore/droidfs/Volume.kt
Normal 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)
|
@ -6,8 +6,8 @@ import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Base64
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
@ -15,6 +15,8 @@ import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.android.synthetic.main.checkboxes_section.*
|
||||
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.widgets.ColoredAlertDialogBuilder
|
||||
import java.security.KeyStore
|
||||
@ -22,7 +24,9 @@ import javax.crypto.*
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
|
||||
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 biometricCanAuthenticateCode: Int = -1
|
||||
private lateinit var biometricManager: BiometricManager
|
||||
@ -30,10 +34,13 @@ open class VolumeActionActivity : BaseActivity() {
|
||||
private lateinit var keyStore: KeyStore
|
||||
private lateinit var key: SecretKey
|
||||
private lateinit var cipher: Cipher
|
||||
private var isCipherReady = false
|
||||
private var actionMode: Int? = null
|
||||
private lateinit var onAuthenticationResult: (success: Boolean) -> Unit
|
||||
private lateinit var onPasswordDecrypted: (password: ByteArray) -> Unit
|
||||
private lateinit var dataToProcess: ByteArray
|
||||
private lateinit var originalHiddenVolumeSectionLayoutParams: LinearLayout.LayoutParams
|
||||
private lateinit var originalNormalVolumeSectionLayoutParams: LinearLayout.LayoutParams
|
||||
companion object {
|
||||
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
|
||||
private const val KEY_ALIAS = "Hash Key"
|
||||
@ -42,6 +49,10 @@ open class VolumeActionActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
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)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && usf_fingerprint) {
|
||||
biometricManager = BiometricManager.from(this)
|
||||
@ -73,12 +84,7 @@ open class VolumeActionActivity : BaseActivity() {
|
||||
when (actionMode) {
|
||||
Cipher.ENCRYPT_MODE -> {
|
||||
val cipherText = cipherObject.doFinal(dataToProcess)
|
||||
val encodedCipherText = Base64.encodeToString(cipherText, 0)
|
||||
val encodedIv = Base64.encodeToString(cipherObject.iv, 0)
|
||||
val sharedPrefsEditor = sharedPrefs.edit()
|
||||
sharedPrefsEditor.putString(rootCipherDir, "$encodedIv:$encodedCipherText")
|
||||
sharedPrefsEditor.apply()
|
||||
success = true
|
||||
success = volumeDatabase.addHash(Volume(currentVolumeName, switch_hidden_volume.isChecked, cipherText, cipherObject.iv))
|
||||
}
|
||||
Cipher.DECRYPT_MODE -> {
|
||||
try {
|
||||
@ -117,7 +123,7 @@ open class VolumeActionActivity : BaseActivity() {
|
||||
biometricPrompt = BiometricPrompt(this, executor, callback)
|
||||
}
|
||||
} else {
|
||||
WidgetUtil.hide(checkbox_save_password)
|
||||
WidgetUtil.hideWithPadding(checkbox_save_password)
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,6 +188,7 @@ open class VolumeActionActivity : BaseActivity() {
|
||||
keyGenerator.generateKey()
|
||||
}
|
||||
cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES+"/"+KeyProperties.BLOCK_MODE_GCM+"/"+KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
isCipherReady = true
|
||||
}
|
||||
|
||||
private fun alertKeyPermanentlyInvalidatedException(){
|
||||
@ -198,14 +205,14 @@ open class VolumeActionActivity : BaseActivity() {
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
protected fun savePasswordHash(plainText: ByteArray, onAuthenticationResult: (success: Boolean) -> Unit){
|
||||
val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(rootCipherDir)
|
||||
.setTitle(currentVolumeName)
|
||||
.setSubtitle(getString(R.string.encrypt_action_description))
|
||||
.setDescription(getString(R.string.fingerprint_instruction))
|
||||
.setNegativeButtonText(getString(R.string.cancel))
|
||||
.setDeviceCredentialAllowed(false)
|
||||
.setConfirmationRequired(false)
|
||||
.build()
|
||||
if (!::cipher.isInitialized){
|
||||
if (!isCipherReady){
|
||||
prepareCipher()
|
||||
}
|
||||
actionMode = Cipher.ENCRYPT_MODE
|
||||
@ -220,9 +227,9 @@ open class VolumeActionActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
@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()
|
||||
.setTitle(rootCipherDir)
|
||||
.setTitle(currentVolumeName)
|
||||
.setSubtitle(getString(R.string.decrypt_action_description))
|
||||
.setDescription(getString(R.string.fingerprint_instruction))
|
||||
.setNegativeButtonText(getString(R.string.cancel))
|
||||
@ -231,12 +238,10 @@ open class VolumeActionActivity : BaseActivity() {
|
||||
.build()
|
||||
this.onPasswordDecrypted = onPasswordDecrypted
|
||||
actionMode = Cipher.DECRYPT_MODE
|
||||
if (!::cipher.isInitialized){
|
||||
if (!isCipherReady){
|
||||
prepareCipher()
|
||||
}
|
||||
val encodedElements = cipherText.split(":")
|
||||
dataToProcess = Base64.decode(encodedElements[1], 0)
|
||||
val iv = Base64.decode(encodedElements[0], 0)
|
||||
dataToProcess = cipherText
|
||||
val gcmSpec = GCMParameterSpec(GCM_TAG_LEN, iv)
|
||||
try {
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec)
|
||||
@ -248,15 +253,40 @@ open class VolumeActionActivity : BaseActivity() {
|
||||
|
||||
private fun resetHashStorage() {
|
||||
keyStore.deleteEntry(KEY_ALIAS)
|
||||
val savedVolumePaths = sharedPrefs.getStringSet(ConstValues.saved_volumes_key, HashSet<String>()) as Set<String>
|
||||
val sharedPrefsEditor = sharedPrefs.edit()
|
||||
for (path in savedVolumePaths){
|
||||
val savedHash = sharedPrefs.getString(path, null)
|
||||
if (savedHash != null){
|
||||
sharedPrefsEditor.remove(path)
|
||||
}
|
||||
volumeDatabase.getVolumes().forEach { volume ->
|
||||
volumeDatabase.removeHash(volume)
|
||||
}
|
||||
sharedPrefsEditor.apply()
|
||||
isCipherReady = false
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
96
app/src/main/java/sushi/hardcore/droidfs/VolumeDatabase.kt
Normal file
96
app/src/main/java/sushi/hardcore/droidfs/VolumeDatabase.kt
Normal 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
|
||||
}
|
||||
}
|
@ -1,86 +1,99 @@
|
||||
package sushi.hardcore.droidfs.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.Editor
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import sushi.hardcore.droidfs.ConstValues
|
||||
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.widgets.ColoredAlertDialogBuilder
|
||||
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 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 {
|
||||
return savedVolumesPaths.size
|
||||
return volumeDatabase.getVolumes().size
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): String {
|
||||
return savedVolumesPaths[position]
|
||||
override fun getItem(position: Int): Volume {
|
||||
return volumeDatabase.getVolumes()[position]
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
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 {
|
||||
if (!::nonScrollableColoredBorderListView.isInitialized){
|
||||
nonScrollableColoredBorderListView = parent as NonScrollableColoredBorderListView
|
||||
}
|
||||
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)
|
||||
volumeNameTextview.text = currentVolume
|
||||
val deleteImageview = view.findViewById<ImageView>(R.id.delete_imageview)
|
||||
deleteImageview.setOnClickListener {
|
||||
val volumePath = savedVolumesPaths[position]
|
||||
volumeNameTextView.text = currentVolume.name
|
||||
val deleteImageView = view.findViewById<ImageView>(R.id.delete_imageview)
|
||||
deleteImageView.setOnClickListener {
|
||||
val dialog = ColoredAlertDialogBuilder(context)
|
||||
dialog.setTitle(R.string.warning)
|
||||
if (sharedPrefs.getString(volumePath, null) != null){
|
||||
dialog.setMessage(context.getString(R.string.delete_hash_or_all))
|
||||
dialog.setPositiveButton(context.getString(R.string.delete_all)) { _, _ ->
|
||||
savedVolumesPaths.removeAt(position)
|
||||
sharedPrefsEditor.remove(volumePath)
|
||||
updateSharedPrefs()
|
||||
refresh(parent)
|
||||
}
|
||||
dialog.setNegativeButton(context.getString(R.string.delete_hash)) { _, _ ->
|
||||
sharedPrefsEditor.remove(volumePath)
|
||||
sharedPrefsEditor.apply()
|
||||
if (currentVolume.isHidden){
|
||||
if (currentVolume.hash != null) {
|
||||
dialog.setMessage(R.string.hidden_volume_delete_question_hash)
|
||||
dialog.setPositiveButton(R.string.password_hash){ _, _ ->
|
||||
deletePasswordHash(currentVolume)
|
||||
}
|
||||
dialog.setNegativeButton(R.string.password_hash_and_path){ _, _ ->
|
||||
deleteVolumeData(currentVolume, parent)
|
||||
}
|
||||
dialog.setNeutralButton(R.string.whole_volume){ _, _ ->
|
||||
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 {
|
||||
dialog.setMessage(context.getString(R.string.ask_delete_volume_path))
|
||||
dialog.setPositiveButton(R.string.ok) {_, _ ->
|
||||
savedVolumesPaths.removeAt(position)
|
||||
updateSharedPrefs()
|
||||
refresh(parent)
|
||||
if (currentVolume.hash != null) {
|
||||
dialog.setMessage(R.string.delete_hash_or_all)
|
||||
dialog.setNegativeButton(R.string.password_hash_and_path) { _, _ ->
|
||||
deleteVolumeData(currentVolume, 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()
|
||||
}
|
||||
@ -90,20 +103,9 @@ class SavedVolumesAdapter(val context: Context, private val sharedPrefs: SharedP
|
||||
private fun refresh(parent: ViewGroup) {
|
||||
notifyDataSetChanged()
|
||||
if (count == 0){
|
||||
WidgetUtil.hide(parent)
|
||||
WidgetUtil.hideWithPadding(parent)
|
||||
} else {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
@ -160,4 +160,19 @@ object PathUtils {
|
||||
val split: Array<String?> = docId.split(":").toTypedArray()
|
||||
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()
|
||||
}
|
||||
}
|
@ -1,13 +1,20 @@
|
||||
package sushi.hardcore.droidfs.util
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
|
||||
object WidgetUtil {
|
||||
fun hide(view: View){
|
||||
fun hideWithPadding(view: View){
|
||||
view.visibility = View.INVISIBLE
|
||||
view.setPadding(0, 0, 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
|
||||
}
|
||||
}
|
@ -1,29 +1,69 @@
|
||||
<?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">
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
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"
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switch_hidden_volume"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"/>
|
||||
android:text="@string/hidden_volume"
|
||||
app:switchPadding="10dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:onClick="onClickSwitchHiddenVolume"/>
|
||||
|
||||
<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
|
||||
android:id="@+id/normal_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_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>
|
@ -9,6 +9,7 @@
|
||||
<string name="password">Password:</string>
|
||||
<string name="password_confirm">Password (confirmation):</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="mkdir">Create folder</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="parent_folder">Parent Folder</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="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>
|
||||
@ -76,10 +78,14 @@
|
||||
<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="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_all">Delete all</string>
|
||||
<string name="delete_hash">Delete password hash</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="password_hash_and_path">Password hash and path</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="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_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>
|
||||
@ -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_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_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_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>
|
||||
@ -185,4 +190,6 @@
|
||||
<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="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>
|
||||
|
@ -1,63 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/ui">
|
||||
<PreferenceCategory android:title="@string/ui">
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/icon_screenshot"
|
||||
android:key="usf_screenshot"
|
||||
android:title="@string/usf_screenshot"
|
||||
android:defaultValue="false"/>
|
||||
android:title="@string/usf_screenshot" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/explorer">
|
||||
<PreferenceCategory android:title="@string/explorer">
|
||||
|
||||
<SwitchPreference
|
||||
android:icon="@drawable/icon_open_in_new"
|
||||
android:key="usf_open"
|
||||
android:title="@string/usf_open"
|
||||
android:defaultValue="false"/>
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/icon_decrypt"
|
||||
android:key="usf_decrypt"
|
||||
android:title="@string/usf_decrypt"
|
||||
android:defaultValue="false"/>
|
||||
android:title="@string/usf_decrypt" />
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/icon_open_in_new"
|
||||
android:key="usf_open"
|
||||
android:title="@string/usf_open" />
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/icon_share"
|
||||
android:key="usf_share"
|
||||
android:title="@string/usf_share"
|
||||
android:defaultValue="false"/>
|
||||
android:title="@string/usf_share" />
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/icon_decrypt"
|
||||
android:key="usf_keep_open"
|
||||
android:title="@string/usf_keep_open"
|
||||
android:defaultValue="false"/>
|
||||
android:title="@string/usf_keep_open" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/usf_volume_management">
|
||||
<PreferenceCategory android:title="@string/usf_volume_management">
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/icon_fingerprint"
|
||||
android:key="usf_fingerprint"
|
||||
android:title="@string/usf_fingerprint"
|
||||
android:defaultValue="false"/>
|
||||
android:title="@string/usf_fingerprint" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
android:title="@string/about">
|
||||
<PreferenceCategory android:title="@string/about">
|
||||
|
||||
<Preference
|
||||
android:title="@string/usf_doc"
|
||||
android:icon="@drawable/icon_notes"
|
||||
android:summary="@string/usf_read_doc"
|
||||
android:icon="@drawable/icon_notes">
|
||||
<intent android:action="android.intent.action.VIEW" android:data="https://github.com/hardcore-sushi/DroidFS#unsafe-features"/>
|
||||
android:title="@string/usf_doc">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="https://github.com/hardcore-sushi/DroidFS#unsafe-features" />
|
||||
</Preference>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
Loading…
Reference in New Issue
Block a user