forked from hardcoresushi/DroidFS
New home UI
This commit is contained in:
parent
842667cdee
commit
71a314b0a0
@ -68,15 +68,15 @@ dependencies {
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
implementation "com.github.bumptech.glide:glide:4.12.0"
|
||||
implementation "androidx.biometric:biometric:1.1.0"
|
||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha04"
|
||||
|
||||
def exoplayer_version = "2.16.1"
|
||||
def exoplayer_version = "2.17.0"
|
||||
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
|
||||
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
|
||||
|
||||
implementation "androidx.concurrent:concurrent-futures:1.1.0"
|
||||
|
||||
def camerax_version = "1.1.0-beta01"
|
||||
def camerax_version = "1.1.0-beta02"
|
||||
implementation "androidx.camera:camera-camera2:$camerax_version"
|
||||
implementation "androidx.camera:camera-lifecycle:$camerax_version"
|
||||
implementation "androidx.camera:camera-view:$camerax_version"
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 1da2407a614f17a3c64d14ee34fb41e081db9a71
|
||||
Subproject commit 89966b1aaef93ac842f2240451ebfd50dd2bbce9
|
@ -31,61 +31,30 @@
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/BaseTheme">
|
||||
<activity
|
||||
android:name=".CameraActivity"
|
||||
android:screenOrientation="nosensor" />
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:label="@string/title_activity_settings"
|
||||
android:parentActivityName=".MainActivity" />
|
||||
<activity android:name=".explorers.ExplorerActivity" />
|
||||
<activity android:name=".explorers.ExplorerActivityPick" />
|
||||
<activity android:name=".explorers.ExplorerActivityDrop" />
|
||||
<activity
|
||||
android:name=".OpenActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:screenOrientation="nosensor">
|
||||
<activity android:name=".MainActivity" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter android:label="@string/share_menu_label">
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".CreateActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:screenOrientation="nosensor" />
|
||||
<activity
|
||||
android:name=".ChangePasswordActivity"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:screenOrientation="nosensor" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:screenOrientation="nosensor">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".file_viewers.ImageViewer"
|
||||
android:configChanges="screenSize|orientation" /> <!-- don't reload content on configuration change -->
|
||||
<activity
|
||||
android:name=".file_viewers.VideoPlayer"
|
||||
android:configChanges="screenSize|orientation" />
|
||||
<activity
|
||||
android:name=".file_viewers.PdfViewer"
|
||||
android:configChanges="screenSize|orientation" />
|
||||
<activity
|
||||
android:name=".file_viewers.AudioPlayer"
|
||||
android:configChanges="screenSize|orientation" />
|
||||
<activity
|
||||
android:name=".file_viewers.TextEditor"
|
||||
android:configChanges="screenSize|orientation" />
|
||||
<activity android:name=".SettingsActivity" android:label="@string/title_activity_settings"/>
|
||||
<activity android:name=".add_volume.AddVolumeActivity" android:windowSoftInputMode="adjustResize"/>
|
||||
<activity android:name=".ChangePasswordActivity" android:windowSoftInputMode="adjustResize"/>
|
||||
<activity android:name=".explorers.ExplorerActivity"/>
|
||||
<activity android:name=".explorers.ExplorerActivityPick"/>
|
||||
<activity android:name=".explorers.ExplorerActivityDrop"/>
|
||||
<activity android:name=".file_viewers.ImageViewer" android:configChanges="screenSize|orientation" /> <!-- don't reload content on configuration change -->
|
||||
<activity android:name=".file_viewers.VideoPlayer" android:configChanges="screenSize|orientation" />
|
||||
<activity android:name=".file_viewers.PdfViewer" android:configChanges="screenSize|orientation" />
|
||||
<activity android:name=".file_viewers.AudioPlayer" android:configChanges="screenSize|orientation" />
|
||||
<activity android:name=".file_viewers.TextEditor" android:configChanges="screenSize|orientation" />
|
||||
<activity android:name=".CameraActivity" android:screenOrientation="nosensor" />
|
||||
|
||||
<service android:name=".file_operations.FileOperationService" android:exported="false"/>
|
||||
|
||||
|
@ -8,13 +8,13 @@ import androidx.preference.PreferenceManager
|
||||
|
||||
open class BaseActivity: AppCompatActivity() {
|
||||
protected lateinit var sharedPrefs: SharedPreferences
|
||||
protected lateinit var themeValue: String
|
||||
protected var shouldCheckTheme = true
|
||||
lateinit var themeValue: String
|
||||
private var shouldCheckTheme = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
if (shouldCheckTheme) {
|
||||
themeValue = sharedPrefs.getString("theme", "dark_green")!!
|
||||
themeValue = sharedPrefs.getString("theme", ConstValues.DEFAULT_THEME_VALUE)!!
|
||||
when (themeValue) {
|
||||
"black_green" -> setTheme(R.style.BlackGreen)
|
||||
"dark_red" -> setTheme(R.style.DarkRed)
|
||||
|
@ -91,6 +91,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
|
||||
binding = ActivityCameraBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.hide()
|
||||
gocryptfsVolume = GocryptfsVolume(applicationContext, intent.getIntExtra("sessionID", -1))
|
||||
outputDirectory = intent.getStringExtra("path")!!
|
||||
|
||||
|
@ -1,195 +1,161 @@
|
||||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.widget.AdapterView.OnItemClickListener
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import sushi.hardcore.droidfs.adapters.SavedVolumesAdapter
|
||||
import sushi.hardcore.droidfs.databinding.ActivityChangePasswordBinding
|
||||
import sushi.hardcore.droidfs.util.PathUtils
|
||||
import sushi.hardcore.droidfs.util.WidgetUtil
|
||||
import sushi.hardcore.droidfs.util.Wiper
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
class ChangePasswordActivity : VolumeActionActivity() {
|
||||
private lateinit var savedVolumesAdapter: SavedVolumesAdapter
|
||||
class ChangePasswordActivity: BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityChangePasswordBinding
|
||||
private lateinit var volume: Volume
|
||||
private lateinit var volumeDatabase: VolumeDatabase
|
||||
private var fingerprintProtector: FingerprintProtector? = null
|
||||
private var usfFingerprint: Boolean = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
volume = intent.getParcelableExtra("volume")!!
|
||||
binding = ActivityChangePasswordBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setupLayout()
|
||||
setupFingerprintStuff()
|
||||
savedVolumesAdapter = SavedVolumesAdapter(this, themeValue, volumeDatabase)
|
||||
if (savedVolumesAdapter.count > 0){
|
||||
binding.savedPathListview.adapter = savedVolumesAdapter
|
||||
binding.savedPathListview.onItemClickListener = OnItemClickListener { _, _, position, _ ->
|
||||
val volume = savedVolumesAdapter.getItem(position)
|
||||
currentVolumeName = volume.name
|
||||
if (volume.isHidden){
|
||||
switchHiddenVolume.isChecked = true
|
||||
editVolumeName.setText(currentVolumeName)
|
||||
} else {
|
||||
switchHiddenVolume.isChecked = false
|
||||
editVolumePath.setText(currentVolumeName)
|
||||
}
|
||||
onClickSwitchHiddenVolume()
|
||||
}
|
||||
} else {
|
||||
WidgetUtil.hideWithPadding(binding.savedPathListview)
|
||||
}
|
||||
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 (volumeDatabase.isVolumeSaved(s.toString())){
|
||||
checkboxRememberPath.isEnabled = false
|
||||
checkboxRememberPath.isChecked = true
|
||||
binding.editOldPassword.apply {
|
||||
if (volumeDatabase.isHashSaved(s.toString())){
|
||||
text = null
|
||||
hint = getString(R.string.hash_saved_hint)
|
||||
isEnabled = false
|
||||
} else {
|
||||
hint = null
|
||||
isEnabled = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
checkboxRememberPath.isEnabled = true
|
||||
binding.editOldPassword.apply {
|
||||
hint = null
|
||||
isEnabled = true
|
||||
}
|
||||
}
|
||||
title = getString(R.string.change_password)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
binding.textVolumeName.text = volume.name
|
||||
volumeDatabase = VolumeDatabase(this)
|
||||
usfFingerprint = sharedPrefs.getBoolean("usf_fingerprint", false)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
fingerprintProtector = FingerprintProtector.new(this, themeValue, volumeDatabase)
|
||||
if (fingerprintProtector != null && volume.encryptedHash != null) {
|
||||
binding.textCurrentPasswordLabel.visibility = View.GONE
|
||||
binding.editCurrentPassword.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
editVolumePath.addTextChangedListener(textWatcher)
|
||||
editVolumeName.addTextChangedListener(textWatcher)
|
||||
binding.editNewPasswordConfirm.setOnEditorActionListener { _, _, _ ->
|
||||
checkVolumePathThenChangePassword()
|
||||
if (!usfFingerprint || fingerprintProtector == null) {
|
||||
binding.checkboxSavePassword.visibility = View.GONE
|
||||
}
|
||||
binding.editPasswordConfirm.setOnEditorActionListener { _, _, _ ->
|
||||
changeVolumePassword()
|
||||
true
|
||||
}
|
||||
binding.buttonChangePassword.setOnClickListener {
|
||||
checkVolumePathThenChangePassword()
|
||||
}
|
||||
binding.button.setOnClickListener { changeVolumePassword() }
|
||||
}
|
||||
|
||||
fun checkVolumePathThenChangePassword() {
|
||||
loadVolumePath {
|
||||
val volumeFile = File(currentVolumePath)
|
||||
if (!GocryptfsVolume.isGocryptfsVolume(volumeFile)){
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.error_not_a_volume)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
} else if (!volumeFile.canWrite()){
|
||||
errorDirectoryNotWritable(R.string.change_pwd_cant_write_error_msg)
|
||||
} else {
|
||||
changePassword()
|
||||
}
|
||||
}
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return if (item.itemId == android.R.id.home) {
|
||||
finish()
|
||||
true
|
||||
} else super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun changePassword(givenHash: ByteArray? = null){
|
||||
val newPassword = binding.editNewPassword.text.toString().toCharArray()
|
||||
val newPasswordConfirm = binding.editNewPasswordConfirm.text.toString().toCharArray()
|
||||
private fun showCurrentPasswordInput() {
|
||||
binding.textCurrentPasswordLabel.visibility = View.VISIBLE
|
||||
binding.editCurrentPassword.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
private fun changeVolumePassword() {
|
||||
val newPassword = CharArray(binding.editNewPassword.text.length)
|
||||
binding.editNewPassword.text.getChars(0, newPassword.size, newPassword, 0)
|
||||
val newPasswordConfirm = CharArray(binding.editPasswordConfirm.text.length)
|
||||
binding.editPasswordConfirm.text.getChars(0, newPasswordConfirm.size, newPasswordConfirm, 0)
|
||||
@SuppressLint("NewApi")
|
||||
if (!newPassword.contentEquals(newPasswordConfirm)) {
|
||||
Toast.makeText(this, R.string.passwords_mismatch, Toast.LENGTH_SHORT).show()
|
||||
Arrays.fill(newPassword, 0.toChar())
|
||||
} else {
|
||||
object : LoadingTask(this, themeValue, R.string.loading_msg_change_password) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
val oldPassword = binding.editOldPassword.text.toString().toCharArray()
|
||||
var returnedHash: ByteArray? = null
|
||||
if (checkboxSavePassword.isChecked) {
|
||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||
}
|
||||
var changePasswordImmediately = true
|
||||
if (givenHash == null) {
|
||||
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
|
||||
}
|
||||
}
|
||||
var changeWithCurrentPassword = true
|
||||
volume.encryptedHash?.let { encryptedHash ->
|
||||
volume.iv?.let { iv ->
|
||||
fingerprintProtector?.let {
|
||||
changeWithCurrentPassword = false
|
||||
it.listener = object : FingerprintProtector.Listener {
|
||||
override fun onHashStorageReset() {
|
||||
showCurrentPasswordInput()
|
||||
volume.encryptedHash = null
|
||||
volume.iv = null
|
||||
}
|
||||
override fun onPasswordHashDecrypted(hash: ByteArray) {
|
||||
changeVolumePassword(newPassword, hash)
|
||||
}
|
||||
override fun onPasswordHashSaved() {}
|
||||
override fun onFailed(pending: Boolean) {
|
||||
Arrays.fill(newPassword, 0.toChar())
|
||||
}
|
||||
}
|
||||
it.loadPasswordHash(volume.name, encryptedHash, iv)
|
||||
}
|
||||
if (changePasswordImmediately) {
|
||||
if (GocryptfsVolume.changePassword(currentVolumePath, oldPassword, givenHash, newPassword, returnedHash)) {
|
||||
val volume = Volume(currentVolumeName, switchHiddenVolume.isChecked)
|
||||
if (volumeDatabase.isHashSaved(currentVolumeName)) {
|
||||
volumeDatabase.removeHash(volume)
|
||||
}
|
||||
if (checkboxRememberPath.isChecked) {
|
||||
volumeDatabase.saveVolume(volume)
|
||||
}
|
||||
if (checkboxSavePassword.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
|
||||
stopTask {
|
||||
savePasswordHash(returnedHash) {
|
||||
onPasswordChanged()
|
||||
}
|
||||
}
|
||||
if (changeWithCurrentPassword) {
|
||||
changeVolumePassword(newPassword)
|
||||
}
|
||||
}
|
||||
Arrays.fill(newPasswordConfirm, 0.toChar())
|
||||
}
|
||||
|
||||
private fun changeVolumePassword(newPassword: CharArray, givenHash: ByteArray? = null) {
|
||||
object : LoadingTask(this, themeValue, R.string.loading_msg_change_password) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
var returnedHash: ByteArray? = null
|
||||
if (binding.checkboxSavePassword.isChecked) {
|
||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||
}
|
||||
var currentPassword: CharArray? = null
|
||||
if (givenHash == null) {
|
||||
currentPassword = CharArray(binding.editCurrentPassword.text.length)
|
||||
binding.editCurrentPassword.text.getChars(0, currentPassword.size, currentPassword, 0)
|
||||
}
|
||||
if (GocryptfsVolume.changePassword(volume.getFullPath(filesDir.path), currentPassword, givenHash, newPassword, returnedHash)) {
|
||||
if (volumeDatabase.isHashSaved(volume.name)) {
|
||||
volumeDatabase.removeHash(volume)
|
||||
}
|
||||
stopTask {
|
||||
@SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
|
||||
if (binding.checkboxSavePassword.isChecked && returnedHash != null) {
|
||||
fingerprintProtector!!.let {
|
||||
it.listener = object : FingerprintProtector.Listener {
|
||||
override fun onHashStorageReset() {
|
||||
// retry
|
||||
it.savePasswordHash(volume, returnedHash)
|
||||
}
|
||||
override fun onPasswordHashDecrypted(hash: ByteArray) {}
|
||||
override fun onPasswordHashSaved() {
|
||||
Arrays.fill(returnedHash, 0)
|
||||
finish()
|
||||
}
|
||||
override fun onFailed(pending: Boolean) {
|
||||
if (!pending) {
|
||||
Arrays.fill(returnedHash, 0)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stopTask { onPasswordChanged() }
|
||||
it.savePasswordHash(volume, returnedHash)
|
||||
}
|
||||
} else {
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.change_password_failed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
finish()
|
||||
}
|
||||
}
|
||||
Arrays.fill(oldPassword, 0.toChar())
|
||||
}
|
||||
override fun doFinally(activity: AppCompatActivity) {
|
||||
Arrays.fill(newPassword, 0.toChar())
|
||||
Arrays.fill(newPasswordConfirm, 0.toChar())
|
||||
} else {
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.change_password_failed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
if (currentPassword != null)
|
||||
Arrays.fill(currentPassword, 0.toChar())
|
||||
Arrays.fill(newPassword, 0.toChar())
|
||||
if (givenHash != null)
|
||||
Arrays.fill(givenHash, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPasswordChanged(){
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.success_change_password)
|
||||
.setMessage(R.string.success_change_password_msg)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { _, _ -> finish() }
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Wiper.wipeEditText(binding.editOldPassword)
|
||||
Wiper.wipeEditText(binding.editNewPassword)
|
||||
Wiper.wipeEditText(binding.editNewPasswordConfirm)
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ class ConstValues {
|
||||
const val MAX_KERNEL_WRITE = 128*1024
|
||||
const val wipe_passes = 2
|
||||
const val slideshow_delay: Long = 4000
|
||||
const val DEFAULT_THEME_VALUE = "dark_green"
|
||||
private val fileExtensions = mapOf(
|
||||
Pair("image", listOf("png", "jpg", "jpeg", "gif", "webp", "bmp")),
|
||||
Pair("video", listOf("mp4", "webm", "mkv", "mov")),
|
||||
|
@ -1,180 +0,0 @@
|
||||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import sushi.hardcore.droidfs.databinding.ActivityCreateBinding
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerActivity
|
||||
import sushi.hardcore.droidfs.util.Wiper
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
class CreateActivity : VolumeActionActivity() {
|
||||
private var sessionID = -1
|
||||
private var isStartingExplorer = false
|
||||
private lateinit var binding: ActivityCreateBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityCreateBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setupLayout()
|
||||
setupFingerprintStuff(mayDecrypt = false)
|
||||
binding.editPasswordConfirm.setOnEditorActionListener { _, _, _ ->
|
||||
createVolume()
|
||||
true
|
||||
}
|
||||
binding.spinnerXchacha.adapter = ArrayAdapter(
|
||||
this@CreateActivity,
|
||||
android.R.layout.simple_spinner_item,
|
||||
resources.getStringArray(R.array.encryption_cipher)
|
||||
).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
binding.spinnerXchacha.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
if (position == 1) {
|
||||
CustomAlertDialogBuilder(this@CreateActivity, themeValue)
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(R.string.xchacha_warning)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
binding.buttonCreate.setOnClickListener {
|
||||
createVolume()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClickSwitchHiddenVolume() {
|
||||
super.onClickSwitchHiddenVolume()
|
||||
if (switchHiddenVolume.isChecked){
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(R.string.hidden_volume_warning)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
fun createVolume() {
|
||||
loadVolumePath {
|
||||
val password = binding.editPassword.text.toString().toCharArray()
|
||||
val passwordConfirm = binding.editPasswordConfirm.text.toString().toCharArray()
|
||||
if (!password.contentEquals(passwordConfirm)) {
|
||||
Toast.makeText(this, R.string.passwords_mismatch, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
val volumeFile = File(currentVolumePath)
|
||||
var goodDirectory = false
|
||||
if (!volumeFile.isDirectory) {
|
||||
if (volumeFile.mkdirs()) {
|
||||
goodDirectory = true
|
||||
} else {
|
||||
errorDirectoryNotWritable(R.string.create_cant_write_error_msg)
|
||||
}
|
||||
} else {
|
||||
val dirContent = volumeFile.list()
|
||||
if (dirContent != null) {
|
||||
if (dirContent.isEmpty()) {
|
||||
if (volumeFile.canWrite()) {
|
||||
goodDirectory = true
|
||||
} else {
|
||||
errorDirectoryNotWritable(R.string.create_cant_write_error_msg)
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, R.string.dir_not_empty, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, R.string.listdir_null_error_msg, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
if (goodDirectory) {
|
||||
object: LoadingTask(this, themeValue, R.string.loading_msg_create) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
val xchacha = when (binding.spinnerXchacha.selectedItemPosition) {
|
||||
0 -> 0
|
||||
1 -> 1
|
||||
else -> -1
|
||||
}
|
||||
if (GocryptfsVolume.createVolume(currentVolumePath, password, false, xchacha, GocryptfsVolume.ScryptDefaultLogN, ConstValues.creator)) {
|
||||
var returnedHash: ByteArray? = null
|
||||
if (checkboxSavePassword.isChecked){
|
||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||
}
|
||||
sessionID = GocryptfsVolume.init(currentVolumePath, password, null, returnedHash)
|
||||
if (sessionID != -1) {
|
||||
if (checkboxRememberPath.isChecked) {
|
||||
if (volumeDatabase.isVolumeSaved(currentVolumeName)) { //cleaning old saved path
|
||||
volumeDatabase.removeVolume(Volume(currentVolumeName))
|
||||
}
|
||||
volumeDatabase.saveVolume(Volume(currentVolumeName, switchHiddenVolume.isChecked))
|
||||
}
|
||||
if (checkboxSavePassword.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
|
||||
stopTask {
|
||||
savePasswordHash(returnedHash) {
|
||||
startExplorer()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stopTask { startExplorer() }
|
||||
}
|
||||
} else {
|
||||
stopTaskWithToast(R.string.open_volume_failed)
|
||||
}
|
||||
} else {
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.create_volume_failed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun doFinally(activity: AppCompatActivity) {
|
||||
Arrays.fill(password, 0.toChar())
|
||||
Arrays.fill(passwordConfirm, 0.toChar())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startExplorer(){
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.success_volume_create)
|
||||
.setMessage(R.string.success_volume_create_msg)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
val intent = Intent(this, ExplorerActivity::class.java)
|
||||
intent.putExtra("sessionID", sessionID)
|
||||
intent.putExtra("volume_name", File(currentVolumeName).name)
|
||||
startActivity(intent)
|
||||
isStartingExplorer = true
|
||||
finish()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
//Closing volume if leaving activity while showing dialog
|
||||
if (sessionID != -1 && !isStartingExplorer) {
|
||||
GocryptfsVolume(applicationContext, sessionID).close()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Wiper.wipeEditText(binding.editPassword)
|
||||
Wiper.wipeEditText(binding.editPasswordConfirm)
|
||||
}
|
||||
}
|
259
app/src/main/java/sushi/hardcore/droidfs/FingerprintProtector.kt
Normal file
259
app/src/main/java/sushi/hardcore/droidfs/FingerprintProtector.kt
Normal file
@ -0,0 +1,259 @@
|
||||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import java.security.KeyStore
|
||||
import java.security.KeyStoreException
|
||||
import java.security.UnrecoverableKeyException
|
||||
import javax.crypto.*
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
class FingerprintProtector private constructor(
|
||||
private val activity: FragmentActivity,
|
||||
private val themeValue: String,
|
||||
private val volumeDatabase: VolumeDatabase,
|
||||
) {
|
||||
|
||||
interface Listener {
|
||||
fun onHashStorageReset()
|
||||
fun onPasswordHashDecrypted(hash: ByteArray)
|
||||
fun onPasswordHashSaved()
|
||||
fun onFailed(pending: Boolean)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
|
||||
private const val KEY_ALIAS = "Hash Key"
|
||||
private const val KEY_SIZE = 256
|
||||
private const val GCM_TAG_LEN = 128
|
||||
|
||||
fun canAuthenticate(context: Context): Int {
|
||||
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
return if (!keyguardManager.isKeyguardSecure)
|
||||
1
|
||||
else when (BiometricManager.from(context).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
|
||||
BiometricManager.BIOMETRIC_SUCCESS -> 0
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> 2
|
||||
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> 3
|
||||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> 4
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
|
||||
fun new(
|
||||
activity: FragmentActivity,
|
||||
themeValue: String,
|
||||
volumeDatabase: VolumeDatabase,
|
||||
): FingerprintProtector? {
|
||||
return if (canAuthenticate(activity) == 0)
|
||||
FingerprintProtector(activity, themeValue, volumeDatabase)
|
||||
else
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
lateinit var listener: Listener
|
||||
private val biometricPrompt = BiometricPrompt(activity, ContextCompat.getMainExecutor(activity), object: BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
if (
|
||||
errorCode != BiometricPrompt.ERROR_USER_CANCELED &&
|
||||
errorCode != BiometricPrompt.ERROR_NEGATIVE_BUTTON &&
|
||||
errorCode != BiometricPrompt.ERROR_TIMEOUT
|
||||
) {
|
||||
Toast.makeText(activity, activity.getString(R.string.biometric_error, errString), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
listener.onFailed(false)
|
||||
}
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
val cipherObject = result.cryptoObject?.cipher
|
||||
if (cipherObject != null) {
|
||||
try {
|
||||
when (cipherActionMode) {
|
||||
Cipher.ENCRYPT_MODE -> {
|
||||
val cipherText = cipherObject.doFinal(dataToProcess)
|
||||
volume.encryptedHash = cipherText
|
||||
volume.iv = cipherObject.iv
|
||||
if (volumeDatabase.addHash(volume))
|
||||
listener.onPasswordHashSaved()
|
||||
else
|
||||
listener.onFailed(false)
|
||||
}
|
||||
Cipher.DECRYPT_MODE -> {
|
||||
try {
|
||||
val plainText = cipherObject.doFinal(dataToProcess)
|
||||
listener.onPasswordHashDecrypted(plainText)
|
||||
} catch (e: AEADBadTagException) {
|
||||
listener.onFailed(true)
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.MAC_verification_failed)
|
||||
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
|
||||
resetHashStorage()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> listener.onFailed(false) }
|
||||
.setOnCancelListener { listener.onFailed(false) }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IllegalBlockSizeException) {
|
||||
listener.onFailed(true)
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.illegal_block_size_exception)
|
||||
.setMessage(R.string.illegal_block_size_exception_msg)
|
||||
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
|
||||
resetHashStorage()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> listener.onFailed(false) }
|
||||
.setOnCancelListener { listener.onFailed(false) }
|
||||
.show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(activity, R.string.error_cipher_null, Toast.LENGTH_SHORT).show()
|
||||
listener.onFailed(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
private lateinit var keyStore: KeyStore
|
||||
private lateinit var key: SecretKey
|
||||
private lateinit var cipher: Cipher
|
||||
private var isCipherReady = false
|
||||
private var cipherActionMode: Int? = null
|
||||
private lateinit var volume: Volume
|
||||
private lateinit var dataToProcess: ByteArray
|
||||
|
||||
private fun resetHashStorage() {
|
||||
try {
|
||||
keyStore.deleteEntry(KEY_ALIAS)
|
||||
} catch (e: KeyStoreException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
volumeDatabase.getVolumes().forEach { volume ->
|
||||
volumeDatabase.removeHash(volume)
|
||||
}
|
||||
isCipherReady = false
|
||||
Toast.makeText(activity, R.string.hash_storage_reset, Toast.LENGTH_SHORT).show()
|
||||
listener.onHashStorageReset()
|
||||
}
|
||||
|
||||
private fun prepareCipher(): Boolean {
|
||||
if (!isCipherReady) {
|
||||
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
|
||||
keyStore.load(null)
|
||||
key = if (keyStore.containsAlias(KEY_ALIAS)) {
|
||||
try {
|
||||
keyStore.getKey(KEY_ALIAS, null) as SecretKey
|
||||
} catch (e: UnrecoverableKeyException) {
|
||||
listener.onFailed(true)
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(activity.getString(R.string.unrecoverable_key_exception))
|
||||
.setMessage(activity.getString(R.string.unrecoverable_key_exception_msg, e.localizedMessage))
|
||||
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
|
||||
resetHashStorage()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> listener.onFailed(false) }
|
||||
.setOnCancelListener { listener.onFailed(false) }
|
||||
.show()
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
val builder = KeyGenParameterSpec.Builder(
|
||||
KEY_ALIAS,
|
||||
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
|
||||
)
|
||||
builder.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
builder.setKeySize(KEY_SIZE)
|
||||
builder.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
builder.setUserAuthenticationRequired(true)
|
||||
val keyGenerator = KeyGenerator.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_AES,
|
||||
ANDROID_KEY_STORE
|
||||
)
|
||||
keyGenerator.init(builder.build())
|
||||
keyGenerator.generateKey()
|
||||
}
|
||||
cipher = Cipher.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_GCM + "/" + KeyProperties.ENCRYPTION_PADDING_NONE
|
||||
)
|
||||
isCipherReady = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun alertKeyPermanentlyInvalidatedException() {
|
||||
listener.onFailed(true)
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.key_permanently_invalidated_exception)
|
||||
.setMessage(R.string.key_permanently_invalidated_exception_msg)
|
||||
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
|
||||
resetHashStorage()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { _, _ -> listener.onFailed(false) }
|
||||
.setOnCancelListener { listener.onFailed(false) }
|
||||
.show()
|
||||
}
|
||||
|
||||
fun savePasswordHash(volume: Volume, plainText: ByteArray) {
|
||||
this.volume = volume
|
||||
val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(activity.getString(R.string.encrypt_action_description))
|
||||
.setSubtitle(volume.shortName)
|
||||
.setDescription(activity.getString(R.string.fingerprint_instruction))
|
||||
.setNegativeButtonText(activity.getString(R.string.cancel))
|
||||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
||||
.setConfirmationRequired(false)
|
||||
.build()
|
||||
cipherActionMode = Cipher.ENCRYPT_MODE
|
||||
if (prepareCipher()) {
|
||||
try {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key)
|
||||
dataToProcess = plainText
|
||||
biometricPrompt.authenticate(
|
||||
biometricPromptInfo,
|
||||
BiometricPrompt.CryptoObject(cipher)
|
||||
)
|
||||
} catch (e: KeyPermanentlyInvalidatedException) {
|
||||
alertKeyPermanentlyInvalidatedException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadPasswordHash(volumeName: String, cipherText: ByteArray, iv: ByteArray) {
|
||||
val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(activity.getString(R.string.decrypt_action_description))
|
||||
.setSubtitle(volumeName)
|
||||
.setDescription(activity.getString(R.string.fingerprint_instruction))
|
||||
.setNegativeButtonText(activity.getString(R.string.cancel))
|
||||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
||||
.setConfirmationRequired(false)
|
||||
.build()
|
||||
cipherActionMode = Cipher.DECRYPT_MODE
|
||||
if (prepareCipher()) {
|
||||
dataToProcess = cipherText
|
||||
val gcmSpec = GCMParameterSpec(GCM_TAG_LEN, iv)
|
||||
try {
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec)
|
||||
biometricPrompt.authenticate(
|
||||
biometricPromptInfo,
|
||||
BiometricPrompt.CryptoObject(cipher)
|
||||
)
|
||||
} catch (e: KeyPermanentlyInvalidatedException) {
|
||||
alertKeyPermanentlyInvalidatedException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -30,7 +30,7 @@ class GocryptfsVolume(val applicationContext: Context, var sessionID: Int) {
|
||||
const val KeyLen = 32
|
||||
const val ScryptDefaultLogN = 16
|
||||
const val DefaultBS = 4096
|
||||
external fun createVolume(root_cipher_dir: String, password: CharArray, plainTextNames: Boolean, xchacha: Int, logN: Int, creator: String): Boolean
|
||||
external fun createVolume(root_cipher_dir: String, password: CharArray, plainTextNames: Boolean, xchacha: Int, logN: Int, creator: String, returnedHash: ByteArray?): Boolean
|
||||
external fun init(root_cipher_dir: String, password: CharArray?, givenHash: ByteArray?, returnedHash: ByteArray?): Int
|
||||
external fun changePassword(root_cipher_dir: String, old_password: CharArray?, givenHash: ByteArray?, new_password: CharArray, returnedHash: ByteArray?): Boolean
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
@ -19,7 +18,6 @@ abstract class LoadingTask(val activity: AppCompatActivity, themeValue: String,
|
||||
startTask()
|
||||
}
|
||||
abstract fun doTask(activity: AppCompatActivity)
|
||||
open fun doFinally(activity: AppCompatActivity){}
|
||||
private fun startTask() {
|
||||
dialogLoading.show()
|
||||
Thread {
|
||||
@ -27,7 +25,6 @@ abstract class LoadingTask(val activity: AppCompatActivity, themeValue: String,
|
||||
if (!isStopped){
|
||||
dialogLoading.dismiss()
|
||||
}
|
||||
activity.runOnUiThread { doFinally(activity) }
|
||||
}.start()
|
||||
}
|
||||
fun stopTask(onUiThread: (() -> Unit)?){
|
||||
@ -39,7 +36,4 @@ abstract class LoadingTask(val activity: AppCompatActivity, themeValue: String,
|
||||
}
|
||||
}
|
||||
}
|
||||
protected fun stopTaskWithToast(stringId: Int){
|
||||
stopTask { Toast.makeText(activity, stringId, Toast.LENGTH_SHORT).show() }
|
||||
}
|
||||
}
|
@ -1,19 +1,69 @@
|
||||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import sushi.hardcore.droidfs.adapters.VolumeAdapter
|
||||
import sushi.hardcore.droidfs.add_volume.AddVolumeActivity
|
||||
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
|
||||
import sushi.hardcore.droidfs.databinding.ActivityMainBinding
|
||||
import sushi.hardcore.droidfs.databinding.DialogDeleteVolumeBinding
|
||||
import sushi.hardcore.droidfs.databinding.DialogOpenVolumeBinding
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerActivity
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerActivityDrop
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerActivityPick
|
||||
import sushi.hardcore.droidfs.util.PathUtils
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
class MainActivity : BaseActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var volumeDatabase: VolumeDatabase
|
||||
private lateinit var volumeAdapter: VolumeAdapter
|
||||
private var fingerprintProtector: FingerprintProtector? = null
|
||||
private var usfFingerprint: Boolean = false
|
||||
private var usfKeepOpen: Boolean = false
|
||||
private var addVolume = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
when (result.resultCode) {
|
||||
AddVolumeActivity.RESULT_VOLUME_ADDED -> {
|
||||
volumeAdapter.apply {
|
||||
volumes = volumeDatabase.getVolumes()
|
||||
notifyItemInserted(volumes.size)
|
||||
}
|
||||
binding.textNoVolumes.visibility = View.GONE
|
||||
}
|
||||
AddVolumeActivity.RESULT_HASH_STORAGE_RESET -> {
|
||||
volumeAdapter.refresh()
|
||||
binding.textNoVolumes.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
private var changePasswordPosition: Int? = null
|
||||
private var changePassword = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
changePasswordPosition?.let {
|
||||
volumeAdapter.selectedItems.remove(it)
|
||||
volumeAdapter.onVolumeChanged(it)
|
||||
}
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
private var pickMode = false
|
||||
private var dropMode = false
|
||||
private var shouldCloseVolume = true // used when launched to pick file from another volume
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.toolbar.toolbar)
|
||||
if (sharedPrefs.getBoolean("applicationFirstOpening", true)) {
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.warning)
|
||||
@ -28,20 +78,141 @@ class MainActivity : BaseActivity() {
|
||||
.setOnDismissListener { sharedPrefs.edit().putBoolean("applicationFirstOpening", false).apply() }
|
||||
.show()
|
||||
}
|
||||
binding.buttonOpen.setOnClickListener {
|
||||
startActivity(OpenActivity::class.java)
|
||||
pickMode = intent.action == "pick"
|
||||
dropMode = (intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE) && intent.extras != null
|
||||
volumeDatabase = VolumeDatabase(this)
|
||||
volumeAdapter = VolumeAdapter(
|
||||
this,
|
||||
volumeDatabase,
|
||||
!pickMode && !dropMode,
|
||||
!dropMode,
|
||||
::onVolumeItemClick,
|
||||
::onVolumeItemLongClick,
|
||||
)
|
||||
binding.recyclerViewVolumes.adapter = volumeAdapter
|
||||
binding.recyclerViewVolumes.layoutManager = LinearLayoutManager(this)
|
||||
if (volumeAdapter.volumes.isEmpty()) {
|
||||
binding.textNoVolumes.visibility = View.VISIBLE
|
||||
}
|
||||
binding.buttonCreate.setOnClickListener {
|
||||
startActivity(CreateActivity::class.java)
|
||||
if (pickMode) {
|
||||
title = getString(R.string.select_volume)
|
||||
binding.fab.visibility = View.GONE
|
||||
} else {
|
||||
binding.fab.setOnClickListener {
|
||||
addVolume.launch(Intent(this, AddVolumeActivity::class.java))
|
||||
}
|
||||
}
|
||||
binding.buttonChangePassword.setOnClickListener {
|
||||
startActivity(ChangePasswordActivity::class.java)
|
||||
usfKeepOpen = sharedPrefs.getBoolean("usf_keep_open", false)
|
||||
usfFingerprint = sharedPrefs.getBoolean("usf_fingerprint", false)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
fingerprintProtector = FingerprintProtector.new(this, themeValue, volumeDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onVolumeItemClick(volume: Volume, position: Int) {
|
||||
if (volumeAdapter.selectedItems.isEmpty())
|
||||
openVolume(volume, position)
|
||||
else
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
private fun onVolumeItemLongClick() {
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
private fun unselectAll() {
|
||||
volumeAdapter.unSelectAll()
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
private fun removeVolumes(volumes: List<Volume>, i: Int = 0, doDeleteVolumeContent: Boolean? = null) {
|
||||
if (i < volumes.size) {
|
||||
if (volumes[i].isHidden) {
|
||||
if (doDeleteVolumeContent == null) {
|
||||
val dialogBinding = DialogDeleteVolumeBinding.inflate(layoutInflater)
|
||||
dialogBinding.textContent.text = getString(R.string.delete_hidden_volume_question, volumes[i].name)
|
||||
// show checkbox only if there is at least one other hidden volume
|
||||
for (j in (i+1 until volumes.size)) {
|
||||
if (volumes[j].isHidden) {
|
||||
dialogBinding.checkboxApplyToAll.visibility = View.VISIBLE
|
||||
break
|
||||
}
|
||||
}
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.warning)
|
||||
.setView(dialogBinding.root)
|
||||
.setPositiveButton(R.string.forget_only) { _, _ ->
|
||||
volumeDatabase.removeVolume(volumes[i].name)
|
||||
removeVolumes(volumes, i + 1, if (dialogBinding.checkboxApplyToAll.isChecked) false else null)
|
||||
}
|
||||
.setNegativeButton(R.string.delete_volume) { _, _ ->
|
||||
PathUtils.recursiveRemoveDirectory(File(volumes[i].getFullPath(filesDir.path)))
|
||||
volumeDatabase.removeVolume(volumes[i].name)
|
||||
removeVolumes(volumes, i + 1, if (dialogBinding.checkboxApplyToAll.isChecked) true else null)
|
||||
}
|
||||
.setOnCancelListener {
|
||||
volumeAdapter.refresh()
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
if (doDeleteVolumeContent) {
|
||||
PathUtils.recursiveRemoveDirectory(File(volumes[i].getFullPath(filesDir.path)))
|
||||
}
|
||||
volumeDatabase.removeVolume(volumes[i].name)
|
||||
removeVolumes(volumes, i + 1, doDeleteVolumeContent)
|
||||
}
|
||||
} else {
|
||||
volumeDatabase.removeVolume(volumes[i].name)
|
||||
removeVolumes(volumes, i + 1, doDeleteVolumeContent)
|
||||
}
|
||||
} else {
|
||||
volumeAdapter.refresh()
|
||||
invalidateOptionsMenu()
|
||||
if (volumeAdapter.volumes.isEmpty()) {
|
||||
binding.textNoVolumes.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.menu_settings -> {
|
||||
android.R.id.home -> {
|
||||
if (pickMode || dropMode) {
|
||||
if (pickMode)
|
||||
shouldCloseVolume = false
|
||||
finish()
|
||||
} else {
|
||||
unselectAll()
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.select_all -> {
|
||||
volumeAdapter.selectAll()
|
||||
invalidateOptionsMenu()
|
||||
true
|
||||
}
|
||||
R.id.remove -> {
|
||||
val selectedVolumes = volumeAdapter.selectedItems.map { i -> volumeAdapter.volumes[i] }
|
||||
removeVolumes(selectedVolumes)
|
||||
true
|
||||
}
|
||||
R.id.forget_password -> {
|
||||
for (i in volumeAdapter.selectedItems) {
|
||||
if (volumeDatabase.removeHash(volumeAdapter.volumes[i]))
|
||||
volumeAdapter.onVolumeChanged(i)
|
||||
}
|
||||
unselectAll()
|
||||
true
|
||||
}
|
||||
R.id.change_password -> {
|
||||
changePasswordPosition = volumeAdapter.selectedItems.elementAt(0)
|
||||
changePassword.launch(Intent(this, ChangePasswordActivity::class.java).apply {
|
||||
putExtra("volume", volumeAdapter.volumes[changePasswordPosition!!])
|
||||
})
|
||||
true
|
||||
}
|
||||
R.id.settings -> {
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
startActivity(intent)
|
||||
true
|
||||
@ -52,11 +223,199 @@ class MainActivity : BaseActivity() {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.main_activity, menu)
|
||||
menu.findItem(R.id.settings).isVisible = !pickMode && !dropMode
|
||||
val isSelecting = volumeAdapter.selectedItems.isNotEmpty()
|
||||
menu.findItem(R.id.select_all).isVisible = isSelecting && !pickMode && !dropMode
|
||||
menu.findItem(R.id.remove).isVisible = isSelecting && !pickMode && !dropMode
|
||||
var showForgetPassword = isSelecting
|
||||
if (isSelecting) {
|
||||
for (volume in volumeAdapter.selectedItems.map { i -> volumeAdapter.volumes[i] }) {
|
||||
if (volume.encryptedHash == null) {
|
||||
showForgetPassword = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
menu.findItem(R.id.forget_password).isVisible = showForgetPassword && !pickMode
|
||||
menu.findItem(R.id.change_password).isVisible =
|
||||
!pickMode && !dropMode &&
|
||||
volumeAdapter.selectedItems.size == 1 &&
|
||||
volumeAdapter.volumes[volumeAdapter.selectedItems.elementAt(0)].canWrite(filesDir.path)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(isSelecting || pickMode || dropMode)
|
||||
return true
|
||||
}
|
||||
|
||||
fun <T> startActivity(clazz: Class<T>) {
|
||||
val intent = Intent(this, clazz)
|
||||
startActivity(intent)
|
||||
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
|
||||
private fun openVolume(volume: Volume, position: Int) {
|
||||
var askForPassword = true
|
||||
fingerprintProtector?.let { fingerprintProtector ->
|
||||
volume.encryptedHash?.let { encryptedHash ->
|
||||
volume.iv?.let { iv ->
|
||||
askForPassword = false
|
||||
fingerprintProtector.listener = object : FingerprintProtector.Listener {
|
||||
override fun onHashStorageReset() {
|
||||
volumeAdapter.refresh()
|
||||
}
|
||||
override fun onPasswordHashDecrypted(hash: ByteArray) {
|
||||
object : LoadingTask(this@MainActivity, themeValue, R.string.loading_msg_open) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), null, hash, null)
|
||||
Arrays.fill(hash, 0)
|
||||
if (sessionId != -1)
|
||||
stopTask { startExplorer(sessionId, volume.shortName) }
|
||||
else
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.open_volume_failed)
|
||||
.setMessage(R.string.open_failed_hash_msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onPasswordHashSaved() {}
|
||||
override fun onFailed(pending: Boolean) {}
|
||||
}
|
||||
fingerprintProtector.loadPasswordHash(volume.shortName, encryptedHash, iv)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (askForPassword)
|
||||
askForPassword(volume, position)
|
||||
}
|
||||
|
||||
private fun onPasswordSubmitted(volume: Volume, position: Int, dialogBinding: DialogOpenVolumeBinding) {
|
||||
val password = CharArray(dialogBinding.editPassword.text.length)
|
||||
dialogBinding.editPassword.text.getChars(0, password.size, password, 0)
|
||||
// openVolumeWithPassword is responsible for wiping the password
|
||||
openVolumeWithPassword(
|
||||
volume,
|
||||
position,
|
||||
password,
|
||||
dialogBinding.checkboxSavePassword.isChecked,
|
||||
)
|
||||
}
|
||||
|
||||
private fun askForPassword(volume: Volume, position: Int) {
|
||||
val dialogBinding = DialogOpenVolumeBinding.inflate(layoutInflater)
|
||||
if (!usfFingerprint || fingerprintProtector == null) {
|
||||
dialogBinding.checkboxSavePassword.visibility = View.GONE
|
||||
}
|
||||
val dialog = CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.open_dialog_title)
|
||||
.setView(dialogBinding.root)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.open) { _, _ ->
|
||||
onPasswordSubmitted(volume, position, dialogBinding)
|
||||
}
|
||||
.create()
|
||||
dialogBinding.editPassword.setOnEditorActionListener { _, _, _ ->
|
||||
dialog.dismiss()
|
||||
onPasswordSubmitted(volume, position, dialogBinding)
|
||||
true
|
||||
}
|
||||
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun openVolumeWithPassword(volume: Volume, position: Int, password: CharArray, savePasswordHash: Boolean) {
|
||||
val usfFingerprint = sharedPrefs.getBoolean("usf_fingerprint", false)
|
||||
object : LoadingTask(this, themeValue, R.string.loading_msg_open) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
var returnedHash: ByteArray? = null
|
||||
if (savePasswordHash && usfFingerprint) {
|
||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||
}
|
||||
val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), password, null, returnedHash)
|
||||
Arrays.fill(password, 0.toChar())
|
||||
if (sessionId != -1) {
|
||||
val fingerprintProtector = fingerprintProtector
|
||||
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
|
||||
if (savePasswordHash && returnedHash != null && fingerprintProtector != null)
|
||||
stopTask {
|
||||
fingerprintProtector.listener = object : FingerprintProtector.Listener {
|
||||
override fun onHashStorageReset() {
|
||||
volumeAdapter.refresh()
|
||||
}
|
||||
override fun onPasswordHashDecrypted(hash: ByteArray) {}
|
||||
override fun onPasswordHashSaved() {
|
||||
Arrays.fill(returnedHash, 0)
|
||||
volumeAdapter.onVolumeChanged(position)
|
||||
startExplorer(sessionId, volume.shortName)
|
||||
}
|
||||
private var isClosed = false
|
||||
override fun onFailed(pending: Boolean) {
|
||||
if (!isClosed) {
|
||||
GocryptfsVolume(this@MainActivity, sessionId).close()
|
||||
isClosed = true
|
||||
}
|
||||
Arrays.fill(returnedHash, 0)
|
||||
}
|
||||
}
|
||||
fingerprintProtector.savePasswordHash(volume, returnedHash)
|
||||
}
|
||||
else
|
||||
stopTask { startExplorer(sessionId, volume.shortName) }
|
||||
} else
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.open_volume_failed)
|
||||
.setMessage(R.string.open_volume_failed_msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setOnDismissListener {
|
||||
askForPassword(volume, position)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startExplorer(sessionId: Int, volumeShortName: String) {
|
||||
var explorerIntent: Intent? = null
|
||||
if (dropMode) { //import via android share menu
|
||||
explorerIntent = Intent(this, ExplorerActivityDrop::class.java)
|
||||
explorerIntent.action = intent.action //forward action
|
||||
explorerIntent.putExtras(intent.extras!!) //forward extras
|
||||
} else if (pickMode) {
|
||||
explorerIntent = Intent(this, ExplorerActivityPick::class.java)
|
||||
explorerIntent.putExtra("originalSessionID", intent.getIntExtra("sessionID", -1))
|
||||
explorerIntent.flags = Intent.FLAG_ACTIVITY_FORWARD_RESULT
|
||||
}
|
||||
if (explorerIntent == null) {
|
||||
explorerIntent = Intent(this, ExplorerActivity::class.java) //default opening
|
||||
}
|
||||
explorerIntent.putExtra("sessionID", sessionId)
|
||||
explorerIntent.putExtra("volume_name", volumeShortName)
|
||||
startActivity(explorerIntent)
|
||||
if (pickMode)
|
||||
shouldCloseVolume = false
|
||||
if (dropMode || pickMode)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (volumeAdapter.selectedItems.isNotEmpty()) {
|
||||
unselectAll()
|
||||
} else {
|
||||
if (pickMode)
|
||||
shouldCloseVolume = false
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if (pickMode && !usfKeepOpen) {
|
||||
finish()
|
||||
if (shouldCloseVolume) {
|
||||
val sessionID = intent.getIntExtra("sessionID", -1)
|
||||
if (sessionID != -1) {
|
||||
GocryptfsVolume(this, sessionID).close()
|
||||
RestrictedFileProvider.wipeAll(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,265 +0,0 @@
|
||||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.MenuItem
|
||||
import android.widget.AdapterView.OnItemClickListener
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import sushi.hardcore.droidfs.adapters.SavedVolumesAdapter
|
||||
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
|
||||
import sushi.hardcore.droidfs.databinding.ActivityOpenBinding
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerActivity
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerActivityDrop
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerActivityPick
|
||||
import sushi.hardcore.droidfs.util.PathUtils
|
||||
import sushi.hardcore.droidfs.util.WidgetUtil
|
||||
import sushi.hardcore.droidfs.util.Wiper
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
class OpenActivity : VolumeActionActivity() {
|
||||
private lateinit var savedVolumesAdapter: SavedVolumesAdapter
|
||||
private var sessionID = -1
|
||||
private var isStartingActivity = false
|
||||
private var isFinishingIntentionally = false
|
||||
private lateinit var binding: ActivityOpenBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityOpenBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setupLayout()
|
||||
setupFingerprintStuff()
|
||||
savedVolumesAdapter = SavedVolumesAdapter(this, themeValue, volumeDatabase)
|
||||
if (savedVolumesAdapter.count > 0){
|
||||
binding.savedPathListview.adapter = savedVolumesAdapter
|
||||
binding.savedPathListview.onItemClickListener = OnItemClickListener { _, _, position, _ ->
|
||||
val volume = savedVolumesAdapter.getItem(position)
|
||||
currentVolumeName = volume.name
|
||||
if (volume.isHidden){
|
||||
switchHiddenVolume.isChecked = true
|
||||
editVolumeName.setText(currentVolumeName)
|
||||
} else {
|
||||
switchHiddenVolume.isChecked = false
|
||||
editVolumePath.setText(currentVolumeName)
|
||||
}
|
||||
onClickSwitchHiddenVolume()
|
||||
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.hideWithPadding(binding.savedPathListview)
|
||||
}
|
||||
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 (volumeDatabase.isVolumeSaved(s.toString())){
|
||||
checkboxRememberPath.isEnabled = false
|
||||
checkboxRememberPath.isChecked = true
|
||||
if (volumeDatabase.isHashSaved(s.toString())){
|
||||
checkboxSavePassword.isEnabled = false
|
||||
checkboxSavePassword.isChecked = true
|
||||
} else {
|
||||
checkboxSavePassword.isEnabled = true
|
||||
}
|
||||
} else {
|
||||
checkboxRememberPath.isEnabled = true
|
||||
checkboxSavePassword.isEnabled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
editVolumePath.addTextChangedListener(textWatcher)
|
||||
editVolumeName.addTextChangedListener(textWatcher)
|
||||
binding.editPassword.setOnEditorActionListener { _, _, _ ->
|
||||
checkVolumePathThenOpen()
|
||||
true
|
||||
}
|
||||
binding.buttonOpen.setOnClickListener {
|
||||
checkVolumePathThenOpen()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when(item.itemId){
|
||||
android.R.id.home -> {
|
||||
isFinishingIntentionally = true
|
||||
finish()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPickingDirectory() {
|
||||
isStartingActivity = true
|
||||
}
|
||||
|
||||
fun checkVolumePathThenOpen() {
|
||||
loadVolumePath {
|
||||
val volumeFile = File(currentVolumePath)
|
||||
if (!GocryptfsVolume.isGocryptfsVolume(volumeFile)){
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.error_not_a_volume)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
} else if (!volumeFile.canWrite()) {
|
||||
if ((intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE) && intent.extras != null) { //import via android share menu
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.open_cant_write_error_msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
} else {
|
||||
val dialog = CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.warning)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { _, _ -> openVolume() }
|
||||
if (PathUtils.isPathOnExternalStorage(currentVolumeName, this)){
|
||||
dialog.setView(
|
||||
layoutInflater.inflate(R.layout.dialog_sdcard_error, null).apply {
|
||||
findViewById<TextView>(R.id.path).text = PathUtils.getPackageDataFolder(this@OpenActivity)
|
||||
findViewById<TextView>(R.id.footer).text = getString(R.string.open_read_only)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
dialog.setMessage(R.string.open_cant_write_warning)
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
} else {
|
||||
openVolume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openVolume(){
|
||||
object : LoadingTask(this, themeValue, R.string.loading_msg_open) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
val password = binding.editPassword.text.toString().toCharArray()
|
||||
var returnedHash: ByteArray? = null
|
||||
if (checkboxSavePassword.isChecked && usf_fingerprint) {
|
||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||
}
|
||||
sessionID = GocryptfsVolume.init(currentVolumePath, password, null, returnedHash)
|
||||
if (sessionID != -1) {
|
||||
if (checkboxRememberPath.isChecked) {
|
||||
volumeDatabase.saveVolume(Volume(currentVolumeName, switchHiddenVolume.isChecked))
|
||||
}
|
||||
if (checkboxSavePassword.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
|
||||
stopTask {
|
||||
savePasswordHash(returnedHash) { success ->
|
||||
if (success){
|
||||
startExplorer()
|
||||
} else {
|
||||
GocryptfsVolume(applicationContext, sessionID).close()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stopTask { startExplorer() }
|
||||
}
|
||||
} else {
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.open_volume_failed)
|
||||
.setMessage(R.string.open_volume_failed_msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
Arrays.fill(password, 0.toChar())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openUsingPasswordHash(passwordHash: ByteArray){
|
||||
object : LoadingTask(this, themeValue, R.string.loading_msg_open) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
sessionID = GocryptfsVolume.init(currentVolumePath, null, passwordHash, null)
|
||||
if (sessionID != -1){
|
||||
stopTask { startExplorer() }
|
||||
} else {
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.open_volume_failed)
|
||||
.setMessage(R.string.open_failed_hash_msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
Arrays.fill(passwordHash, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startExplorer() {
|
||||
var explorerIntent: Intent? = null
|
||||
val currentIntentAction = intent.action
|
||||
if (currentIntentAction != null) {
|
||||
if ((currentIntentAction == Intent.ACTION_SEND || currentIntentAction == Intent.ACTION_SEND_MULTIPLE) && intent.extras != null) { //import via android share menu
|
||||
explorerIntent = Intent(this, ExplorerActivityDrop::class.java)
|
||||
explorerIntent.action = currentIntentAction //forward action
|
||||
explorerIntent.putExtras(intent.extras!!) //forward extras
|
||||
} else if (currentIntentAction == "pick") { //pick items to import
|
||||
explorerIntent = Intent(this, ExplorerActivityPick::class.java)
|
||||
explorerIntent.putExtra("originalSessionID", intent.getIntExtra("sessionID", -1))
|
||||
explorerIntent.flags = Intent.FLAG_ACTIVITY_FORWARD_RESULT
|
||||
}
|
||||
}
|
||||
if (explorerIntent == null) {
|
||||
explorerIntent = Intent(this, ExplorerActivity::class.java) //default opening
|
||||
}
|
||||
explorerIntent.putExtra("sessionID", sessionID)
|
||||
explorerIntent.putExtra("volume_name", File(currentVolumeName).name)
|
||||
startActivity(explorerIntent)
|
||||
isFinishingIntentionally = true
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
super.onBackPressed()
|
||||
isFinishingIntentionally = true
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if (intent.action == "pick"){
|
||||
if (isStartingActivity) {
|
||||
isStartingActivity = false
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Wiper.wipeEditText(binding.editPassword)
|
||||
if (intent.action == "pick" && !isFinishingIntentionally){
|
||||
val sessionID = intent.getIntExtra("sessionID", -1)
|
||||
if (sessionID != -1){
|
||||
GocryptfsVolume(applicationContext, sessionID).close()
|
||||
RestrictedFileProvider.wipeAll(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SwitchPreference
|
||||
import sushi.hardcore.droidfs.databinding.ActivitySettingsBinding
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
|
||||
class SettingsActivity : BaseActivity() {
|
||||
|
||||
@ -12,7 +15,6 @@ class SettingsActivity : BaseActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.toolbar.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
val screen = intent.extras?.getString("screen") ?: "main"
|
||||
val fragment = if (screen == "UnsafeFeaturesSettingsFragment") {
|
||||
@ -49,6 +51,38 @@ class SettingsActivity : BaseActivity() {
|
||||
class UnsafeFeaturesSettingsFragment : PreferenceFragmentCompat() {
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.unsafe_features_preferences, rootKey)
|
||||
findPreference<SwitchPreference>("usf_fingerprint")?.setOnPreferenceChangeListener { _, checked ->
|
||||
if (checked as Boolean) {
|
||||
var errorMsg: String? = null
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val reason = when (FingerprintProtector.canAuthenticate(requireContext())) {
|
||||
0 -> null
|
||||
1 -> R.string.keyguard_not_secure
|
||||
2 -> R.string.no_hardware
|
||||
3 -> R.string.hardware_unavailable
|
||||
4 -> R.string.no_fingerprint
|
||||
else -> R.string.unknown_error
|
||||
}
|
||||
reason?.let {
|
||||
errorMsg = getString(R.string.fingerprint_error_msg, getString(it))
|
||||
}
|
||||
} else {
|
||||
errorMsg = getString(R.string.error_marshmallow_required)
|
||||
}
|
||||
if (errorMsg == null) {
|
||||
true
|
||||
} else {
|
||||
CustomAlertDialogBuilder(requireContext(), (requireActivity() as BaseActivity).themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(errorMsg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,54 @@
|
||||
package sushi.hardcore.droidfs
|
||||
|
||||
class Volume(val name: String, val isHidden: Boolean = false, var hash: ByteArray? = null, var iv: ByteArray? = null)
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import sushi.hardcore.droidfs.util.PathUtils
|
||||
import java.io.File
|
||||
|
||||
class Volume(val name: String, val isHidden: Boolean = false, var encryptedHash: ByteArray? = null, var iv: ByteArray? = null): Parcelable {
|
||||
|
||||
constructor(parcel: Parcel) : this(
|
||||
parcel.readString()!!,
|
||||
parcel.readByte() != 0.toByte(),
|
||||
parcel.createByteArray(),
|
||||
parcel.createByteArray()
|
||||
)
|
||||
|
||||
val shortName: String by lazy {
|
||||
File(name).name
|
||||
}
|
||||
|
||||
fun getFullPath(filesDir: String): String {
|
||||
return if (isHidden)
|
||||
PathUtils.pathJoin(filesDir, name)
|
||||
else
|
||||
name
|
||||
}
|
||||
|
||||
fun canWrite(filesDir: String): Boolean {
|
||||
return File(getFullPath(filesDir)).canWrite()
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
with (dest) {
|
||||
writeString(name)
|
||||
writeByte(if (isHidden) 1 else 0)
|
||||
writeByteArray(encryptedHash)
|
||||
writeByteArray(iv)
|
||||
}
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<Volume> {
|
||||
override fun createFromParcel(parcel: Parcel): Volume {
|
||||
return Volume(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<Volume?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,397 +0,0 @@
|
||||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.Manifest
|
||||
import android.app.KeyguardManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyPermanentlyInvalidatedException
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.widget.*
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.SwitchCompat
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import sushi.hardcore.droidfs.util.PathUtils
|
||||
import sushi.hardcore.droidfs.util.WidgetUtil
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import java.security.KeyStore
|
||||
import javax.crypto.*
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
|
||||
abstract class VolumeActionActivity : BaseActivity() {
|
||||
|
||||
companion object {
|
||||
private const val STORAGE_PERMISSIONS_REQUEST = 0
|
||||
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
|
||||
private const val KEY_ALIAS = "Hash Key"
|
||||
private const val KEY_SIZE = 256
|
||||
private const val GCM_TAG_LEN = 128
|
||||
}
|
||||
|
||||
protected lateinit var currentVolumeName: String
|
||||
protected lateinit var currentVolumePath: String
|
||||
protected lateinit var volumeDatabase: VolumeDatabase
|
||||
protected val pickDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
|
||||
if (uri != null) {
|
||||
onDirectoryPicked(uri)
|
||||
}
|
||||
}
|
||||
protected var usf_fingerprint = false
|
||||
private var biometricCanAuthenticateCode: Int = -1
|
||||
private lateinit var biometricManager: BiometricManager
|
||||
private lateinit var biometricPrompt: BiometricPrompt
|
||||
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
|
||||
protected lateinit var switchHiddenVolume: SwitchCompat
|
||||
protected lateinit var checkboxRememberPath: CheckBox
|
||||
protected lateinit var checkboxSavePassword: CheckBox
|
||||
protected lateinit var editVolumeName: EditText
|
||||
protected lateinit var editVolumePath: EditText
|
||||
private lateinit var hiddenVolumeSection: LinearLayout
|
||||
private lateinit var normalVolumeSection: LinearLayout
|
||||
|
||||
protected fun setupLayout() {
|
||||
setSupportActionBar(findViewById(R.id.toolbar))
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
findViewById<ImageButton>(R.id.button_pick_directory).setOnClickListener {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) +
|
||||
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE), STORAGE_PERMISSIONS_REQUEST)
|
||||
} else {
|
||||
safePickDirectory()
|
||||
}
|
||||
} else {
|
||||
safePickDirectory()
|
||||
}
|
||||
}
|
||||
switchHiddenVolume = findViewById(R.id.switch_hidden_volume)
|
||||
checkboxRememberPath = findViewById(R.id.checkbox_remember_path)
|
||||
checkboxSavePassword = findViewById(R.id.checkbox_save_password)
|
||||
editVolumeName = findViewById(R.id.edit_volume_name)
|
||||
editVolumePath = findViewById(R.id.edit_volume_path)
|
||||
hiddenVolumeSection = findViewById(R.id.hidden_volume_section)
|
||||
normalVolumeSection = findViewById(R.id.normal_volume_section)
|
||||
switchHiddenVolume.setOnClickListener {
|
||||
onClickSwitchHiddenVolume()
|
||||
}
|
||||
checkboxRememberPath.setOnClickListener {
|
||||
if (!checkboxRememberPath.isChecked) {
|
||||
checkboxSavePassword.isChecked = false
|
||||
}
|
||||
}
|
||||
checkboxSavePassword.setOnClickListener {
|
||||
if (checkboxSavePassword.isChecked) {
|
||||
if (biometricCanAuthenticateCode == 0) {
|
||||
checkboxRememberPath.isChecked = true
|
||||
} else {
|
||||
checkboxSavePassword.isChecked = false
|
||||
printAuthenticateImpossibleError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onClickSwitchHiddenVolume() {
|
||||
if (switchHiddenVolume.isChecked){
|
||||
WidgetUtil.show(hiddenVolumeSection, originalHiddenVolumeSectionLayoutParams)
|
||||
WidgetUtil.hide(normalVolumeSection)
|
||||
} else {
|
||||
WidgetUtil.show(normalVolumeSection, originalNormalVolumeSectionLayoutParams)
|
||||
WidgetUtil.hide(hiddenVolumeSection)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onPickingDirectory() {}
|
||||
protected fun onDirectoryPicked(uri: Uri) {
|
||||
val path = PathUtils.getFullPathFromTreeUri(uri, this)
|
||||
if (path != null) {
|
||||
editVolumePath.setText(path)
|
||||
} else {
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.path_from_uri_null_error_msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun safePickDirectory() {
|
||||
try {
|
||||
onPickingDirectory()
|
||||
pickDirectory.launch(null)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.open_tree_failed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
when (requestCode) {
|
||||
STORAGE_PERMISSIONS_REQUEST -> if (grantResults.size == 2) {
|
||||
if (grantResults[0] != PackageManager.PERMISSION_GRANTED || grantResults[1] != PackageManager.PERMISSION_GRANTED) {
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.storage_perm_denied)
|
||||
.setMessage(R.string.storage_perm_denied_msg)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
} else {
|
||||
safePickDirectory()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun setupFingerprintStuff(mayDecrypt: Boolean = true) {
|
||||
originalHiddenVolumeSectionLayoutParams = hiddenVolumeSection.layoutParams as LinearLayout.LayoutParams
|
||||
originalNormalVolumeSectionLayoutParams = normalVolumeSection.layoutParams as LinearLayout.LayoutParams
|
||||
WidgetUtil.hide(hiddenVolumeSection)
|
||||
volumeDatabase = VolumeDatabase(this)
|
||||
usf_fingerprint = sharedPrefs.getBoolean("usf_fingerprint", false)
|
||||
val marshmallow = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
||||
if (!marshmallow || !usf_fingerprint) {
|
||||
WidgetUtil.hideWithPadding(checkboxSavePassword)
|
||||
}
|
||||
if (marshmallow && (mayDecrypt || usf_fingerprint)) {
|
||||
biometricManager = BiometricManager.from(this)
|
||||
biometricCanAuthenticateCode = canAuthenticate()
|
||||
if (biometricCanAuthenticateCode == 0){
|
||||
val executor = ContextCompat.getMainExecutor(this)
|
||||
val activityContext = this
|
||||
val callback = object: BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
super.onAuthenticationError(errorCode, errString)
|
||||
Toast.makeText(applicationContext, errString, Toast.LENGTH_SHORT).show()
|
||||
if (actionMode == Cipher.ENCRYPT_MODE){
|
||||
onAuthenticationResult(false)
|
||||
}
|
||||
}
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
Toast.makeText(applicationContext, R.string.authentication_failed, Toast.LENGTH_SHORT).show()
|
||||
if (actionMode == Cipher.ENCRYPT_MODE){
|
||||
onAuthenticationResult(false)
|
||||
}
|
||||
}
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
var success = false
|
||||
val cipherObject = result.cryptoObject?.cipher
|
||||
if (cipherObject != null){
|
||||
try {
|
||||
when (actionMode) {
|
||||
Cipher.ENCRYPT_MODE -> {
|
||||
val cipherText = cipherObject.doFinal(dataToProcess)
|
||||
success = volumeDatabase.addHash(Volume(currentVolumeName, switchHiddenVolume.isChecked, cipherText, cipherObject.iv))
|
||||
}
|
||||
Cipher.DECRYPT_MODE -> {
|
||||
try {
|
||||
val plainText = cipherObject.doFinal(dataToProcess)
|
||||
onPasswordDecrypted(plainText)
|
||||
} catch (e: AEADBadTagException){
|
||||
CustomAlertDialogBuilder(activityContext, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.MAC_verification_failed)
|
||||
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
|
||||
resetHashStorage()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IllegalBlockSizeException){
|
||||
CustomAlertDialogBuilder(activityContext, themeValue)
|
||||
.setTitle(R.string.illegal_block_size_exception)
|
||||
.setMessage(R.string.illegal_block_size_exception_msg)
|
||||
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
|
||||
resetHashStorage()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(applicationContext, R.string.error_cipher_null, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
if (actionMode == Cipher.ENCRYPT_MODE){
|
||||
onAuthenticationResult(success)
|
||||
}
|
||||
}
|
||||
}
|
||||
biometricPrompt = BiometricPrompt(this, executor, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun canAuthenticate(): Int {
|
||||
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
return if (!keyguardManager.isKeyguardSecure) {
|
||||
1
|
||||
} else {
|
||||
when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)){
|
||||
BiometricManager.BIOMETRIC_SUCCESS -> 0
|
||||
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> 2
|
||||
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> 3
|
||||
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> 4
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun printAuthenticateImpossibleError() {
|
||||
Toast.makeText(this, when (biometricCanAuthenticateCode){
|
||||
1 -> R.string.fingerprint_error_no_fingerprints
|
||||
2 -> R.string.fingerprint_error_hw_not_present
|
||||
3 -> R.string.fingerprint_error_hw_not_available
|
||||
4 -> R.string.fingerprint_error_no_fingerprints
|
||||
else -> R.string.error
|
||||
}, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private fun prepareCipher() {
|
||||
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
|
||||
keyStore.load(null)
|
||||
key = if (keyStore.containsAlias(KEY_ALIAS)){
|
||||
keyStore.getKey(KEY_ALIAS, null) as SecretKey
|
||||
} else {
|
||||
val builder = KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
||||
builder.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
builder.setKeySize(KEY_SIZE)
|
||||
builder.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
builder.setUserAuthenticationRequired(true)
|
||||
val keyGenerator = KeyGenerator.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_AES,
|
||||
ANDROID_KEY_STORE
|
||||
)
|
||||
keyGenerator.init(builder.build())
|
||||
keyGenerator.generateKey()
|
||||
}
|
||||
cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES+"/"+KeyProperties.BLOCK_MODE_GCM+"/"+KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
isCipherReady = true
|
||||
}
|
||||
|
||||
private fun alertKeyPermanentlyInvalidatedException(){
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.key_permanently_invalidated_exception)
|
||||
.setMessage(R.string.key_permanently_invalidated_exception_msg)
|
||||
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
|
||||
resetHashStorage()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
protected fun savePasswordHash(plainText: ByteArray, onAuthenticationResult: (success: Boolean) -> Unit){
|
||||
val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(currentVolumeName)
|
||||
.setSubtitle(getString(R.string.encrypt_action_description))
|
||||
.setDescription(getString(R.string.fingerprint_instruction))
|
||||
.setNegativeButtonText(getString(R.string.cancel))
|
||||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
||||
.setConfirmationRequired(false)
|
||||
.build()
|
||||
if (!isCipherReady){
|
||||
prepareCipher()
|
||||
}
|
||||
actionMode = Cipher.ENCRYPT_MODE
|
||||
try {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, key)
|
||||
this.onAuthenticationResult = onAuthenticationResult
|
||||
dataToProcess = plainText
|
||||
biometricPrompt.authenticate(biometricPromptInfo, BiometricPrompt.CryptoObject(cipher))
|
||||
} catch (e: KeyPermanentlyInvalidatedException){
|
||||
alertKeyPermanentlyInvalidatedException()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
protected fun loadPasswordHash(cipherText: ByteArray, iv: ByteArray, onPasswordDecrypted: (password: ByteArray) -> Unit){
|
||||
val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(currentVolumeName)
|
||||
.setSubtitle(getString(R.string.decrypt_action_description))
|
||||
.setDescription(getString(R.string.fingerprint_instruction))
|
||||
.setNegativeButtonText(getString(R.string.cancel))
|
||||
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
||||
.setConfirmationRequired(false)
|
||||
.build()
|
||||
this.onPasswordDecrypted = onPasswordDecrypted
|
||||
actionMode = Cipher.DECRYPT_MODE
|
||||
if (!isCipherReady){
|
||||
prepareCipher()
|
||||
}
|
||||
dataToProcess = cipherText
|
||||
val gcmSpec = GCMParameterSpec(GCM_TAG_LEN, iv)
|
||||
try {
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec)
|
||||
biometricPrompt.authenticate(biometricPromptInfo, BiometricPrompt.CryptoObject(cipher))
|
||||
} catch (e: KeyPermanentlyInvalidatedException){
|
||||
alertKeyPermanentlyInvalidatedException()
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetHashStorage() {
|
||||
keyStore.deleteEntry(KEY_ALIAS)
|
||||
volumeDatabase.getVolumes().forEach { volume ->
|
||||
volumeDatabase.removeHash(volume)
|
||||
}
|
||||
isCipherReady = false
|
||||
Toast.makeText(this, R.string.hash_storage_reset, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
protected fun loadVolumePath(callback: () -> Unit){
|
||||
currentVolumeName = if (switchHiddenVolume.isChecked){
|
||||
editVolumeName.text.toString()
|
||||
} else {
|
||||
editVolumePath.text.toString()
|
||||
}
|
||||
if (currentVolumeName.isEmpty()) {
|
||||
Toast.makeText(this, if (switchHiddenVolume.isChecked) {R.string.enter_volume_name} else {R.string.enter_volume_path}, Toast.LENGTH_SHORT).show()
|
||||
} else if (switchHiddenVolume.isChecked && currentVolumeName.contains("/")){
|
||||
Toast.makeText(this, R.string.error_slash_in_name, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
currentVolumePath = if (switchHiddenVolume.isChecked) {
|
||||
PathUtils.pathJoin(filesDir.path, currentVolumeName)
|
||||
} else {
|
||||
currentVolumeName
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
fun errorDirectoryNotWritable(errorMsg: Int) {
|
||||
val dialog = CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
if (PathUtils.isPathOnExternalStorage(currentVolumePath, this)) {
|
||||
dialog.setView(
|
||||
layoutInflater.inflate(R.layout.dialog_sdcard_error, null).apply {
|
||||
findViewById<TextView>(R.id.path).text = PathUtils.getPackageDataFolder(this@VolumeActionActivity)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
dialog.setMessage(errorMsg)
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ class VolumeDatabase(context: Context): SQLiteOpenHelper(context,
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(COLUMN_NAME, volume.name)
|
||||
contentValues.put(COLUMN_HIDDEN, volume.isHidden)
|
||||
contentValues.put(COLUMN_HASH, volume.hash)
|
||||
contentValues.put(COLUMN_HASH, volume.encryptedHash)
|
||||
contentValues.put(COLUMN_IV, volume.iv)
|
||||
return contentValues
|
||||
}
|
||||
@ -31,15 +31,19 @@ class VolumeDatabase(context: Context): SQLiteOpenHelper(context,
|
||||
|
||||
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)
|
||||
fun isVolumeSaved(volumeName: String, isHidden: Boolean): Boolean {
|
||||
val cursor = readableDatabase.query(TABLE_NAME,
|
||||
arrayOf(COLUMN_NAME), "$COLUMN_NAME=? AND $COLUMN_HIDDEN=?",
|
||||
arrayOf(volumeName, (if (isHidden) 1 else 0).toString()),
|
||||
null, null, null
|
||||
)
|
||||
val result = cursor.count > 0
|
||||
cursor.close()
|
||||
return result
|
||||
}
|
||||
|
||||
fun saveVolume(volume: Volume): Boolean {
|
||||
if (!isVolumeSaved(volume.name)){
|
||||
if (!isVolumeSaved(volume.name, volume.isHidden)) {
|
||||
return (writableDatabase.insert(TABLE_NAME, null, contentValuesFromVolume(volume)) == 0.toLong())
|
||||
}
|
||||
return false
|
||||
@ -65,8 +69,8 @@ class VolumeDatabase(context: Context): SQLiteOpenHelper(context,
|
||||
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){
|
||||
if (cursor.moveToNext()) {
|
||||
if (cursor.getBlob(cursor.getColumnIndex(COLUMN_HASH)) != null) {
|
||||
isHashSaved = true
|
||||
}
|
||||
}
|
||||
@ -90,7 +94,7 @@ class VolumeDatabase(context: Context): SQLiteOpenHelper(context,
|
||||
), "$COLUMN_NAME=?", arrayOf(volume.name)) > 0
|
||||
}
|
||||
|
||||
fun removeVolume(volume: Volume): Boolean {
|
||||
return writableDatabase.delete(TABLE_NAME, "$COLUMN_NAME=?", arrayOf(volume.name)) > 0
|
||||
fun removeVolume(volumeName: String): Boolean {
|
||||
return writableDatabase.delete(TABLE_NAME, "$COLUMN_NAME=?", arrayOf(volumeName)) > 0
|
||||
}
|
||||
}
|
@ -1,113 +0,0 @@
|
||||
package sushi.hardcore.droidfs.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
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.R
|
||||
import sushi.hardcore.droidfs.Volume
|
||||
import sushi.hardcore.droidfs.VolumeDatabase
|
||||
import sushi.hardcore.droidfs.util.PathUtils
|
||||
import sushi.hardcore.droidfs.util.WidgetUtil
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import sushi.hardcore.droidfs.widgets.NonScrollableColoredBorderListView
|
||||
import java.io.File
|
||||
|
||||
class SavedVolumesAdapter(private val context: Context, private val themeValue: String, private val volumeDatabase: VolumeDatabase) : BaseAdapter() {
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
private lateinit var nonScrollableColoredBorderListView: NonScrollableColoredBorderListView
|
||||
|
||||
override fun getCount(): Int {
|
||||
return volumeDatabase.getVolumes().size
|
||||
}
|
||||
|
||||
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 currentVolume = getItem(position)
|
||||
volumeNameTextView.text = currentVolume.name
|
||||
val deleteImageView = view.findViewById<ImageView>(R.id.delete_imageview)
|
||||
deleteImageView.imageTintList = ColorStateList.valueOf(nonScrollableColoredBorderListView.colorAccent) //fix a strange bug that sometimes displays the icon in white
|
||||
deleteImageView.setOnClickListener {
|
||||
val dialog = CustomAlertDialogBuilder(context, themeValue)
|
||||
dialog.setTitle(R.string.warning)
|
||||
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 {
|
||||
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.show()
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
private fun refresh(parent: ViewGroup) {
|
||||
notifyDataSetChanged()
|
||||
if (count == 0){
|
||||
WidgetUtil.hideWithPadding(parent)
|
||||
} else {
|
||||
nonScrollableColoredBorderListView.layoutParams.height = nonScrollableColoredBorderListView.computeHeight()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
package sushi.hardcore.droidfs.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sushi.hardcore.droidfs.R
|
||||
import sushi.hardcore.droidfs.Volume
|
||||
import sushi.hardcore.droidfs.VolumeDatabase
|
||||
import java.io.File
|
||||
|
||||
class VolumeAdapter(
|
||||
private val context: Context,
|
||||
private val volumeDatabase: VolumeDatabase,
|
||||
private val allowSelection: Boolean,
|
||||
private val showReadOnly: Boolean,
|
||||
private val onVolumeItemClick: (Volume, Int) -> Unit,
|
||||
private val onVolumeItemLongClick: () -> Unit,
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
private val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
lateinit var volumes: List<Volume>
|
||||
val selectedItems: MutableSet<Int> = HashSet()
|
||||
|
||||
init {
|
||||
reloadVolumes()
|
||||
}
|
||||
|
||||
private fun reloadVolumes() {
|
||||
volumes = if (showReadOnly) {
|
||||
volumeDatabase.getVolumes()
|
||||
} else {
|
||||
volumeDatabase.getVolumes().filter { v -> v.canWrite(context.filesDir.path) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSelection(position: Int): Boolean {
|
||||
return if (selectedItems.contains(position)) {
|
||||
selectedItems.remove(position)
|
||||
false
|
||||
} else {
|
||||
selectedItems.add(position)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun onItemClick(position: Int): Boolean {
|
||||
onVolumeItemClick(volumes[position], position)
|
||||
if (allowSelection && selectedItems.isNotEmpty()) {
|
||||
return toggleSelection(position)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun onItemLongClick(position: Int): Boolean {
|
||||
onVolumeItemLongClick()
|
||||
return if (allowSelection)
|
||||
toggleSelection(position)
|
||||
else
|
||||
false
|
||||
}
|
||||
|
||||
fun onVolumeChanged(position: Int) {
|
||||
reloadVolumes()
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun selectAll() {
|
||||
for (i in volumes.indices) {
|
||||
if (!selectedItems.contains(i))
|
||||
selectedItems.add(i)
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun unSelectAll() {
|
||||
selectedItems.clear()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
reloadVolumes()
|
||||
unSelectAll()
|
||||
}
|
||||
|
||||
inner class VolumeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
private fun setBackground(isSelected: Boolean) {
|
||||
itemView.setBackgroundResource(if (isSelected) R.color.itemSelected else 0)
|
||||
}
|
||||
|
||||
fun bind(position: Int) {
|
||||
val volume = volumes[position]
|
||||
itemView.findViewById<TextView>(R.id.text_volume_name).text = volume.shortName
|
||||
itemView.findViewById<ImageView>(R.id.image_icon).setImageResource(R.drawable.icon_volume)
|
||||
itemView.findViewById<TextView>(R.id.text_path).text = if (volume.isHidden)
|
||||
context.getString(R.string.hidden_volume)
|
||||
else
|
||||
volume.name
|
||||
val canWrite = volume.canWrite(context.filesDir.path)
|
||||
val infoString: String? = if (volume.encryptedHash == null)
|
||||
if (canWrite) null else '(' + context.getString(R.string.read_only) + ')'
|
||||
else
|
||||
'(' +
|
||||
(if (canWrite) "" else context.getString(R.string.read_only) + ", ") +
|
||||
context.getString(R.string.password_hash_saved) +
|
||||
')'
|
||||
itemView.findViewById<TextView>(R.id.text_info).apply {
|
||||
if (infoString == null)
|
||||
visibility = View.GONE
|
||||
else {
|
||||
text = infoString
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
(bindingAdapter as VolumeAdapter?)?.let { adapter ->
|
||||
itemView.findViewById<LinearLayout>(R.id.selectable_container).apply {
|
||||
setOnClickListener {
|
||||
setBackground(adapter.onItemClick(layoutPosition))
|
||||
}
|
||||
setOnLongClickListener {
|
||||
setBackground(adapter.onItemLongClick(layoutPosition))
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
setBackground(selectedItems.contains(position))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val view: View = inflater.inflate(R.layout.adapter_volume, parent, false)
|
||||
return VolumeViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
(holder as VolumeViewHolder).bind(position)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return volumes.size
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
package sushi.hardcore.droidfs.add_volume
|
||||
|
||||
enum class Action {
|
||||
ADD,
|
||||
CREATE,
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
package sushi.hardcore.droidfs.add_volume
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import sushi.hardcore.droidfs.BaseActivity
|
||||
import sushi.hardcore.droidfs.R
|
||||
import sushi.hardcore.droidfs.databinding.ActivityAddVolumeBinding
|
||||
|
||||
class AddVolumeActivity: BaseActivity() {
|
||||
|
||||
companion object {
|
||||
const val RESULT_VOLUME_ADDED = 1
|
||||
const val RESULT_HASH_STORAGE_RESET = 2
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityAddVolumeBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityAddVolumeBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.add(
|
||||
R.id.fragment_container,
|
||||
SelectPathFragment.newInstance(themeValue),
|
||||
)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
if (supportFragmentManager.backStackEntryCount > 0)
|
||||
supportFragmentManager.popBackStack()
|
||||
else
|
||||
finish()
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
fun onFragmentLoaded(selectPathFragment: Boolean) {
|
||||
title = getString(
|
||||
if (selectPathFragment) {
|
||||
R.string.add_volume
|
||||
} else {
|
||||
R.string.create_volume
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun onSelectedAlreadySavedVolume() {
|
||||
finish()
|
||||
}
|
||||
|
||||
fun onVolumeAdded(hashStorageReset: Boolean) {
|
||||
setResult(if (hashStorageReset) RESULT_HASH_STORAGE_RESET else RESULT_VOLUME_ADDED)
|
||||
finish()
|
||||
}
|
||||
|
||||
fun createVolume(volumePath: String, isHidden: Boolean) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(
|
||||
R.id.fragment_container, CreateVolumeFragment.newInstance(
|
||||
themeValue,
|
||||
volumePath,
|
||||
isHidden,
|
||||
sharedPrefs.getBoolean("usf_fingerprint", false),
|
||||
)
|
||||
)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
}
|
@ -0,0 +1,183 @@
|
||||
package sushi.hardcore.droidfs.add_volume
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import sushi.hardcore.droidfs.*
|
||||
import sushi.hardcore.droidfs.databinding.FragmentCreateVolumeBinding
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
class CreateVolumeFragment: Fragment() {
|
||||
companion object {
|
||||
private const val KEY_THEME_VALUE = "theme"
|
||||
private const val KEY_VOLUME_PATH = "path"
|
||||
private const val KEY_IS_HIDDEN = "hidden"
|
||||
private const val KEY_USF_FINGERPRINT = "fingerprint"
|
||||
|
||||
fun newInstance(
|
||||
themeValue: String,
|
||||
volumePath: String,
|
||||
isHidden: Boolean,
|
||||
usfFingerprint: Boolean,
|
||||
): CreateVolumeFragment {
|
||||
return CreateVolumeFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(KEY_THEME_VALUE, themeValue)
|
||||
putString(KEY_VOLUME_PATH, volumePath)
|
||||
putBoolean(KEY_IS_HIDDEN, isHidden)
|
||||
putBoolean(KEY_USF_FINGERPRINT, usfFingerprint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var binding: FragmentCreateVolumeBinding
|
||||
private var themeValue = ConstValues.DEFAULT_THEME_VALUE
|
||||
private lateinit var volumePath: String
|
||||
private var isHiddenVolume: Boolean = false
|
||||
private var usfFingerprint: Boolean = false
|
||||
private lateinit var volumeDatabase: VolumeDatabase
|
||||
private var fingerprintProtector: FingerprintProtector? = null
|
||||
private var hashStorageReset = false
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentCreateVolumeBinding.inflate(layoutInflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
requireArguments().let { arguments ->
|
||||
arguments.getString(KEY_THEME_VALUE)?.let { themeValue = it }
|
||||
volumePath = arguments.getString(KEY_VOLUME_PATH)!!
|
||||
isHiddenVolume = arguments.getBoolean(KEY_IS_HIDDEN)
|
||||
usfFingerprint = arguments.getBoolean(KEY_USF_FINGERPRINT)
|
||||
}
|
||||
volumeDatabase = VolumeDatabase(requireContext())
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
fingerprintProtector = FingerprintProtector.new(requireActivity(), themeValue, volumeDatabase)
|
||||
}
|
||||
if (!usfFingerprint || fingerprintProtector == null) {
|
||||
binding.checkboxSavePassword.visibility = View.GONE
|
||||
}
|
||||
binding.editPasswordConfirm.setOnEditorActionListener { _, _, _ ->
|
||||
createVolume()
|
||||
true
|
||||
}
|
||||
binding.spinnerXchacha.adapter = ArrayAdapter(
|
||||
requireContext(),
|
||||
android.R.layout.simple_spinner_item,
|
||||
resources.getStringArray(R.array.encryption_cipher)
|
||||
).apply {
|
||||
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
}
|
||||
binding.spinnerXchacha.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
if (position == 1)
|
||||
CustomAlertDialogBuilder(requireContext(), themeValue)
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(R.string.xchacha_warning)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
binding.buttonCreate.setOnClickListener {
|
||||
createVolume()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||
super.onViewStateRestored(savedInstanceState)
|
||||
(activity as AddVolumeActivity).onFragmentLoaded(false)
|
||||
}
|
||||
|
||||
private fun createVolume() {
|
||||
val password = CharArray(binding.editPassword.text.length)
|
||||
binding.editPassword.text.getChars(0, password.size, password, 0)
|
||||
val passwordConfirm = CharArray(binding.editPasswordConfirm.text.length)
|
||||
binding.editPasswordConfirm.text.getChars(0, passwordConfirm.size, passwordConfirm, 0)
|
||||
if (!password.contentEquals(passwordConfirm)) {
|
||||
Toast.makeText(requireContext(), R.string.passwords_mismatch, Toast.LENGTH_SHORT).show()
|
||||
Arrays.fill(password, 0.toChar())
|
||||
} else {
|
||||
object: LoadingTask(requireActivity() as AppCompatActivity, themeValue, R.string.loading_msg_create) {
|
||||
override fun doTask(activity: AppCompatActivity) {
|
||||
val xchacha = when (binding.spinnerXchacha.selectedItemPosition) {
|
||||
0 -> 0
|
||||
1 -> 1
|
||||
else -> -1
|
||||
}
|
||||
val volumeFile = File(volumePath)
|
||||
if (!volumeFile.exists())
|
||||
volumeFile.mkdirs()
|
||||
var returnedHash: ByteArray? = null
|
||||
if (binding.checkboxSavePassword.isChecked)
|
||||
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
|
||||
if (GocryptfsVolume.createVolume(volumePath, password, false, xchacha, GocryptfsVolume.ScryptDefaultLogN, ConstValues.creator, returnedHash)) {
|
||||
val volumeName = if (isHiddenVolume) File(volumePath).name else volumePath
|
||||
val volume = Volume(volumeName, isHiddenVolume)
|
||||
volumeDatabase.apply {
|
||||
if (isVolumeSaved(volumeName, isHiddenVolume)) // cleaning old saved path
|
||||
removeVolume(volumeName)
|
||||
saveVolume(volume)
|
||||
}
|
||||
stopTask {
|
||||
@SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
|
||||
if (binding.checkboxSavePassword.isChecked && returnedHash != null) {
|
||||
fingerprintProtector!!.let {
|
||||
it.listener = object : FingerprintProtector.Listener {
|
||||
override fun onHashStorageReset() {
|
||||
hashStorageReset = true
|
||||
// retry
|
||||
it.savePasswordHash(volume, returnedHash)
|
||||
}
|
||||
override fun onPasswordHashDecrypted(hash: ByteArray) {} // shouldn't happen here
|
||||
override fun onPasswordHashSaved() {
|
||||
Arrays.fill(returnedHash, 0)
|
||||
onVolumeCreated()
|
||||
}
|
||||
override fun onFailed(pending: Boolean) {
|
||||
if (!pending) {
|
||||
Arrays.fill(returnedHash, 0)
|
||||
onVolumeCreated()
|
||||
}
|
||||
}
|
||||
}
|
||||
it.savePasswordHash(volume, returnedHash)
|
||||
}
|
||||
} else onVolumeCreated()
|
||||
}
|
||||
} else {
|
||||
stopTask {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.create_volume_failed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
Arrays.fill(password, 0.toChar())
|
||||
}
|
||||
}
|
||||
}
|
||||
Arrays.fill(passwordConfirm, 0.toChar())
|
||||
}
|
||||
|
||||
private fun onVolumeCreated() {
|
||||
(activity as AddVolumeActivity).onVolumeAdded(hashStorageReset)
|
||||
}
|
||||
}
|
@ -0,0 +1,286 @@
|
||||
package sushi.hardcore.droidfs.add_volume
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import sushi.hardcore.droidfs.*
|
||||
import sushi.hardcore.droidfs.databinding.DialogSdcardErrorBinding
|
||||
import sushi.hardcore.droidfs.databinding.FragmentSelectPathBinding
|
||||
import sushi.hardcore.droidfs.util.PathUtils
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import java.io.File
|
||||
|
||||
class SelectPathFragment: Fragment() {
|
||||
companion object {
|
||||
private const val KEY_THEME_VALUE = "theme"
|
||||
|
||||
fun newInstance(themeValue: String): SelectPathFragment {
|
||||
return SelectPathFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(KEY_THEME_VALUE, themeValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var binding: FragmentSelectPathBinding
|
||||
private val askStoragePermissions = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
|
||||
if (result[Manifest.permission.READ_EXTERNAL_STORAGE] == true && result[Manifest.permission.WRITE_EXTERNAL_STORAGE] == true)
|
||||
safePickDirectory()
|
||||
else
|
||||
CustomAlertDialogBuilder(requireContext(), themeValue)
|
||||
.setTitle(R.string.storage_perm_denied)
|
||||
.setMessage(R.string.storage_perm_denied_msg)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
private val pickDirectory = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
|
||||
if (uri != null)
|
||||
onDirectoryPicked(uri)
|
||||
}
|
||||
private var themeValue = ConstValues.DEFAULT_THEME_VALUE
|
||||
private lateinit var volumeDatabase: VolumeDatabase
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentSelectPathBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
arguments?.let { arguments ->
|
||||
arguments.getString(KEY_THEME_VALUE)?.let { themeValue = it }
|
||||
}
|
||||
volumeDatabase = VolumeDatabase(requireContext())
|
||||
binding.containerHiddenVolume.setOnClickListener {
|
||||
binding.switchHiddenVolume.performClick()
|
||||
}
|
||||
binding.switchHiddenVolume.setOnClickListener {
|
||||
showRightSection()
|
||||
}
|
||||
binding.buttonPickDirectory.setOnClickListener {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
) +
|
||||
ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
)
|
||||
safePickDirectory()
|
||||
else
|
||||
askStoragePermissions.launch(
|
||||
arrayOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
)
|
||||
)
|
||||
} else
|
||||
safePickDirectory()
|
||||
}
|
||||
var isVolumeAlreadySaved = false
|
||||
var volumeAction: Action? = null
|
||||
binding.editVolumeName.addTextChangedListener(object: TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {}
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
isVolumeAlreadySaved = volumeDatabase.isVolumeSaved(s.toString(), binding.switchHiddenVolume.isChecked)
|
||||
if (isVolumeAlreadySaved)
|
||||
binding.textWarning.apply {
|
||||
text = getString(R.string.volume_alread_saved)
|
||||
visibility = View.VISIBLE
|
||||
}
|
||||
else
|
||||
binding.textWarning.visibility = View.GONE
|
||||
val path = File(getCurrentVolumePath())
|
||||
volumeAction = if (path.isDirectory)
|
||||
if (path.list()?.isEmpty() == true) Action.CREATE else Action.ADD
|
||||
else
|
||||
Action.CREATE
|
||||
binding.buttonAction.text = getString(when (volumeAction) {
|
||||
Action.CREATE -> R.string.create
|
||||
else -> R.string.add_volume
|
||||
})
|
||||
}
|
||||
})
|
||||
binding.editVolumeName.setOnEditorActionListener { _, _, _ -> onPathSelected(isVolumeAlreadySaved, volumeAction); true }
|
||||
binding.buttonAction.setOnClickListener { onPathSelected(isVolumeAlreadySaved, volumeAction) }
|
||||
}
|
||||
|
||||
override fun onViewStateRestored(savedInstanceState: Bundle?) {
|
||||
super.onViewStateRestored(savedInstanceState)
|
||||
(activity as AddVolumeActivity).onFragmentLoaded(true)
|
||||
showRightSection()
|
||||
}
|
||||
|
||||
private fun showRightSection() {
|
||||
if (binding.switchHiddenVolume.isChecked) {
|
||||
binding.textLabel.text = requireContext().getString(R.string.volume_name_label)
|
||||
binding.editVolumeName.hint = requireContext().getString(R.string.volume_name_hint)
|
||||
binding.buttonPickDirectory.visibility = View.GONE
|
||||
} else {
|
||||
binding.textLabel.text = requireContext().getString(R.string.volume_path_label)
|
||||
binding.editVolumeName.hint = requireContext().getString(R.string.volume_path_hint)
|
||||
binding.buttonPickDirectory.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun safePickDirectory() {
|
||||
try {
|
||||
pickDirectory.launch(null)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
CustomAlertDialogBuilder(requireContext(), themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.open_tree_failed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDirectoryPicked(uri: Uri) {
|
||||
val path = PathUtils.getFullPathFromTreeUri(uri, requireContext())
|
||||
if (path != null)
|
||||
binding.editVolumeName.setText(path)
|
||||
else
|
||||
CustomAlertDialogBuilder(requireContext(), themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.path_from_uri_null_error_msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun getCurrentVolumePath(): String {
|
||||
return if (binding.switchHiddenVolume.isChecked)
|
||||
PathUtils.pathJoin(requireContext().filesDir.path, binding.editVolumeName.text.toString())
|
||||
else
|
||||
binding.editVolumeName.text.toString()
|
||||
}
|
||||
|
||||
private fun onPathSelected(isVolumeAlreadySaved: Boolean, volumeAction: Action?) {
|
||||
if (isVolumeAlreadySaved) {
|
||||
(activity as AddVolumeActivity).onSelectedAlreadySavedVolume()
|
||||
} else {
|
||||
if (binding.switchHiddenVolume.isChecked && volumeAction == Action.CREATE) {
|
||||
CustomAlertDialogBuilder(requireContext(), themeValue)
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(R.string.hidden_volume_warning)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
addVolume(volumeAction)
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
addVolume(volumeAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addVolume(volumeAction: Action?) {
|
||||
val currentVolumeValue = binding.editVolumeName.text.toString()
|
||||
val isHidden = binding.switchHiddenVolume.isChecked
|
||||
if (currentVolumeValue.isEmpty()) {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
if (isHidden) R.string.enter_volume_name else R.string.enter_volume_path,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else if (isHidden && currentVolumeValue.contains("/")) {
|
||||
Toast.makeText(requireContext(), R.string.error_slash_in_name, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
val volumePath = getCurrentVolumePath()
|
||||
when (volumeAction!!) {
|
||||
Action.CREATE -> {
|
||||
val volumeFile = File(volumePath)
|
||||
var goodDirectory = false
|
||||
if (volumeFile.isFile) {
|
||||
Toast.makeText(requireContext(), R.string.error_is_file, Toast.LENGTH_SHORT).show()
|
||||
} else if (volumeFile.isDirectory) {
|
||||
val dirContent = volumeFile.list()
|
||||
if (dirContent != null) {
|
||||
if (dirContent.isEmpty()) {
|
||||
if (volumeFile.canWrite())
|
||||
goodDirectory = true
|
||||
else
|
||||
errorDirectoryNotWritable(volumePath)
|
||||
} else
|
||||
Toast.makeText(requireContext(), R.string.dir_not_empty, Toast.LENGTH_SHORT).show()
|
||||
} else
|
||||
Toast.makeText(requireContext(), R.string.listdir_null_error_msg, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
if (File(PathUtils.getParentPath(volumePath)).canWrite())
|
||||
goodDirectory = true
|
||||
else
|
||||
errorDirectoryNotWritable(volumePath)
|
||||
}
|
||||
if (goodDirectory)
|
||||
(activity as AddVolumeActivity).createVolume(volumePath, isHidden)
|
||||
}
|
||||
Action.ADD -> {
|
||||
if (!GocryptfsVolume.isGocryptfsVolume(File(volumePath))) {
|
||||
CustomAlertDialogBuilder(requireContext(), themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.error_not_a_volume)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
} else if (!File(volumePath).canWrite()) {
|
||||
val dialog = CustomAlertDialogBuilder(requireContext(), themeValue)
|
||||
.setTitle(R.string.warning)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { _, _ -> addVolume(if (isHidden) currentVolumeValue else volumePath, isHidden) }
|
||||
if (PathUtils.isPathOnExternalStorage(volumePath, requireContext()))
|
||||
dialog.setView(
|
||||
DialogSdcardErrorBinding.inflate(layoutInflater).apply {
|
||||
path.text = PathUtils.getPackageDataFolder(requireContext())
|
||||
}.root
|
||||
)
|
||||
else
|
||||
dialog.setMessage(R.string.add_cant_write_warning)
|
||||
dialog.show()
|
||||
} else {
|
||||
addVolume(if (isHidden) currentVolumeValue else volumePath, isHidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun errorDirectoryNotWritable(volumePath: String) {
|
||||
val dialog = CustomAlertDialogBuilder(requireContext(), themeValue)
|
||||
.setTitle(R.string.error)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
@SuppressLint("InflateParams")
|
||||
if (PathUtils.isPathOnExternalStorage(volumePath, requireContext()))
|
||||
dialog.setView(
|
||||
layoutInflater.inflate(R.layout.dialog_sdcard_error, null).apply {
|
||||
findViewById<TextView>(R.id.path).text = PathUtils.getPackageDataFolder(requireContext())
|
||||
}
|
||||
)
|
||||
else
|
||||
dialog.setMessage(R.string.create_cant_write_error_msg)
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun addVolume(volumeName: String, isHidden: Boolean) {
|
||||
volumeDatabase.saveVolume(Volume(volumeName, isHidden))
|
||||
(activity as AddVolumeActivity).onVolumeAdded(false)
|
||||
}
|
||||
}
|
@ -61,7 +61,6 @@ open class BaseExplorerActivity : BaseActivity() {
|
||||
protected var isStartingActivity = false
|
||||
private var usf_open = false
|
||||
protected var usf_keep_open = false
|
||||
private lateinit var toolbar: androidx.appcompat.widget.Toolbar
|
||||
private lateinit var titleText: TextView
|
||||
private lateinit var recycler_view_explorer: RecyclerView
|
||||
private lateinit var refresher: SwipeRefreshLayout
|
||||
@ -82,14 +81,16 @@ open class BaseExplorerActivity : BaseActivity() {
|
||||
mapFolders = sharedPrefs.getBoolean("map_folders", true)
|
||||
currentSortOrderIndex = resources.getStringArray(R.array.sort_orders_values).indexOf(sharedPrefs.getString(ConstValues.sort_order_key, "name"))
|
||||
init()
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
titleText = findViewById(R.id.title_text)
|
||||
recycler_view_explorer = findViewById(R.id.recycler_view_explorer)
|
||||
refresher = findViewById(R.id.refresher)
|
||||
textDirEmpty = findViewById(R.id.text_dir_empty)
|
||||
currentPathText = findViewById(R.id.current_path_text)
|
||||
totalSizeText = findViewById(R.id.total_size_text)
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.apply {
|
||||
setDisplayShowCustomEnabled(true)
|
||||
setCustomView(R.layout.action_bar)
|
||||
titleText = customView.findViewById(R.id.title_text)
|
||||
}
|
||||
title = ""
|
||||
titleText.text = getString(R.string.volume, volumeName)
|
||||
explorerAdapter = ExplorerElementAdapter(
|
||||
@ -176,34 +177,31 @@ open class BaseExplorerActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
protected open fun onExplorerItemClick(position: Int) {
|
||||
val wasSelecting = explorerAdapter.selectedItems.isNotEmpty()
|
||||
if (explorerAdapter.selectedItems.isEmpty()) {
|
||||
if (!wasSelecting) {
|
||||
val fullPath = explorerElements[position].fullPath
|
||||
when {
|
||||
explorerElements[position].isDirectory -> {
|
||||
setCurrentPath(fullPath)
|
||||
}
|
||||
explorerElements[position].isParentFolder -> {
|
||||
setCurrentPath(PathUtils.getParentPath(currentDirectoryPath))
|
||||
}
|
||||
isImage(fullPath) -> {
|
||||
startFileViewer(ImageViewer::class.java, fullPath)
|
||||
}
|
||||
isVideo(fullPath) -> {
|
||||
startFileViewer(VideoPlayer::class.java, fullPath)
|
||||
}
|
||||
isText(fullPath) -> {
|
||||
startFileViewer(TextEditor::class.java, fullPath)
|
||||
}
|
||||
isPDF(fullPath) -> {
|
||||
startFileViewer(PdfViewer::class.java, fullPath)
|
||||
}
|
||||
isAudio(fullPath) -> {
|
||||
startFileViewer(AudioPlayer::class.java, fullPath)
|
||||
}
|
||||
else -> showOpenAsDialog(fullPath)
|
||||
val fullPath = explorerElements[position].fullPath
|
||||
when {
|
||||
explorerElements[position].isDirectory -> {
|
||||
setCurrentPath(fullPath)
|
||||
}
|
||||
explorerElements[position].isParentFolder -> {
|
||||
setCurrentPath(PathUtils.getParentPath(currentDirectoryPath))
|
||||
}
|
||||
isImage(fullPath) -> {
|
||||
startFileViewer(ImageViewer::class.java, fullPath)
|
||||
}
|
||||
isVideo(fullPath) -> {
|
||||
startFileViewer(VideoPlayer::class.java, fullPath)
|
||||
}
|
||||
isText(fullPath) -> {
|
||||
startFileViewer(TextEditor::class.java, fullPath)
|
||||
}
|
||||
isPDF(fullPath) -> {
|
||||
startFileViewer(PdfViewer::class.java, fullPath)
|
||||
}
|
||||
isAudio(fullPath) -> {
|
||||
startFileViewer(AudioPlayer::class.java, fullPath)
|
||||
}
|
||||
else -> showOpenAsDialog(fullPath)
|
||||
}
|
||||
}
|
||||
invalidateOptionsMenu()
|
||||
@ -240,7 +238,7 @@ open class BaseExplorerActivity : BaseActivity() {
|
||||
)
|
||||
}
|
||||
}
|
||||
textDirEmpty.visibility = if (explorerElements.size == 0) View.VISIBLE else View.INVISIBLE
|
||||
textDirEmpty.visibility = if (explorerElements.size == 0) View.VISIBLE else View.GONE
|
||||
currentDirectoryPath = path
|
||||
currentPathText.text = getString(R.string.location, currentDirectoryPath)
|
||||
if (mapFolders) {
|
||||
@ -484,15 +482,12 @@ open class BaseExplorerActivity : BaseActivity() {
|
||||
val noItemSelected = explorerAdapter.selectedItems.isEmpty()
|
||||
val iconColor = ContextCompat.getColor(this, R.color.neutralIconTint)
|
||||
setMenuIconTint(menu, iconColor, R.id.sort, R.drawable.icon_sort)
|
||||
setMenuIconTint(menu, iconColor, R.id.delete, R.drawable.icon_delete)
|
||||
setMenuIconTint(menu, iconColor, R.id.decrypt, R.drawable.icon_decrypt)
|
||||
setMenuIconTint(menu, iconColor, R.id.share, R.drawable.icon_share)
|
||||
menu.findItem(R.id.sort).isVisible = noItemSelected
|
||||
menu.findItem(R.id.close).isVisible = noItemSelected
|
||||
if (noItemSelected){
|
||||
toolbar.navigationIcon = null
|
||||
} else {
|
||||
toolbar.setNavigationIcon(R.drawable.icon_arrow_back)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(!noItemSelected)
|
||||
if (!noItemSelected) {
|
||||
if (explorerAdapter.selectedItems.size == 1) {
|
||||
menu.findItem(R.id.rename).isVisible = true
|
||||
if (explorerElements[explorerAdapter.selectedItems[0]].isRegularFile) {
|
||||
|
@ -12,7 +12,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import sushi.hardcore.droidfs.CameraActivity
|
||||
import sushi.hardcore.droidfs.GocryptfsVolume
|
||||
import sushi.hardcore.droidfs.OpenActivity
|
||||
import sushi.hardcore.droidfs.MainActivity
|
||||
import sushi.hardcore.droidfs.R
|
||||
import sushi.hardcore.droidfs.adapters.IconTextDialogAdapter
|
||||
import sushi.hardcore.droidfs.content_providers.ExternalProvider
|
||||
@ -177,7 +177,7 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
.setSingleChoiceItems(adapter, -1){ thisDialog, which ->
|
||||
when (adapter.getItem(which)){
|
||||
"importFromOtherVolumes" -> {
|
||||
val intent = Intent(this, OpenActivity::class.java)
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.action = "pick"
|
||||
intent.putExtra("sessionID", gocryptfsVolume.sessionID)
|
||||
isStartingActivity = true
|
||||
@ -225,7 +225,7 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
}
|
||||
thisDialog.dismiss()
|
||||
}
|
||||
.setTitle(getString(R.string.fab_dialog_title))
|
||||
.setTitle(getString(R.string.add))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ class ImageViewer: FileViewerActivity() {
|
||||
private var rotatedBitmap: Bitmap? = null
|
||||
private val hideUI = Runnable {
|
||||
binding.actionButtons.visibility = View.GONE
|
||||
binding.actionBar.visibility = View.GONE
|
||||
binding.topBar.visibility = View.GONE
|
||||
}
|
||||
private val slideshowNext = Runnable {
|
||||
if (slideshowActive){
|
||||
@ -60,13 +60,14 @@ class ImageViewer: FileViewerActivity() {
|
||||
override fun viewFile() {
|
||||
binding = ActivityImageViewerBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.hide()
|
||||
handler = Handler(mainLooper)
|
||||
binding.imageViewer.setOnInteractionListener(object : ZoomableImageView.OnInteractionListener {
|
||||
override fun onSingleTap(event: MotionEvent?) {
|
||||
handler.removeCallbacks(hideUI)
|
||||
if (binding.actionButtons.visibility == View.GONE) {
|
||||
binding.actionButtons.visibility = View.VISIBLE
|
||||
binding.actionBar.visibility = View.VISIBLE
|
||||
binding.topBar.visibility = View.VISIBLE
|
||||
handler.postDelayed(hideUI, hideDelay)
|
||||
} else {
|
||||
hideUI.run()
|
||||
|
@ -14,6 +14,7 @@ abstract class MediaPlayer: FileViewerActivity() {
|
||||
private lateinit var player: ExoPlayer
|
||||
|
||||
override fun viewFile() {
|
||||
supportActionBar?.hide()
|
||||
initializePlayer()
|
||||
}
|
||||
|
||||
|
@ -2,15 +2,13 @@ package sushi.hardcore.droidfs.file_viewers
|
||||
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import sushi.hardcore.droidfs.R
|
||||
import org.grapheneos.pdfviewer.PdfViewer
|
||||
import sushi.hardcore.droidfs.databinding.ActivityPdfViewerBinding
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
|
||||
class PdfViewer: FileViewerActivity() {
|
||||
private lateinit var binding: ActivityPdfViewerBinding
|
||||
private lateinit var pdfViewer: PdfViewer
|
||||
|
||||
override fun hideSystemUi() {
|
||||
//don't hide system ui
|
||||
@ -21,31 +19,27 @@ class PdfViewer: FileViewerActivity() {
|
||||
}
|
||||
|
||||
override fun viewFile() {
|
||||
binding = ActivityPdfViewerBinding.inflate(layoutInflater)
|
||||
val toolbar = binding.root.findViewById<Toolbar>(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
title = ""
|
||||
val titleText = toolbar.findViewById<TextView>(R.id.title_text)
|
||||
pdfViewer = ActivityPdfViewerBinding.inflate(layoutInflater).root
|
||||
val fileName = File(filePath).name
|
||||
titleText.text = fileName
|
||||
binding.pdfViewer.activity = this
|
||||
setContentView(binding.root)
|
||||
title = fileName
|
||||
pdfViewer.activity = this
|
||||
setContentView(pdfViewer)
|
||||
val fileSize = gocryptfsVolume.getSize(filePath)
|
||||
loadWholeFile(filePath, fileSize)?.let {
|
||||
binding.pdfViewer.loadPdf(ByteArrayInputStream(it), fileName, fileSize)
|
||||
pdfViewer.loadPdf(ByteArrayInputStream(it), fileName, fileSize)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
binding.pdfViewer.onCreateOptionMenu(menu)
|
||||
pdfViewer.onCreateOptionMenu(menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
return binding.pdfViewer.onPrepareOptionsMenu(menu)
|
||||
return pdfViewer.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return binding.pdfViewer.onOptionsItemSelected(item) || super.onOptionsItemSelected(item)
|
||||
return pdfViewer.onOptionsItemSelected(item) || super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
@ -6,11 +6,9 @@ import android.text.TextWatcher
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import sushi.hardcore.droidfs.R
|
||||
import sushi.hardcore.droidfs.GocryptfsVolume
|
||||
import sushi.hardcore.droidfs.R
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
@ -18,8 +16,6 @@ import java.io.File
|
||||
class TextEditor: FileViewerActivity() {
|
||||
private lateinit var fileName: String
|
||||
private lateinit var editor: EditText
|
||||
private lateinit var toolbar: Toolbar
|
||||
private lateinit var titleText: TextView
|
||||
private var changedSinceLastSave = false
|
||||
private var wordWrap = true
|
||||
override fun hideSystemUi() {
|
||||
@ -51,11 +47,8 @@ class TextEditor: FileViewerActivity() {
|
||||
} else {
|
||||
setContentView(R.layout.activity_text_editor)
|
||||
}
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
title = ""
|
||||
titleText = findViewById(R.id.title_text)
|
||||
titleText.text = fileName
|
||||
title = fileName
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
editor = findViewById(R.id.text_editor)
|
||||
editor.setText(fileContent)
|
||||
editor.addTextChangedListener(object: TextWatcher {
|
||||
@ -67,7 +60,7 @@ class TextEditor: FileViewerActivity() {
|
||||
if (!changedSinceLastSave){
|
||||
changedSinceLastSave = true
|
||||
@SuppressLint("SetTextI18n")
|
||||
titleText.text = "*$fileName"
|
||||
title = "*$fileName"
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -122,7 +115,6 @@ class TextEditor: FileViewerActivity() {
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.text_editor, menu)
|
||||
toolbar.setNavigationIcon(R.drawable.icon_arrow_back)
|
||||
menu.findItem(R.id.word_wrap).isChecked = wordWrap
|
||||
return true
|
||||
}
|
||||
@ -135,7 +127,7 @@ class TextEditor: FileViewerActivity() {
|
||||
R.id.menu_save -> {
|
||||
if (save()){
|
||||
changedSinceLastSave = false
|
||||
titleText.text = fileName
|
||||
title = fileName
|
||||
}
|
||||
}
|
||||
R.id.word_wrap -> {
|
||||
|
@ -1,20 +0,0 @@
|
||||
package sushi.hardcore.droidfs.util
|
||||
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
|
||||
object WidgetUtil {
|
||||
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
|
||||
}
|
||||
}
|
@ -3,14 +3,11 @@ package sushi.hardcore.droidfs.util
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.widget.EditText
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import sushi.hardcore.droidfs.ConstValues
|
||||
import sushi.hardcore.droidfs.R
|
||||
import java.io.*
|
||||
import java.lang.Exception
|
||||
import java.lang.StringBuilder
|
||||
import java.lang.UnsupportedOperationException
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.util.*
|
||||
import kotlin.math.ceil
|
||||
|
||||
@ -81,18 +78,4 @@ object Wiper {
|
||||
return e.message
|
||||
}
|
||||
}
|
||||
private fun randomString(minSize: Int, maxSize: Int): String {
|
||||
val r = Random()
|
||||
val sb = StringBuilder()
|
||||
val length = r.nextInt(maxSize-minSize)+minSize
|
||||
for (i in 0..length){
|
||||
sb.append((r.nextInt(94)+32).toChar())
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
fun wipeEditText(editText: EditText){
|
||||
if (editText.text.isNotEmpty()){
|
||||
editText.setText(randomString(editText.text.length, editText.text.length*3))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package sushi.hardcore.droidfs.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.widget.ListAdapter
|
||||
import android.widget.ListView
|
||||
import androidx.core.content.ContextCompat
|
||||
import sushi.hardcore.droidfs.R
|
||||
|
||||
class NonScrollableColoredBorderListView: ListView {
|
||||
constructor(context: Context) : super(context) { applyColor() }
|
||||
constructor(context: Context, attrs: AttributeSet): super(context, attrs) { applyColor() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int): super(context, attrs, defStyleAttr) { applyColor() }
|
||||
|
||||
val colorAccent: Int
|
||||
|
||||
init {
|
||||
val typedValue = TypedValue()
|
||||
context.theme.resolveAttribute(R.attr.colorAccent, typedValue, true)
|
||||
colorAccent = typedValue.data
|
||||
}
|
||||
|
||||
fun applyColor() {
|
||||
divider = ColorDrawable(colorAccent)
|
||||
dividerHeight = context.resources.displayMetrics.density.toInt()*2
|
||||
background = ContextCompat.getDrawable(context, R.drawable.listview_border)
|
||||
}
|
||||
|
||||
fun computeHeight(): Int {
|
||||
var totalHeight = 0
|
||||
for (i in 0 until adapter.count){
|
||||
val item = adapter.getView(i, null, this)
|
||||
item.measure(0, 0)
|
||||
totalHeight += item.measuredHeight
|
||||
}
|
||||
return totalHeight + (dividerHeight * (adapter.count-1))
|
||||
}
|
||||
|
||||
override fun setAdapter(adapter: ListAdapter?) {
|
||||
super.setAdapter(adapter)
|
||||
layoutParams.height = computeHeight()
|
||||
}
|
||||
}
|
@ -35,7 +35,8 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_00024Companion_createVolume(JNIEnv *
|
||||
jboolean plainTextNames,
|
||||
jint xchacha,
|
||||
jint logN,
|
||||
jstring jcreator) {
|
||||
jstring jcreator,
|
||||
jbyteArray jreturned_hash) {
|
||||
const char* root_cipher_dir = (*env)->GetStringUTFChars(env, jroot_cipher_dir, NULL);
|
||||
const char* creator = (*env)->GetStringUTFChars(env, jcreator, NULL);
|
||||
GoString gofilename = {root_cipher_dir, strlen(root_cipher_dir)}, gocreator = {creator, strlen(creator)};
|
||||
@ -46,12 +47,33 @@ Java_sushi_hardcore_droidfs_GocryptfsVolume_00024Companion_createVolume(JNIEnv *
|
||||
jcharArray_to_charArray(jchar_password, password, password_len);
|
||||
GoSlice go_password = {password, password_len, password_len};
|
||||
|
||||
GoUint8 result = gcf_create_volume(gofilename, go_password, plainTextNames, xchacha, logN, gocreator);
|
||||
size_t returned_hash_len;
|
||||
jbyte* jbyte_returned_hash;
|
||||
unsigned char* returned_hash;
|
||||
GoSlice go_returned_hash = {NULL, 0, 0};
|
||||
if (!(*env)->IsSameObject(env, jreturned_hash, NULL)) {
|
||||
returned_hash_len = (*env)->GetArrayLength(env, jreturned_hash);
|
||||
jbyte_returned_hash = (*env)->GetByteArrayElements(env, jreturned_hash, NULL);
|
||||
returned_hash = malloc(returned_hash_len);
|
||||
go_returned_hash.data = returned_hash;
|
||||
go_returned_hash.len = returned_hash_len;
|
||||
go_returned_hash.cap = returned_hash_len;
|
||||
}
|
||||
|
||||
GoUint8 result = gcf_create_volume(gofilename, go_password, plainTextNames, xchacha, logN, gocreator, go_returned_hash);
|
||||
|
||||
(*env)->ReleaseStringUTFChars(env, jroot_cipher_dir, root_cipher_dir);
|
||||
(*env)->ReleaseStringUTFChars(env, jcreator, creator);
|
||||
wipe(password, password_len);
|
||||
(*env)->ReleaseCharArrayElements(env, jpassword, jchar_password, 0);
|
||||
|
||||
if (!(*env)->IsSameObject(env, jreturned_hash, NULL)) {
|
||||
unsignedCharArray_to_jbyteArray(returned_hash, jbyte_returned_hash, returned_hash_len);
|
||||
wipe(returned_hash, returned_hash_len);
|
||||
free(returned_hash);
|
||||
(*env)->ReleaseByteArrayElements(env, jreturned_hash, jbyte_returned_hash, 0);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||
</vector>
|
@ -1,5 +1,5 @@
|
||||
<vector android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?attr/colorAccent" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
<path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
||||
|
9
app/src/main/res/drawable/icon_hidden.xml
Normal file
9
app/src/main/res/drawable/icon_hidden.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorAccent"
|
||||
android:pathData="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/icon_volume.xml
Normal file
9
app/src/main/res/drawable/icon_volume.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M4,4C2.9,4 2.0098,4.9 2.0098,6L2,18C2,19.1 2.9,20 4,20L20,20C21.1,20 22,19.1 22,18L22,8C22,6.9 21.1,6 20,6L12,6L10,4L4,4zM12,8.3867C13.2156,8.3867 14.2012,9.3743 14.2012,10.5898L14.2012,11.4707L14.6426,11.4707C15.127,11.4707 15.5234,11.8671 15.5234,12.3516L15.5234,16.7559C15.5234,17.2403 15.127,17.6367 14.6426,17.6367L9.3574,17.6367C8.873,17.6367 8.4766,17.2403 8.4766,16.7559L8.4766,12.3516C8.4766,11.8671 8.873,11.4707 9.3574,11.4707L9.7988,11.4707L9.7988,10.5898C9.7988,9.3743 10.7844,8.3867 12,8.3867zM12,9.2246C11.2469,9.2246 10.6348,9.8367 10.6348,10.5898L10.6348,11.4707L13.3652,11.4707L13.3652,10.5898C13.3652,9.8367 12.7531,9.2246 12,9.2246zM12,13.6719C11.5155,13.6719 11.1191,14.0683 11.1191,14.5527C11.1191,15.0372 11.5155,15.4336 12,15.4336C12.4845,15.4336 12.8809,15.0372 12.8809,14.5527C12.8809,14.0683 12.4845,13.6719 12,13.6719z"
|
||||
android:fillColor="?attr/colorAccent"/>
|
||||
</vector>
|
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#00000000"/>
|
||||
<corners android:radius="5dp"/>
|
||||
<stroke android:width="1dp" android:color="?attr/colorAccent"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
Binary file not shown.
Before Width: | Height: | Size: 49 KiB |
8
app/src/main/res/layout/action_bar.xml
Normal file
8
app/src/main/res/layout/action_bar.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/title_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="start"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title" />
|
12
app/src/main/res/layout/activity_add_volume.xml
Normal file
12
app/src/main/res/layout/activity_add_volume.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</ScrollView>
|
@ -12,7 +12,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
android:textSize="18sp"
|
||||
android:textSize="@dimen/title_text_size"
|
||||
android:padding="10dp"/>
|
||||
|
||||
<com.google.android.exoplayer2.ui.PlayerControlView
|
||||
|
@ -1,111 +1,88 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<include layout="@layout/toolbar"/>
|
||||
|
||||
<ScrollView
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/volume_operation_horizontal_gap"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical"
|
||||
android:paddingVertical="@dimen/volume_operation_vertical_gap">
|
||||
|
||||
<LinearLayout
|
||||
<TextView
|
||||
android:id="@+id/text_volume_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp">
|
||||
android:layout_marginBottom="@dimen/volume_operation_vertical_gap"
|
||||
android:gravity="center"
|
||||
android:textSize="@dimen/title_text_size"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<include layout="@layout/volume_path_section"/>
|
||||
<TextView
|
||||
android:id="@+id/text_current_password_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/current_password_label" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<EditText
|
||||
android:id="@+id/edit_current_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="@dimen/volume_operation_vertical_gap"
|
||||
android:autofillHints="password"
|
||||
android:hint="@string/current_password_hint"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="@dimen/change_password_activity_label_width"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/old_password"
|
||||
android:textSize="@dimen/edit_text_label_text_size" />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/new_password_label" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edit_old_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"/>
|
||||
<EditText
|
||||
android:id="@+id/edit_new_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="password"
|
||||
android:hint="@string/new_password_hint"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1" />
|
||||
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/volume_operation_vertical_gap"
|
||||
android:text="@string/new_password_confirmation_label" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<EditText
|
||||
android:id="@+id/edit_password_confirm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autofillHints="password"
|
||||
android:hint="@string/password_confirmation_hint"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="@dimen/change_password_activity_label_width"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/new_password"
|
||||
android:textSize="@dimen/edit_text_label_text_size" />
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox_save_password"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="@dimen/volume_operation_vertical_gap"
|
||||
android:text="@string/fingerprint_save_checkbox_text" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edit_new_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"/>
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/volume_operation_button_height"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="70dp"
|
||||
android:layout_marginTop="@dimen/volume_operation_vertical_gap"
|
||||
android:text="@string/change_password" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:layout_width="@dimen/change_password_activity_label_width"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/new_password_confirmation"
|
||||
android:textSize="@dimen/edit_text_label_text_size" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edit_new_password_confirm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"
|
||||
android:imeOptions="actionDone"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include layout="@layout/checkboxes_section"/>
|
||||
|
||||
<sushi.hardcore.droidfs.widgets.NonScrollableColoredBorderListView
|
||||
android:id="@+id/saved_path_listview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/action_activity_listview_margin_horizontal"
|
||||
android:layout_marginTop="@dimen/action_activity_listview_margin_top"
|
||||
android:background="@drawable/listview_border"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/warning_msg_padding"
|
||||
android:gravity="center"
|
||||
android:text="@string/create_password_warning"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/button_change_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/action_activity_button_height"
|
||||
android:layout_marginHorizontal="@dimen/action_activity_button_horizontal_margin"
|
||||
android:layout_marginBottom="@dimen/action_activity_button_margin_bottom"
|
||||
android:text="@string/change_password"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
@ -1,100 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/toolbar"/>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp">
|
||||
|
||||
<include layout="@layout/volume_path_section"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:layout_width="@dimen/create_activity_label_width"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/password"
|
||||
android:textSize="@dimen/edit_text_label_text_size" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edit_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:layout_width="@dimen/create_activity_label_width"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/password_confirm"
|
||||
android:textSize="@dimen/edit_text_label_text_size" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edit_password_confirm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"
|
||||
android:imeOptions="actionDone"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="15dp"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/encryption_cipher_label"/>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_xchacha"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include layout="@layout/checkboxes_section"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/warning_msg_padding"
|
||||
android:gravity="center"
|
||||
android:text="@string/create_password_warning"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/button_create"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/action_activity_button_height"
|
||||
android:layout_marginHorizontal="@dimen/action_activity_button_horizontal_margin"
|
||||
android:text="@string/create"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
@ -6,8 +6,6 @@
|
||||
android:orientation="vertical"
|
||||
tools:context="sushi.hardcore.droidfs.explorers.ExplorerActivity">
|
||||
|
||||
<include layout="@layout/toolbar"/>
|
||||
|
||||
<include layout="@layout/explorer_info_bar"/>
|
||||
|
||||
<RelativeLayout
|
||||
|
@ -6,8 +6,6 @@
|
||||
android:orientation="vertical"
|
||||
tools:context="sushi.hardcore.droidfs.explorers.BaseExplorerActivity">
|
||||
|
||||
<include layout="@layout/toolbar"/>
|
||||
|
||||
<include layout="@layout/explorer_info_bar"/>
|
||||
|
||||
<RelativeLayout
|
||||
|
@ -4,8 +4,6 @@
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include layout="@layout/toolbar"/>
|
||||
|
||||
<include layout="@layout/explorer_info_bar"/>
|
||||
|
||||
<RelativeLayout
|
||||
|
@ -13,7 +13,7 @@
|
||||
android:layout_gravity="center"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/action_bar"
|
||||
android:id="@+id/top_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
@ -38,7 +38,6 @@
|
||||
android:background="#00000000"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/icon_delete"
|
||||
app:tint="@color/white"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_toStartOf="@id/image_button_slideshow"
|
||||
|
@ -1,55 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
tools:context="sushi.hardcore.droidfs.MainActivity">
|
||||
|
||||
<include android:id="@+id/toolbar" layout="@layout/toolbar"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_logo"
|
||||
<TextView
|
||||
android:id="@+id/text_no_volumes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="30dp"
|
||||
android:src="@drawable/logo"
|
||||
app:tint="?attr/colorAccent"
|
||||
android:layout_weight="1"/>
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:textSize="25sp"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:text="@string/no_volumes_text"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view_volumes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/button_open"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/main_activity_button_height"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/button_create"
|
||||
android:layout_marginHorizontal="@dimen/main_activity_button_hor_margin"
|
||||
android:text="@string/open_volume"/>
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_margin="15dp"
|
||||
android:src="@drawable/icon_add"
|
||||
android:contentDescription="@string/add" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/button_create"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/main_activity_button_height"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_open"
|
||||
app:layout_constraintBottom_toTopOf="@id/button_change_password"
|
||||
android:layout_marginHorizontal="@dimen/main_activity_button_hor_margin"
|
||||
android:text="@string/create_volume"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/button_change_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/main_activity_button_height"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_create"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginHorizontal="@dimen/main_activity_button_hor_margin"
|
||||
android:text="@string/change_volume_password"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
@ -1,70 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/toolbar"/>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="10dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include layout="@layout/volume_path_section"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:layout_width="@dimen/open_activity_label_width"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/password"
|
||||
android:textSize="@dimen/edit_text_label_text_size" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edit_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"
|
||||
android:imeOptions="actionDone"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include layout="@layout/checkboxes_section"/>
|
||||
|
||||
<sushi.hardcore.droidfs.widgets.NonScrollableColoredBorderListView
|
||||
android:id="@+id/saved_path_listview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/action_activity_listview_margin_horizontal"
|
||||
android:layout_marginTop="@dimen/action_activity_listview_margin_top"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/warning_msg_padding"
|
||||
android:gravity="center"
|
||||
android:text="@string/open_activity_warning"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/button_open"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/action_activity_button_height"
|
||||
android:layout_marginHorizontal="@dimen/action_activity_button_horizontal_margin"
|
||||
android:layout_marginBottom="@dimen/action_activity_button_margin_bottom"
|
||||
android:text="@string/open"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
@ -1,14 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<org.grapheneos.pdfviewer.PdfViewer xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include android:id="@+id/toolbar" layout="@layout/toolbar"/>
|
||||
|
||||
<org.grapheneos.pdfviewer.PdfViewer
|
||||
android:id="@+id/pdf_viewer"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"/>
|
||||
|
||||
</LinearLayout>
|
||||
android:layout_height="match_parent"/>
|
@ -1,14 +1,4 @@
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include
|
||||
android:id="@+id/toolbar"
|
||||
layout="@layout/toolbar"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@id/toolbar"/>
|
||||
</RelativeLayout>
|
||||
android:layout_height="match_parent"/>
|
@ -1,32 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:orientation="vertical">
|
||||
android:fillViewport="true">
|
||||
|
||||
<include android:id="@+id/toolbar" layout="@layout/toolbar"/>
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
<ScrollView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<ScrollView
|
||||
<EditText
|
||||
android:id="@+id/text_editor"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top"
|
||||
android:textSize="15sp"
|
||||
android:background="@null"
|
||||
android:padding="5dp"
|
||||
android:importantForAutofill="no"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/text_editor"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top"
|
||||
android:textSize="15sp"
|
||||
android:background="@null"
|
||||
android:padding="5dp"/>
|
||||
</ScrollView>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
</HorizontalScrollView>
|
@ -1,25 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:orientation="vertical">
|
||||
android:fillViewport="true">
|
||||
|
||||
<include android:id="@+id/toolbar" layout="@layout/toolbar"/>
|
||||
<EditText
|
||||
android:id="@+id/text_editor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top"
|
||||
android:textSize="15sp"
|
||||
android:background="@null"
|
||||
android:padding="5dp"
|
||||
android:importantForAutofill="no"/>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/text_editor"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="top"
|
||||
android:textSize="15sp"
|
||||
android:background="@null"
|
||||
android:padding="5dp"/>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
@ -37,14 +37,14 @@
|
||||
android:id="@+id/text_element_mtime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="10sp"
|
||||
android:textSize="@dimen/details_text_size"
|
||||
android:layout_marginEnd="7dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_element_size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="10sp"/>
|
||||
android:textSize="@dimen/details_text_size"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="10dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/volume_name_textview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginEnd="30dp"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="start"
|
||||
android:textSize="17sp"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/delete_imageview"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_margin="5dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:src="@drawable/icon_delete"/>
|
||||
|
||||
</RelativeLayout>
|
54
app/src/main/res/layout/adapter_volume.xml
Normal file
54
app/src/main/res/layout/adapter_volume.xml
Normal file
@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/selectable_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
tools:ignore="UselessParent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_icon"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_volume_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/adapter_text_size"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_path"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="7dp"
|
||||
android:textSize="12sp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_info"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:textSize="12sp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox_remember_path"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/remember_volume_path"/>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox_save_password"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/fingerprint_save_checkbox_text"/>
|
||||
|
||||
</LinearLayout>
|
24
app/src/main/res/layout/dialog_delete_volume.xml
Normal file
24
app/src/main/res/layout/dialog_delete_volume.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingHorizontal="@dimen/dialog_horizontal_padding"
|
||||
android:paddingTop="@dimen/dialog_padding_top">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/dialog_text_size"/>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox_apply_to_all"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/apply_to_all"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="5dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
@ -2,14 +2,14 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:paddingHorizontal="@dimen/dialog_horizontal_padding">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/dialog_edit_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:inputType="text"
|
||||
android:layout_marginHorizontal="30dp"
|
||||
android:imeOptions="actionDone">
|
||||
<requestFocus/>
|
||||
</EditText>
|
||||
|
@ -4,7 +4,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingTop="@dimen/dialog_padding_top"
|
||||
android:paddingBottom="20dp">
|
||||
|
||||
<ProgressBar
|
||||
|
26
app/src/main/res/layout/dialog_open_volume.xml
Normal file
26
app/src/main/res/layout/dialog_open_volume.xml
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="@dimen/dialog_horizontal_padding">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edit_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"
|
||||
android:autofillHints="password"
|
||||
android:imeOptions="actionDone">
|
||||
<requestFocus/>
|
||||
</EditText>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox_save_password"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/fingerprint_save_checkbox_text"/>
|
||||
|
||||
</LinearLayout>
|
@ -3,14 +3,14 @@
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingHorizontal="25dp"
|
||||
android:paddingTop="10dp">
|
||||
android:paddingHorizontal="@dimen/dialog_horizontal_padding"
|
||||
android:paddingTop="@dimen/dialog_padding_top">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sdcard_error_header"
|
||||
android:textSize="16sp"/>
|
||||
android:textSize="@dimen/dialog_text_size"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/path"
|
||||
@ -21,10 +21,9 @@
|
||||
android:layout_marginVertical="10dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/footer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sdcard_error_footer"
|
||||
android:textSize="16sp"/>
|
||||
android:text="@string/add_read_only"
|
||||
android:textSize="@dimen/dialog_text_size"/>
|
||||
|
||||
</LinearLayout>
|
@ -8,7 +8,7 @@
|
||||
android:gravity="center"
|
||||
android:textSize="30sp"
|
||||
android:text="@string/dir_empty"
|
||||
android:visibility="invisible"/>
|
||||
android:visibility="gone"/>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/refresher"
|
||||
|
74
app/src/main/res/layout/fragment_create_volume.xml
Normal file
74
app/src/main/res/layout/fragment_create_volume.xml
Normal file
@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginHorizontal="@dimen/volume_operation_horizontal_gap">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/password_label"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edit_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"
|
||||
android:autofillHints="password"
|
||||
android:hint="@string/password"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/password_confirmation_label"
|
||||
android:layout_marginTop="@dimen/volume_operation_vertical_gap"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edit_password_confirm"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1"
|
||||
android:autofillHints="password"
|
||||
android:hint="@string/password_confirmation_hint"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="@dimen/volume_operation_vertical_gap">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/encryption_cipher_label"
|
||||
android:layout_toStartOf="@id/spinner_xchacha"
|
||||
android:layout_alignParentStart="true"/>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_xchacha"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox_save_password"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/fingerprint_save_checkbox_text" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/button_create"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/volume_operation_button_height"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="70dp"
|
||||
android:layout_marginTop="@dimen/volume_operation_vertical_gap"
|
||||
android:text="@string/create" />
|
||||
|
||||
</LinearLayout>
|
113
app/src/main/res/layout/fragment_select_path.xml
Normal file
113
app/src/main/res/layout/fragment_select_path.xml
Normal file
@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/container_hidden_volume"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:paddingHorizontal="@dimen/volume_operation_horizontal_gap"
|
||||
android:layout_marginBottom="@dimen/volume_operation_vertical_gap">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_hidden_volume"
|
||||
android:layout_width="35dp"
|
||||
android:layout_height="35dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:src="@drawable/icon_hidden"
|
||||
android:layout_marginEnd="@dimen/volume_operation_horizontal_gap" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/switch_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_toStartOf="@id/switch_hidden_volume"
|
||||
android:layout_toEndOf="@id/icon_hidden_volume">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hidden_volume"
|
||||
android:textSize="@dimen/title_text_size" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/textColorSecondary"
|
||||
android:text="@string/hidden_volume_description"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switch_hidden_volume"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginHorizontal="@dimen/volume_operation_horizontal_gap">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/volume_path_label"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edit_volume_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0.5"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:importantForAutofill="no"
|
||||
android:hint="@string/volume_path_hint"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_pick_directory"
|
||||
android:layout_width="@dimen/image_button_size"
|
||||
android:layout_height="@dimen/image_button_size"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="#00000000"
|
||||
android:src="@drawable/icon_folder"
|
||||
android:contentDescription="@string/pick_directory" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_warning"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/textColorSecondary"
|
||||
android:layout_marginHorizontal="@dimen/volume_operation_horizontal_gap"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/button_action"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/volume_operation_button_height"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginHorizontal="@dimen/volume_operation_button_horizontal_margin"
|
||||
android:layout_marginTop="@dimen/volume_operation_vertical_gap"
|
||||
android:text="@string/add_volume" />
|
||||
|
||||
</LinearLayout>
|
@ -1,19 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/toolbar"
|
||||
android:background="@color/primary"
|
||||
app:titleTextColor="@color/textColor"
|
||||
android:elevation="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Widget.ActionBar.Title"
|
||||
android:ellipsize="start"
|
||||
android:singleLine="true" />
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
@ -1,68 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switch_hidden_volume"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hidden_volume"
|
||||
app:switchPadding="10dp"
|
||||
android:layout_gravity="center_horizontal"/>
|
||||
|
||||
<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" />
|
||||
|
||||
<EditText
|
||||
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"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_pick_directory"
|
||||
android:layout_width="@dimen/image_button_size"
|
||||
android:layout_height="@dimen/image_button_size"
|
||||
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" />
|
||||
|
||||
<EditText
|
||||
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>
|
@ -1,8 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_settings"
|
||||
app:showAsAction="always"
|
||||
android:id="@+id/select_all"
|
||||
app:showAsAction="ifRoom"
|
||||
android:visible="false"
|
||||
android:title="@string/select_all"
|
||||
android:icon="@drawable/icon_select_all"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/remove"
|
||||
app:showAsAction="ifRoom"
|
||||
android:visible="false"
|
||||
android:title="@string/remove"
|
||||
android:icon="@drawable/icon_delete" />
|
||||
|
||||
<item
|
||||
android:id="@+id/settings"
|
||||
app:showAsAction="ifRoom"
|
||||
android:title="@string/settings"
|
||||
android:icon="@drawable/icon_settings"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/forget_password"
|
||||
app:showAsAction="never"
|
||||
android:visible="false"
|
||||
android:title="@string/forget_password"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/change_password"
|
||||
app:showAsAction="never"
|
||||
android:visible="false"
|
||||
android:title="@string/change_password"/>
|
||||
|
||||
</menu>
|
@ -1,15 +1,10 @@
|
||||
<resources>
|
||||
<string name="app_name">DroidFS</string>
|
||||
<string name="open_volume">Abrir um volume</string>
|
||||
<string name="create_volume">Criar um volume</string>
|
||||
<string name="change_volume_password">Mudar a senha do volume</string>
|
||||
<string name="open">Abrir</string>
|
||||
<string name="create">Criar</string>
|
||||
<string name="change_password">Mudar a senha</string>
|
||||
<string name="password">Senha:</string>
|
||||
<string name="password_confirm">Senha (confirmação):</string>
|
||||
<string name="volume_path">Localização do volume:</string>
|
||||
<string name="volume_name">Nome do volume:</string>
|
||||
<string name="password">Senha</string>
|
||||
<string name="import_files">Importar/Criptografar arquivos</string>
|
||||
<string name="import_folder">Importar/Criptografar pasta</string>
|
||||
<string name="discovering_files">Descobrindo arquivos…</string>
|
||||
@ -31,8 +26,6 @@
|
||||
<string name="remove_failed">Eliminação de %s falhou.</string>
|
||||
<string name="passwords_mismatch">As senhas não correspondem</string>
|
||||
<string name="dir_not_empty">A pasta selecionada não está vazia</string>
|
||||
<string name="success_volume_create">Volume criado com sucesso!</string>
|
||||
<string name="success_volume_create_msg">O volume foi criado com sucesso.</string>
|
||||
<string name="create_volume_failed">Falha ao criar o volume.</string>
|
||||
<string name="open_volume_failed">Não foi possível abrir</string>
|
||||
<string name="open_volume_failed_msg">Falha ao abrir o volume. Por favor, verifique sua senha.</string>
|
||||
@ -57,18 +50,10 @@
|
||||
<string name="ask_for_wipe">Você quer limpar os arquivos originais?</string>
|
||||
<string name="wipe_failed">Não foi possível limpar : %s</string>
|
||||
<string name="wipe_successful">Operação bem sucedida!</string>
|
||||
<string name="create_password_warning">Aviso!\nEsta senha será a única maneira de descriptografar o volume e acessar os arquivos dentro.\nEscolha uma senha muito forte (não \"123456\" ou \"senha\"), não a perca e mantenha-a guardada (de preferência na sua memória).\n\nDroidFS não pode protegê-lo contra apps que capturam a tela, keyloggers, backdooring via apk, acessos comprometidos via root, despejos de memória, etc.\nNão digite as senhas em ambientes inseguros.</string>
|
||||
<string name="open_activity_warning">Aviso!\nA abertura de volumes em ambientes inseguros pode levar a vazamentos de dados.\nDroidFS não pode protegê-lo contra apps que capturam a tela, keyloggers, backdooring via apk, acessos comprometidos via root, despejos de memória, etc.\nNão abra volumes contendo dados sensíveis a menos que você saiba exatamente o que está fazendo.</string>
|
||||
<string name="rename">Renomear</string>
|
||||
<string name="rename_title">Novo nome:</string>
|
||||
<string name="rename_failed">Falha ao renomear %s</string>
|
||||
<string name="remember_volume_path">Lembrar a localização do volume</string>
|
||||
<string name="sort_order">Tipo de classificação:</string>
|
||||
<string name="old_password">Senha antiga:</string>
|
||||
<string name="new_password">Nova senha:</string>
|
||||
<string name="new_password_confirmation">Nova senha (confirmação):</string>
|
||||
<string name="success_change_password">Senha alterada com sucesso!</string>
|
||||
<string name="success_change_password_msg">A senha do volume foi alterada com sucesso.</string>
|
||||
<string name="change_password_failed">A operação falhou. Por favor, verifique sua senha antiga.</string>
|
||||
<string name="share_menu_label">Criptografar via DroidFS</string>
|
||||
<string name="share_intent_parsing_failed">Falha ao processar o intento de compartilhamento.</string>
|
||||
@ -76,15 +61,6 @@
|
||||
<string name="fingerprint_save_checkbox_text">Salvar o hash da senha usando impressão digital</string>
|
||||
<string name="fingerprint_instruction">Por favor, toque no sensor da impressão digital</string>
|
||||
<string name="open_failed_hash_msg">Falha ao abrir o volume. A senha poderia ter mudado.</string>
|
||||
<string name="authentication_failed">A autenticação falhou</string>
|
||||
<string name="delete_hash_or_all">Excluir apenas o hash da senha ou o hash da senha com a localização salva? (Isto não excluirá o próprio volume)</string>
|
||||
<string name="password_hash_and_path">Hash e local da senha</string>
|
||||
<string name="password_hash">Hash da senha</string>
|
||||
<string name="ask_delete_volume_path">Tem certeza que quer remover o local de volume? (Isto não excluirá o próprio volume)</string>
|
||||
<string name="hidden_volume_delete_question_hash">Excluir somente o hash da senha, o hash da senha com a localização salva ou o volume COMPLETO (incluindo tudo conteúdo)?</string>
|
||||
<string name="whole_volume">Volume completo</string>
|
||||
<string name="hidden_volume_delete_question">Excluir somente o local ou o volume COMPLETO (incluindo tudo conteúdo)?</string>
|
||||
<string name="path_only">Somente a localização</string>
|
||||
<string name="illegal_block_size_exception">IllegalBlockSizeException</string>
|
||||
<string name="illegal_block_size_exception_msg">Isto poderia ter acontecido, se você tiver adicionado uma nova impressão digital. Tente resetar o hash do armazenamento para resolver este problema.</string>
|
||||
<string name="reset_hash_storage">Resetar o hash do armazenamento</string>
|
||||
@ -92,7 +68,6 @@
|
||||
<string name="hash_storage_reset">O hash do armazenamento foi resetado com sucesso</string>
|
||||
<string name="encrypt_action_description">Criptografando e salvando o hash da senha.</string>
|
||||
<string name="decrypt_action_description">Descriptografando o hash da senha.</string>
|
||||
<string name="hash_saved_hint">O hash da senha salvo</string>
|
||||
<string name="title_activity_settings">Configurações do DroidFS</string>
|
||||
<string name="explorer">Navegador</string>
|
||||
<string name="settings_title_sort_order">Padrão de classificação</string>
|
||||
@ -140,7 +115,7 @@
|
||||
<string name="decrypt_files">Exportar/Descriptografar</string>
|
||||
<string name="copy_failed">Falha ao copiar o %s .</string>
|
||||
<string name="copy_success">Cópia feita!</string>
|
||||
<string name="fab_dialog_title">Adicionar</string>
|
||||
<string name="add">Adicionar</string>
|
||||
<string name="camera">Câmera</string>
|
||||
<string name="picture_save_success">Foto salva em %s</string>
|
||||
<string name="picture_save_failed">Falha ao salvar esta imagem.</string>
|
||||
@ -154,13 +129,8 @@
|
||||
<string name="enter_timer_duration">Definir a duração do tempo (em s)</string>
|
||||
<string name="timer_empty_error_msg">Por favor, digite um valor numérico</string>
|
||||
<string name="path_from_uri_null_error_msg">Falha ao carregar a localização selecionada.</string>
|
||||
<string name="open_read_only">Abrindo o volume com permissões somente de leitura.</string>
|
||||
<string name="create_cant_write_error_msg">DroidFS não pode salvar neste local. Por favor, tente algum outro.</string>
|
||||
<string name="open_cant_write_warning">DroidFS não tem permissão para salvar neste local. Abrindo o volume apenas com acesso de leitura.</string>
|
||||
<string name="open_cant_write_error_msg">DroidFS não tem permissão para salvar neste local. Por favor, tente outro volume.</string>
|
||||
<string name="sdcard_error_header">DroidFS só pode escrever em memórias SD removíveis sob:</string>
|
||||
<string name="sdcard_error_footer">Por favor, use um subdiretório desta localização ou armazenamento interno.</string>
|
||||
<string name="change_pwd_cant_write_error_msg">DroidFS não tem permissão para salvar neste local. Pode tentar mover o volume para um local com permissão de escrita.</string>
|
||||
<string name="slideshow_stopped">Apresentação parou</string>
|
||||
<string name="slideshow_started">Apresentação começou</string>
|
||||
<string name="ask_save_img_rotated">A imagem foi alternada. Você deseja salvar estas mudanças e substituir a imagem original?</string>
|
||||
@ -210,4 +180,6 @@
|
||||
<string name="thumbnails_summary">Mostrar imagens e vídeos em miniatura</string>
|
||||
<string name="seek_seconds_forward">+%d segundos</string>
|
||||
<string name="seek_seconds_backward">-%d segundos</string>
|
||||
<string name="password_confirmation_hint">Senha (confirmação)</string>
|
||||
<string name="new_password_hint">Nova senha</string>
|
||||
</resources>
|
||||
|
@ -1,14 +1,9 @@
|
||||
<resources>
|
||||
<string name="open_volume">Открыть том</string>
|
||||
<string name="create_volume">Создать том</string>
|
||||
<string name="change_volume_password">Изменить пароль тома</string>
|
||||
<string name="open">Открыть</string>
|
||||
<string name="create">Создать</string>
|
||||
<string name="change_password">Изменить пароль</string>
|
||||
<string name="password">Пароль:</string>
|
||||
<string name="password_confirm">Повтор пароля:</string>
|
||||
<string name="volume_path">Путь тома:</string>
|
||||
<string name="volume_name">Имя тома:</string>
|
||||
<string name="password">Пароль</string>
|
||||
<string name="import_files">Импорт/шифрование файлов</string>
|
||||
<string name="import_folder">Импорт/шифрование папки</string>
|
||||
<string name="discovering_files">Обнаружение файлов…</string>
|
||||
@ -29,8 +24,6 @@
|
||||
<string name="remove_failed">Ошибка при удалении %s.</string>
|
||||
<string name="passwords_mismatch">Пароли не совпадают</string>
|
||||
<string name="dir_not_empty">Выбранная папка не пустая</string>
|
||||
<string name="success_volume_create">Том создан!</string>
|
||||
<string name="success_volume_create_msg">Том успешно создан.</string>
|
||||
<string name="create_volume_failed">Невозможно создать том.</string>
|
||||
<string name="open_volume_failed">Ошибка открытия</string>
|
||||
<string name="open_volume_failed_msg">Невозможно открыть том. Проверьте пароль.</string>
|
||||
@ -55,18 +48,10 @@
|
||||
<string name="ask_for_wipe">Уничтожить исходные файлы?</string>
|
||||
<string name="wipe_failed">Ошибка при уничтожении: %s</string>
|
||||
<string name="wipe_successful">Файлы уничтожены!</string>
|
||||
<string name="create_password_warning">Предупреждение!\nЭтот пароль будет единственным способом расшифровать том и получить доступ к файлам внутри.\nВыберите надёжный пароль (не \"123456\" или \"password\"), не потеряйте его и храните в безопасности (желательно только в своей голове).\n\nDroidFS не может защитить вас от приложений записи экрана, клавиатурных шпионов, скрытых недокументированных функций приложений, скомпрометированных прав root-доступа, дампов памяти и т.д.\nНе вводите пароль в небезопасных средах.</string>
|
||||
<string name="open_activity_warning">Предупреждение!\nОткрытие томов в небезопасных средах может привести к утечке данных.\nDroidFS не может защитить вас от приложений записи экрана, клавиатурных шпионов, скрытых недокументированных функций приложений, скомпрометированных прав root-доступа, дампов памяти и т.д.\nНе открывайте тома, содержащие конфиденциальные данные, если не уверены в своих действиях.</string>
|
||||
<string name="rename">Переименовать</string>
|
||||
<string name="rename_title">Новое имя:</string>
|
||||
<string name="rename_failed">Ошибка при переименовании %s.</string>
|
||||
<string name="remember_volume_path">Запомнить путь тома</string>
|
||||
<string name="sort_order">Порядок сортировки:</string>
|
||||
<string name="old_password">Прежний пароль:</string>
|
||||
<string name="new_password">Новый пароль:</string>
|
||||
<string name="new_password_confirmation">Повтор пароля:</string>
|
||||
<string name="success_change_password">Пароль изменён!</string>
|
||||
<string name="success_change_password_msg">Пароль тома успешно изменён.</string>
|
||||
<string name="change_password_failed">Операция не выполнена. Проверьте прежний пароль.</string>
|
||||
<string name="share_menu_label">Зашифровать в DroidFS</string>
|
||||
<string name="share_intent_parsing_failed">Невозможно обработать запрос на совместное использование.</string>
|
||||
@ -74,22 +59,12 @@
|
||||
<string name="fingerprint_save_checkbox_text">Сохранить хеш пароля отпечатком пальца</string>
|
||||
<string name="fingerprint_instruction">Прикоснитесь к сканеру отпечатков пальцев</string>
|
||||
<string name="open_failed_hash_msg">Невозможно открыть том. Возможно, изменился пароль.</string>
|
||||
<string name="authentication_failed">Ошибка аутентификации</string>
|
||||
<string name="delete_hash_or_all">Удалить только хеш пароля или хеш пароля и сохранённый путь? (Сам том не будет удалён.)</string>
|
||||
<string name="password_hash_and_path">Хеш пароля и путь</string>
|
||||
<string name="password_hash">Хеш пароля</string>
|
||||
<string name="ask_delete_volume_path">Забыть путь к тому? (Сам том не будет удалён.)</string>
|
||||
<string name="hidden_volume_delete_question_hash">Удалить только хеш пароля, хеш пароля и сохранённый путь или ВЕСЬ том (включая его содержимое)?</string>
|
||||
<string name="whole_volume">Весь том</string>
|
||||
<string name="hidden_volume_delete_question">Удалить только путь или ВЕСЬ том (включая его содержимое)?</string>
|
||||
<string name="path_only">Только путь</string>
|
||||
<string name="illegal_block_size_exception_msg">Это могло произойти, если вы добавили новый отпечаток пальца. Сброс хранилища хешей должен решить эту проблему.</string>
|
||||
<string name="reset_hash_storage">Сброс хранилища хешей</string>
|
||||
<string name="MAC_verification_failed">Проверка подписи/MAC не выполнена. Хранилище ключей Android или сохранённый хеш был изменён. Сброс хранилища хешей должен решить эту проблему.</string>
|
||||
<string name="hash_storage_reset">Хранилище хешей успешно сброшено</string>
|
||||
<string name="encrypt_action_description">Шифрование и сохранение хеша пароля</string>
|
||||
<string name="decrypt_action_description">Расшифровка хеша пароля</string>
|
||||
<string name="hash_saved_hint">Хеш сохранённого пароля</string>
|
||||
<string name="title_activity_settings">Настройки DroidFS</string>
|
||||
<string name="explorer">Проводник</string>
|
||||
<string name="settings_title_sort_order">Порядок сортировки</string>
|
||||
@ -135,7 +110,7 @@
|
||||
<string name="decrypt_files">Экспорт/расшифровка</string>
|
||||
<string name="copy_failed">Ошибка при копировании %s.</string>
|
||||
<string name="copy_success">Копирование выполнено!</string>
|
||||
<string name="fab_dialog_title">Добавить</string>
|
||||
<string name="add">Добавить</string>
|
||||
<string name="camera">Камера</string>
|
||||
<string name="picture_save_success">Изображение сохранено в %s</string>
|
||||
<string name="picture_save_failed">Невозможно сохранить изображение.</string>
|
||||
@ -150,12 +125,7 @@
|
||||
<string name="timer_empty_error_msg">Введите числовое значение.</string>
|
||||
<string name="path_from_uri_null_error_msg">Невозможно получить выбранный путь.</string>
|
||||
<string name="create_cant_write_error_msg">DroidFS не имеет доступа на запись по этому пути. Попробуйте найти другое место.</string>
|
||||
<string name="open_read_only">Открытие тома с доступом только для чтения.</string>
|
||||
<string name="open_cant_write_warning">DroidFS не имеет доступа на запись по этому пути. Том открывается только для чтения.</string>
|
||||
<string name="open_cant_write_error_msg">DroidFS не имеет доступа на запись по этому пути. Попробуйте другой том.</string>
|
||||
<string name="sdcard_error_header">DroidFS может записывать на SD-карты только в:</string>
|
||||
<string name="sdcard_error_footer">Используйте вложенную папку этого пути или внутреннее хранилище.</string>
|
||||
<string name="change_pwd_cant_write_error_msg">DroidFS не имеет доступа на запись по этому пути. Попробуйте переместить том в место, доступное для записи.</string>
|
||||
<string name="slideshow_stopped">Слайдшоу остановлено</string>
|
||||
<string name="slideshow_started">Слайдшоу началось</string>
|
||||
<string name="ask_save_img_rotated">Изображение было повёрнуто. Сохранить изменения и перезаписать исходное изображение?</string>
|
||||
@ -204,4 +174,6 @@
|
||||
<string name="thumbnails_summary">Показывать эскизы изображений и видео</string>
|
||||
<string name="seek_seconds_forward">+%d сек.</string>
|
||||
<string name="seek_seconds_backward">-%d сек.</string>
|
||||
<string name="password_confirmation_hint">Повтор пароля</string>
|
||||
<string name="new_password_hint">Новый пароль</string>
|
||||
</resources>
|
||||
|
@ -12,6 +12,7 @@
|
||||
<color name="neutralIconTint">@color/white</color>
|
||||
<color name="fullScreenBackgroundColor">@color/black</color>
|
||||
<color name="textColor">@color/white</color>
|
||||
<color name="textColorSecondary">#AAAAAA</color>
|
||||
<color name="itemSelected">#66666666</color>
|
||||
<color name="playerDoubleTapTouch">#18FFFFFF</color>
|
||||
<color name="playerDoubleTapBackground">#20EEEEEE</color>
|
||||
|
@ -1,17 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="main_activity_button_height">60dp</dimen>
|
||||
<dimen name="main_activity_button_hor_margin">50dp</dimen>
|
||||
<dimen name="create_activity_label_width">100dp</dimen>
|
||||
<dimen name="edit_text_label_text_size">15sp</dimen>
|
||||
<dimen name="open_activity_label_width">90dp</dimen>
|
||||
<dimen name="change_password_activity_label_width">100dp</dimen>
|
||||
<dimen name="action_activity_button_horizontal_margin">60dp</dimen>
|
||||
<dimen name="action_activity_button_height">50dp</dimen>
|
||||
<dimen name="action_activity_listview_margin_horizontal">50dp</dimen>
|
||||
<dimen name="action_activity_listview_margin_top">20dp</dimen>
|
||||
<dimen name="warning_msg_padding">20dp</dimen>
|
||||
<dimen name="action_activity_button_margin_bottom">20dp</dimen>
|
||||
<dimen name="adapter_text_size">18sp</dimen>
|
||||
<dimen name="title_text_size">18sp</dimen>
|
||||
<dimen name="adapter_text_size">@dimen/title_text_size</dimen>
|
||||
<dimen name="image_button_size">40dp</dimen>
|
||||
<dimen name="volume_operation_horizontal_gap">20dp</dimen>
|
||||
<dimen name="volume_operation_vertical_gap">20dp</dimen>
|
||||
<dimen name="volume_operation_button_height">40dp</dimen>
|
||||
<dimen name="volume_operation_button_horizontal_margin">90dp</dimen>
|
||||
<dimen name="details_text_size">10sp</dimen>
|
||||
<dimen name="dialog_horizontal_padding">30dp</dimen>
|
||||
<dimen name="dialog_padding_top">10dp</dimen>
|
||||
<dimen name="dialog_text_size">16sp</dimen>
|
||||
</resources>
|
@ -1,15 +1,10 @@
|
||||
<resources>
|
||||
<string name="app_name">DroidFS</string>
|
||||
<string name="open_volume">Open a volume</string>
|
||||
<string name="create_volume">Create a volume</string>
|
||||
<string name="change_volume_password">Change a volume\'s password</string>
|
||||
<string name="create_volume">Create volume</string>
|
||||
<string name="open">Open</string>
|
||||
<string name="create">Create</string>
|
||||
<string name="change_password">Change password</string>
|
||||
<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="password">Password</string>
|
||||
<string name="import_files">Import/Encrypt files</string>
|
||||
<string name="import_folder">Import/Encrypt folder</string>
|
||||
<string name="discovering_files">Discovering files…</string>
|
||||
@ -31,8 +26,6 @@
|
||||
<string name="remove_failed">Deletion of %s failed.</string>
|
||||
<string name="passwords_mismatch">Passwords don\'t match</string>
|
||||
<string name="dir_not_empty">The selected directory isn\'t empty</string>
|
||||
<string name="success_volume_create">Volume successfully created !</string>
|
||||
<string name="success_volume_create_msg">The volume has been successfully created.</string>
|
||||
<string name="create_volume_failed">The volume creation has failed.</string>
|
||||
<string name="open_volume_failed">Open failed</string>
|
||||
<string name="open_volume_failed_msg">Failed to open the volume. Please check your password.</string>
|
||||
@ -57,18 +50,10 @@
|
||||
<string name="ask_for_wipe">Do you want to wipe the original files ?</string>
|
||||
<string name="wipe_failed">Wiping failed: %s</string>
|
||||
<string name="wipe_successful">Files successfully wiped !</string>
|
||||
<string name="create_password_warning">Warning !\nThis password will be the only way to decrypt the volume and access the files inside.\nChoose a very strong password (not \"123456\" or \"password\"), do not lose it and keep it secure (preferably only in your mind).\n\nDroidFS cannot protect you from screen recording apps, keyloggers, apk backdooring, compromised root accesses, memory dumps etc.\nDo not type passwords in insecure environments.</string>
|
||||
<string name="open_activity_warning">Warning !\nOpening volumes in insecure environments can lead to data leaks.\nDroidFS cannot protect you from screen recording apps, keyloggers, apk backdooring, compromised root accesses, memory dumps etc.\nDo not open volumes containing sensitive data unless you know exactly what you are doing.</string>
|
||||
<string name="rename">Rename</string>
|
||||
<string name="rename_title">New name:</string>
|
||||
<string name="rename_failed">Failed to rename %s</string>
|
||||
<string name="remember_volume_path">Remember volume path</string>
|
||||
<string name="sort_order">Sort order:</string>
|
||||
<string name="old_password">Old password:</string>
|
||||
<string name="new_password">New password:</string>
|
||||
<string name="new_password_confirmation">New Password (confirmation):</string>
|
||||
<string name="success_change_password">Password successfully changed !</string>
|
||||
<string name="success_change_password_msg">The volume\'s password has been successfully changed.</string>
|
||||
<string name="change_password_failed">Operation failed. Please check your old password.</string>
|
||||
<string name="share_menu_label">Encrypt with DroidFS</string>
|
||||
<string name="share_intent_parsing_failed">Failed to handle the share request.</string>
|
||||
@ -76,15 +61,6 @@
|
||||
<string name="fingerprint_save_checkbox_text">Save password hash using fingerprint</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="authentication_failed">Authentication failed</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>
|
||||
@ -92,7 +68,6 @@
|
||||
<string name="hash_storage_reset">Hash storage successfully reset</string>
|
||||
<string name="encrypt_action_description">Encrypting and saving password hash.</string>
|
||||
<string name="decrypt_action_description">Decrypting password hash.</string>
|
||||
<string name="hash_saved_hint">Saved password hash</string>
|
||||
<string name="title_activity_settings">DroidFS Settings</string>
|
||||
<string name="explorer">Explorer</string>
|
||||
<string name="settings_title_sort_order">Default sort order</string>
|
||||
@ -140,7 +115,7 @@
|
||||
<string name="decrypt_files">Export/Decrypt</string>
|
||||
<string name="copy_failed">Copy of %s failed.</string>
|
||||
<string name="copy_success">Copy successful !</string>
|
||||
<string name="fab_dialog_title">Add</string>
|
||||
<string name="add">Add</string>
|
||||
<string name="camera">Camera</string>
|
||||
<string name="picture_save_success">Picture saved to %s</string>
|
||||
<string name="picture_save_failed">Failed to save this picture.</string>
|
||||
@ -155,12 +130,9 @@
|
||||
<string name="timer_empty_error_msg">Please enter a numeric value</string>
|
||||
<string name="path_from_uri_null_error_msg">Failed to retrieve the selected path.</string>
|
||||
<string name="create_cant_write_error_msg">DroidFS doesn\'t have write access to this path. Please try another location.</string>
|
||||
<string name="open_read_only">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="add_read_only">Adding volume with read-only access.</string>
|
||||
<string name="add_cant_write_warning">DroidFS doesn\'t have write access to this path. Adding volume with read-only access.</string>
|
||||
<string name="sdcard_error_header">DroidFS can only write to removable SD cards under:</string>
|
||||
<string name="sdcard_error_footer">Please use a subdirectory of this path or internal storage.</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="slideshow_stopped">Slideshow stopped</string>
|
||||
<string name="slideshow_started">Slideshow started</string>
|
||||
<string name="ask_save_img_rotated">The image has been rotated. Do you want to save these changes and overwrite the original image ?</string>
|
||||
@ -175,9 +147,9 @@
|
||||
<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="hidden_volume">Hidden volume</string>
|
||||
<string name="error_slash_in_name">Volume name cannot contain slashes</string>
|
||||
<string name="hidden_volume_warning">Hidden volumes are stored in the app\'s internal storage. Other apps can\'t see these volumes without root access. However, if you uninstall DroidFS or clear data of the app, all your hidden volumes will be LOST. Be sure to make backups !</string>
|
||||
<string name="hidden_volume_warning">Hidden volumes are stored in the app\'s internal storage. Other apps can\'t see these volumes without root access. However, if you uninstall DroidFS or clear the app\'s data, all your hidden volumes will be LOST. Be sure to make backups !</string>
|
||||
<string name="camera_perm_needed">Camera permission is needed to take photo.</string>
|
||||
<string name="choose_resolution">Choose a resolution</string>
|
||||
<string name="file_operations">File Operations</string>
|
||||
@ -210,4 +182,44 @@
|
||||
<string name="thumbnails_summary">Show images and videos thumbnails</string>
|
||||
<string name="seek_seconds_forward">+%d seconds</string>
|
||||
<string name="seek_seconds_backward">-%d seconds</string>
|
||||
<string name="add_volume">Add volume</string>
|
||||
<string name="pick_directory">Pick directory</string>
|
||||
<string name="volume_alread_saved">Volume already saved</string>
|
||||
<string name="open_dialog_title">Enter your password:</string>
|
||||
<string name="remove">Remove</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="select_all">Select All</string>
|
||||
<string name="forget_password">Forget password</string>
|
||||
<string name="unrecoverable_key_exception_msg">%s. Can\'t load encryption key.</string>
|
||||
<string name="unrecoverable_key_exception">UnrecoverableKeyException</string>
|
||||
<string name="delete_hidden_volume_question">%s is hidden, do you just want to forget the path of the volume or also DELETE all its CONTENT?</string>
|
||||
<string name="forget_only">Forget only</string>
|
||||
<string name="delete_volume">Delete volume</string>
|
||||
<string name="hidden_volume_description">Store the volume in the DroidFS internal storage</string>
|
||||
<string name="error_is_file">Error: file exists</string>
|
||||
<string name="volume_path_label">Select the path to the volume:</string>
|
||||
<string name="volume_name_label">Enter the name of the volume:</string>
|
||||
<string name="volume_path_hint">Volume path</string>
|
||||
<string name="volume_name_hint">Volume name</string>
|
||||
<string name="password_label">Enter the volume password:</string>
|
||||
<string name="password_confirmation_label">Repeat the password:</string>
|
||||
<string name="password_confirmation_hint">Password (confirmation)</string>
|
||||
<string name="password_hash_saved">password hash saved</string>
|
||||
<string name="read_only">read-only</string>
|
||||
<string name="no_volumes_text">No volume saved, add some by clicking on the + button</string>
|
||||
<string name="fingerprint_error_msg">Fingerprint authentication can\'t be used: %s.</string>
|
||||
<string name="keyguard_not_secure">keyguard not secure</string>
|
||||
<string name="no_hardware">no suitable hardware found</string>
|
||||
<string name="hardware_unavailable">hardware unavailable</string>
|
||||
<string name="no_fingerprint">no enrolled fingerprint</string>
|
||||
<string name="unknown_error">unknown error</string>
|
||||
<string name="biometric_error">Biometric error: %s</string>
|
||||
<string name="apply_to_all">Apply this choice to all hidden volumes</string>
|
||||
<string name="select_volume">Select volume</string>
|
||||
<string name="current_password_label">Enter the current volume password:</string>
|
||||
<string name="current_password_hint">Current password</string>
|
||||
<string name="new_password_label">Enter the new volume password:</string>
|
||||
<string name="new_password_hint">New password</string>
|
||||
<string name="new_password_confirmation_label">Repeat the new password:</string>
|
||||
<string name="error_marshmallow_required">This feature is only available on Android 6.0 (Marshmallow) or above.</string>
|
||||
</resources>
|
||||
|
@ -1,7 +1,8 @@
|
||||
<resources>
|
||||
<style name="BaseTheme" parent="Theme.AppCompat.NoActionBar">
|
||||
<style name="BaseTheme" parent="Theme.AppCompat">
|
||||
<item name="android:textColor">@color/textColor</item>
|
||||
<item name="colorAccent">@color/secondary</item>
|
||||
<item name="colorPrimary">@color/primary</item>
|
||||
<item name="android:statusBarColor">@color/primary</item>
|
||||
<item name="infoBarBackgroundColor">#111111</item>
|
||||
<item name="buttonBackgroundColor">#5B5A5C</item>
|
||||
|
@ -6,7 +6,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.1.1'
|
||||
classpath 'com.android.tools.build:gradle:7.1.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user