New home UI

This commit is contained in:
Matéo Duparc 2022-03-05 12:51:02 +01:00
parent 842667cdee
commit 71a314b0a0
Signed by: hardcoresushi
GPG Key ID: AFE384344A45E13A
76 changed files with 2223 additions and 2009 deletions

View File

@ -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

View File

@ -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"/>

View File

@ -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)

View File

@ -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")!!

View File

@ -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)
}
}

View File

@ -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")),

View File

@ -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)
}
}

View 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()
}
}
}
}

View File

@ -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

View File

@ -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() }
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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()
}
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,6 @@
package sushi.hardcore.droidfs.add_volume
enum class Action {
ADD,
CREATE,
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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()
}

View File

@ -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()

View File

@ -14,6 +14,7 @@ abstract class MediaPlayer: FileViewerActivity() {
private lateinit var player: ExoPlayer
override fun viewFile() {
supportActionBar?.hide()
initializePlayer()
}

View File

@ -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)
}
}

View File

@ -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 -> {

View File

@ -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
}
}

View File

@ -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))
}
}
}

View File

@ -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()
}
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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>

View 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>

View 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>

View File

@ -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

View 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" />

View 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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -4,8 +4,6 @@
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/toolbar"/>
<include layout="@layout/explorer_info_bar"/>
<RelativeLayout

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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"/>

View File

@ -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"/>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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

View 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>

View File

@ -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>

View File

@ -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"

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"
}
}