forked from hardcoresushi/DroidFS
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.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"
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
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.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
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
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
@ -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>
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user