diff --git a/app/build.gradle b/app/build.gradle index cf25ab0..e5b6601 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/libgocryptfs b/app/libgocryptfs index 1da2407..89966b1 160000 --- a/app/libgocryptfs +++ b/app/libgocryptfs @@ -1 +1 @@ -Subproject commit 1da2407a614f17a3c64d14ee34fb41e081db9a71 +Subproject commit 89966b1aaef93ac842f2240451ebfd50dd2bbce9 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f820d2c..134fe49 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,61 +31,30 @@ android:requestLegacyExternalStorage="true" android:supportsRtl="true" android:theme="@style/BaseTheme"> - - - - - - + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/app/src/main/java/sushi/hardcore/droidfs/BaseActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/BaseActivity.kt index 082014f..b5c2fed 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/BaseActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/BaseActivity.kt @@ -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) diff --git a/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt index af2b140..56fa4ef 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt @@ -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")!! diff --git a/app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt index 407f624..ceed968 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt @@ -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) - } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt b/app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt index a1d2291..51b87d1 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt @@ -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")), diff --git a/app/src/main/java/sushi/hardcore/droidfs/CreateActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/CreateActivity.kt deleted file mode 100644 index 0fe685f..0000000 --- a/app/src/main/java/sushi/hardcore/droidfs/CreateActivity.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/main/java/sushi/hardcore/droidfs/FingerprintProtector.kt b/app/src/main/java/sushi/hardcore/droidfs/FingerprintProtector.kt new file mode 100644 index 0000000..1665471 --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/FingerprintProtector.kt @@ -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() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/GocryptfsVolume.kt b/app/src/main/java/sushi/hardcore/droidfs/GocryptfsVolume.kt index 2f310ad..d5ea409 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/GocryptfsVolume.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/GocryptfsVolume.kt @@ -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 diff --git a/app/src/main/java/sushi/hardcore/droidfs/LoadingTask.kt b/app/src/main/java/sushi/hardcore/droidfs/LoadingTask.kt index a8ca8da..157022e 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/LoadingTask.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/LoadingTask.kt @@ -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() } - } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt index 5ce7025..ecbb5ce 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt @@ -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, 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 startActivity(clazz: Class) { - 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) + } + } + } } } diff --git a/app/src/main/java/sushi/hardcore/droidfs/OpenActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/OpenActivity.kt deleted file mode 100644 index 889deda..0000000 --- a/app/src/main/java/sushi/hardcore/droidfs/OpenActivity.kt +++ /dev/null @@ -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(R.id.path).text = PathUtils.getPackageDataFolder(this@OpenActivity) - findViewById(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) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt index 4a8cbf8..f932c87 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt @@ -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("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 + } + } } } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/Volume.kt b/app/src/main/java/sushi/hardcore/droidfs/Volume.kt index d829e35..a6739c8 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/Volume.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/Volume.kt @@ -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) \ No newline at end of file +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 { + override fun createFromParcel(parcel: Parcel): Volume { + return Volume(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/VolumeActionActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/VolumeActionActivity.kt deleted file mode 100644 index 54c792c..0000000 --- a/app/src/main/java/sushi/hardcore/droidfs/VolumeActionActivity.kt +++ /dev/null @@ -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(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, 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(R.id.path).text = PathUtils.getPackageDataFolder(this@VolumeActionActivity) - } - ) - } else { - dialog.setMessage(errorMsg) - } - dialog.show() - } -} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/VolumeDatabase.kt b/app/src/main/java/sushi/hardcore/droidfs/VolumeDatabase.kt index 7976ec5..ca2a59a 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/VolumeDatabase.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/VolumeDatabase.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/adapters/SavedVolumesAdapter.kt b/app/src/main/java/sushi/hardcore/droidfs/adapters/SavedVolumesAdapter.kt deleted file mode 100644 index f348ea5..0000000 --- a/app/src/main/java/sushi/hardcore/droidfs/adapters/SavedVolumesAdapter.kt +++ /dev/null @@ -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(R.id.volume_name_textview) - val currentVolume = getItem(position) - volumeNameTextView.text = currentVolume.name - val deleteImageView = view.findViewById(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() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/adapters/VolumeAdapter.kt b/app/src/main/java/sushi/hardcore/droidfs/adapters/VolumeAdapter.kt new file mode 100644 index 0000000..b27acbd --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/adapters/VolumeAdapter.kt @@ -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() { + private val inflater: LayoutInflater = LayoutInflater.from(context) + lateinit var volumes: List + val selectedItems: MutableSet = 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(R.id.text_volume_name).text = volume.shortName + itemView.findViewById(R.id.image_icon).setImageResource(R.drawable.icon_volume) + itemView.findViewById(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(R.id.text_info).apply { + if (infoString == null) + visibility = View.GONE + else { + text = infoString + visibility = View.VISIBLE + } + } + (bindingAdapter as VolumeAdapter?)?.let { adapter -> + itemView.findViewById(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 + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/add_volume/Action.kt b/app/src/main/java/sushi/hardcore/droidfs/add_volume/Action.kt new file mode 100644 index 0000000..7d0e93e --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/add_volume/Action.kt @@ -0,0 +1,6 @@ +package sushi.hardcore.droidfs.add_volume + +enum class Action { + ADD, + CREATE, +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/add_volume/AddVolumeActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/add_volume/AddVolumeActivity.kt new file mode 100644 index 0000000..e8bf3ff --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/add_volume/AddVolumeActivity.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/add_volume/CreateVolumeFragment.kt b/app/src/main/java/sushi/hardcore/droidfs/add_volume/CreateVolumeFragment.kt new file mode 100644 index 0000000..a47b1da --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/add_volume/CreateVolumeFragment.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/add_volume/SelectPathFragment.kt b/app/src/main/java/sushi/hardcore/droidfs/add_volume/SelectPathFragment.kt new file mode 100644 index 0000000..022245a --- /dev/null +++ b/app/src/main/java/sushi/hardcore/droidfs/add_volume/SelectPathFragment.kt @@ -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(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) + } +} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt index 66cd168..3015adf 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt @@ -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) { diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt index a9cd235..99747f6 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt @@ -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() } diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/ImageViewer.kt b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/ImageViewer.kt index 4462292..454cd48 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/ImageViewer.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/ImageViewer.kt @@ -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() diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/MediaPlayer.kt b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/MediaPlayer.kt index 5d5d974..403b89f 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/MediaPlayer.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/MediaPlayer.kt @@ -14,6 +14,7 @@ abstract class MediaPlayer: FileViewerActivity() { private lateinit var player: ExoPlayer override fun viewFile() { + supportActionBar?.hide() initializePlayer() } diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/PdfViewer.kt b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/PdfViewer.kt index 02ba9d8..c7cd0f1 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/PdfViewer.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/PdfViewer.kt @@ -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(R.id.toolbar) - setSupportActionBar(toolbar) - title = "" - val titleText = toolbar.findViewById(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) } } diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/TextEditor.kt b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/TextEditor.kt index 7380d76..8085923 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/file_viewers/TextEditor.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/file_viewers/TextEditor.kt @@ -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 -> { diff --git a/app/src/main/java/sushi/hardcore/droidfs/util/WidgetUtil.kt b/app/src/main/java/sushi/hardcore/droidfs/util/WidgetUtil.kt deleted file mode 100644 index fff01ff..0000000 --- a/app/src/main/java/sushi/hardcore/droidfs/util/WidgetUtil.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/util/Wiper.kt b/app/src/main/java/sushi/hardcore/droidfs/util/Wiper.kt index 5475d44..1fff7a8 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/util/Wiper.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/util/Wiper.kt @@ -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)) - } - } } \ No newline at end of file diff --git a/app/src/main/java/sushi/hardcore/droidfs/widgets/NonScrollableColoredBorderListView.kt b/app/src/main/java/sushi/hardcore/droidfs/widgets/NonScrollableColoredBorderListView.kt deleted file mode 100644 index 2f720ae..0000000 --- a/app/src/main/java/sushi/hardcore/droidfs/widgets/NonScrollableColoredBorderListView.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/app/src/main/native/gocryptfs_jni.c b/app/src/main/native/gocryptfs_jni.c index 566f16d..6258bfb 100644 --- a/app/src/main/native/gocryptfs_jni.c +++ b/app/src/main/native/gocryptfs_jni.c @@ -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; } diff --git a/app/src/main/res/drawable/icon_arrow_back.xml b/app/src/main/res/drawable/icon_arrow_back.xml deleted file mode 100644 index fa122e1..0000000 --- a/app/src/main/res/drawable/icon_arrow_back.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/icon_delete.xml b/app/src/main/res/drawable/icon_delete.xml index a48a72d..07d6e8b 100644 --- a/app/src/main/res/drawable/icon_delete.xml +++ b/app/src/main/res/drawable/icon_delete.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/icon_hidden.xml b/app/src/main/res/drawable/icon_hidden.xml new file mode 100644 index 0000000..f0f78aa --- /dev/null +++ b/app/src/main/res/drawable/icon_hidden.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/icon_volume.xml b/app/src/main/res/drawable/icon_volume.xml new file mode 100644 index 0000000..da8c434 --- /dev/null +++ b/app/src/main/res/drawable/icon_volume.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/listview_border.xml b/app/src/main/res/drawable/listview_border.xml deleted file mode 100644 index b0366c6..0000000 --- a/app/src/main/res/drawable/listview_border.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/logo.png b/app/src/main/res/drawable/logo.png deleted file mode 100644 index 426babc..0000000 Binary files a/app/src/main/res/drawable/logo.png and /dev/null differ diff --git a/app/src/main/res/layout/action_bar.xml b/app/src/main/res/layout/action_bar.xml new file mode 100644 index 0000000..a942cb2 --- /dev/null +++ b/app/src/main/res/layout/action_bar.xml @@ -0,0 +1,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_add_volume.xml b/app/src/main/res/layout/activity_add_volume.xml new file mode 100644 index 0000000..d8791e1 --- /dev/null +++ b/app/src/main/res/layout/activity_add_volume.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_audio_player.xml b/app/src/main/res/layout/activity_audio_player.xml index b84fd07..0983c7c 100644 --- a/app/src/main/res/layout/activity_audio_player.xml +++ b/app/src/main/res/layout/activity_audio_player.xml @@ -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"/> - + android:layout_height="match_parent" + android:fillViewport="true"> - - - + 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"> - + android:layout_marginBottom="@dimen/volume_operation_vertical_gap" + android:gravity="center" + android:textSize="@dimen/title_text_size" + android:textStyle="bold" /> - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_create.xml b/app/src/main/res/layout/activity_create.xml deleted file mode 100644 index 9d0d3de..0000000 --- a/app/src/main/res/layout/activity_create.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_explorer.xml b/app/src/main/res/layout/activity_explorer.xml index d005fe9..3e1eeb6 100644 --- a/app/src/main/res/layout/activity_explorer.xml +++ b/app/src/main/res/layout/activity_explorer.xml @@ -6,8 +6,6 @@ android:orientation="vertical" tools:context="sushi.hardcore.droidfs.explorers.ExplorerActivity"> - - - - - - - - - - + android:layout_height="match_parent" + android:gravity="center" + android:textSize="25sp" + android:layout_marginHorizontal="20dp" + android:text="@string/no_volumes_text" + android:visibility="gone"/> - + android:layout_height="match_parent"/> - + - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_open.xml b/app/src/main/res/layout/activity_open.xml deleted file mode 100644 index b5a1c22..0000000 --- a/app/src/main/res/layout/activity_open.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_pdf_viewer.xml b/app/src/main/res/layout/activity_pdf_viewer.xml index 221bbda..0c1c3ff 100644 --- a/app/src/main/res/layout/activity_pdf_viewer.xml +++ b/app/src/main/res/layout/activity_pdf_viewer.xml @@ -1,14 +1,4 @@ - - - - - - - \ No newline at end of file + android:layout_height="match_parent"/> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 13ad7fe..eff433c 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -1,14 +1,4 @@ - - - - - - \ No newline at end of file + android:layout_height="match_parent"/> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_text_editor.xml b/app/src/main/res/layout/activity_text_editor.xml index 752f8b3..bb5e935 100644 --- a/app/src/main/res/layout/activity_text_editor.xml +++ b/app/src/main/res/layout/activity_text_editor.xml @@ -1,32 +1,24 @@ - + android:fillViewport="true"> - - - - + android:layout_height="wrap_content" + android:gravity="top" + android:textSize="15sp" + android:background="@null" + android:padding="5dp" + android:importantForAutofill="no"/> - + - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_text_editor_wrap.xml b/app/src/main/res/layout/activity_text_editor_wrap.xml index 9822ddf..6bbc724 100644 --- a/app/src/main/res/layout/activity_text_editor_wrap.xml +++ b/app/src/main/res/layout/activity_text_editor_wrap.xml @@ -1,25 +1,17 @@ - + android:fillViewport="true"> - + - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_explorer_element.xml b/app/src/main/res/layout/adapter_explorer_element.xml index a9ca38b..5d4a796 100644 --- a/app/src/main/res/layout/adapter_explorer_element.xml +++ b/app/src/main/res/layout/adapter_explorer_element.xml @@ -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"/> + android:textSize="@dimen/details_text_size"/> diff --git a/app/src/main/res/layout/adapter_saved_volume.xml b/app/src/main/res/layout/adapter_saved_volume.xml deleted file mode 100644 index 38f8261..0000000 --- a/app/src/main/res/layout/adapter_saved_volume.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_volume.xml b/app/src/main/res/layout/adapter_volume.xml new file mode 100644 index 0000000..f909a1a --- /dev/null +++ b/app/src/main/res/layout/adapter_volume.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/checkboxes_section.xml b/app/src/main/res/layout/checkboxes_section.xml deleted file mode 100644 index ecb8b10..0000000 --- a/app/src/main/res/layout/checkboxes_section.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_delete_volume.xml b/app/src/main/res/layout/dialog_delete_volume.xml new file mode 100644 index 0000000..02f5637 --- /dev/null +++ b/app/src/main/res/layout/dialog_delete_volume.xml @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_edit_text.xml b/app/src/main/res/layout/dialog_edit_text.xml index c951a74..558ef0f 100644 --- a/app/src/main/res/layout/dialog_edit_text.xml +++ b/app/src/main/res/layout/dialog_edit_text.xml @@ -2,14 +2,14 @@ + android:layout_height="match_parent" + android:paddingHorizontal="@dimen/dialog_horizontal_padding"> diff --git a/app/src/main/res/layout/dialog_loading.xml b/app/src/main/res/layout/dialog_loading.xml index a149d49..1fda55f 100644 --- a/app/src/main/res/layout/dialog_loading.xml +++ b/app/src/main/res/layout/dialog_loading.xml @@ -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"> + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_sdcard_error.xml b/app/src/main/res/layout/dialog_sdcard_error.xml index a04214f..b0164cd 100644 --- a/app/src/main/res/layout/dialog_sdcard_error.xml +++ b/app/src/main/res/layout/dialog_sdcard_error.xml @@ -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"> + android:textSize="@dimen/dialog_text_size"/> + android:text="@string/add_read_only" + android:textSize="@dimen/dialog_text_size"/> \ No newline at end of file diff --git a/app/src/main/res/layout/explorer_content.xml b/app/src/main/res/layout/explorer_content.xml index 4f256e5..babfb5e 100644 --- a/app/src/main/res/layout/explorer_content.xml +++ b/app/src/main/res/layout/explorer_content.xml @@ -8,7 +8,7 @@ android:gravity="center" android:textSize="30sp" android:text="@string/dir_empty" - android:visibility="invisible"/> + android:visibility="gone"/> + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_select_path.xml b/app/src/main/res/layout/fragment_select_path.xml new file mode 100644 index 0000000..ffef32d --- /dev/null +++ b/app/src/main/res/layout/fragment_select_path.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/toolbar.xml b/app/src/main/res/layout/toolbar.xml deleted file mode 100644 index 1503941..0000000 --- a/app/src/main/res/layout/toolbar.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/volume_path_section.xml b/app/src/main/res/layout/volume_path_section.xml deleted file mode 100644 index 5709c51..0000000 --- a/app/src/main/res/layout/volume_path_section.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/main_activity.xml b/app/src/main/res/menu/main_activity.xml index 8d845b4..7de86d7 100644 --- a/app/src/main/res/menu/main_activity.xml +++ b/app/src/main/res/menu/main_activity.xml @@ -1,8 +1,37 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 63ff2f8..3649ed0 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1,15 +1,10 @@ DroidFS - Abrir um volume Criar um volume - Mudar a senha do volume Abrir Criar Mudar a senha - Senha: - Senha (confirmação): - Localização do volume: - Nome do volume: + Senha Importar/Criptografar arquivos Importar/Criptografar pasta Descobrindo arquivos… @@ -31,8 +26,6 @@ Eliminação de %s falhou. As senhas não correspondem A pasta selecionada não está vazia - Volume criado com sucesso! - O volume foi criado com sucesso. Falha ao criar o volume. Não foi possível abrir Falha ao abrir o volume. Por favor, verifique sua senha. @@ -57,18 +50,10 @@ Você quer limpar os arquivos originais? Não foi possível limpar : %s Operação bem sucedida! - 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. - 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. Renomear Novo nome: Falha ao renomear %s - Lembrar a localização do volume Tipo de classificação: - Senha antiga: - Nova senha: - Nova senha (confirmação): - Senha alterada com sucesso! - A senha do volume foi alterada com sucesso. A operação falhou. Por favor, verifique sua senha antiga. Criptografar via DroidFS Falha ao processar o intento de compartilhamento. @@ -76,15 +61,6 @@ Salvar o hash da senha usando impressão digital Por favor, toque no sensor da impressão digital Falha ao abrir o volume. A senha poderia ter mudado. - A autenticação falhou - Excluir apenas o hash da senha ou o hash da senha com a localização salva? (Isto não excluirá o próprio volume) - Hash e local da senha - Hash da senha - Tem certeza que quer remover o local de volume? (Isto não excluirá o próprio volume) - Excluir somente o hash da senha, o hash da senha com a localização salva ou o volume COMPLETO (incluindo tudo conteúdo)? - Volume completo - Excluir somente o local ou o volume COMPLETO (incluindo tudo conteúdo)? - Somente a localização IllegalBlockSizeException Isto poderia ter acontecido, se você tiver adicionado uma nova impressão digital. Tente resetar o hash do armazenamento para resolver este problema. Resetar o hash do armazenamento @@ -92,7 +68,6 @@ O hash do armazenamento foi resetado com sucesso Criptografando e salvando o hash da senha. Descriptografando o hash da senha. - O hash da senha salvo Configurações do DroidFS Navegador Padrão de classificação @@ -140,7 +115,7 @@ Exportar/Descriptografar Falha ao copiar o %s . Cópia feita! - Adicionar + Adicionar Câmera Foto salva em %s Falha ao salvar esta imagem. @@ -154,13 +129,8 @@ Definir a duração do tempo (em s) Por favor, digite um valor numérico Falha ao carregar a localização selecionada. - Abrindo o volume com permissões somente de leitura. DroidFS não pode salvar neste local. Por favor, tente algum outro. - DroidFS não tem permissão para salvar neste local. Abrindo o volume apenas com acesso de leitura. - DroidFS não tem permissão para salvar neste local. Por favor, tente outro volume. DroidFS só pode escrever em memórias SD removíveis sob: - Por favor, use um subdiretório desta localização ou armazenamento interno. - DroidFS não tem permissão para salvar neste local. Pode tentar mover o volume para um local com permissão de escrita. Apresentação parou Apresentação começou A imagem foi alternada. Você deseja salvar estas mudanças e substituir a imagem original? @@ -210,4 +180,6 @@ Mostrar imagens e vídeos em miniatura +%d segundos -%d segundos + Senha (confirmação) + Nova senha diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 316bbe7..e21185a 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,14 +1,9 @@ - Открыть том Создать том - Изменить пароль тома Открыть Создать Изменить пароль - Пароль: - Повтор пароля: - Путь тома: - Имя тома: + Пароль Импорт/шифрование файлов Импорт/шифрование папки Обнаружение файлов… @@ -29,8 +24,6 @@ Ошибка при удалении %s. Пароли не совпадают Выбранная папка не пустая - Том создан! - Том успешно создан. Невозможно создать том. Ошибка открытия Невозможно открыть том. Проверьте пароль. @@ -55,18 +48,10 @@ Уничтожить исходные файлы? Ошибка при уничтожении: %s Файлы уничтожены! - Предупреждение!\nЭтот пароль будет единственным способом расшифровать том и получить доступ к файлам внутри.\nВыберите надёжный пароль (не \"123456\" или \"password\"), не потеряйте его и храните в безопасности (желательно только в своей голове).\n\nDroidFS не может защитить вас от приложений записи экрана, клавиатурных шпионов, скрытых недокументированных функций приложений, скомпрометированных прав root-доступа, дампов памяти и т.д.\nНе вводите пароль в небезопасных средах. - Предупреждение!\nОткрытие томов в небезопасных средах может привести к утечке данных.\nDroidFS не может защитить вас от приложений записи экрана, клавиатурных шпионов, скрытых недокументированных функций приложений, скомпрометированных прав root-доступа, дампов памяти и т.д.\nНе открывайте тома, содержащие конфиденциальные данные, если не уверены в своих действиях. Переименовать Новое имя: Ошибка при переименовании %s. - Запомнить путь тома Порядок сортировки: - Прежний пароль: - Новый пароль: - Повтор пароля: - Пароль изменён! - Пароль тома успешно изменён. Операция не выполнена. Проверьте прежний пароль. Зашифровать в DroidFS Невозможно обработать запрос на совместное использование. @@ -74,22 +59,12 @@ Сохранить хеш пароля отпечатком пальца Прикоснитесь к сканеру отпечатков пальцев Невозможно открыть том. Возможно, изменился пароль. - Ошибка аутентификации - Удалить только хеш пароля или хеш пароля и сохранённый путь? (Сам том не будет удалён.) - Хеш пароля и путь - Хеш пароля - Забыть путь к тому? (Сам том не будет удалён.) - Удалить только хеш пароля, хеш пароля и сохранённый путь или ВЕСЬ том (включая его содержимое)? - Весь том - Удалить только путь или ВЕСЬ том (включая его содержимое)? - Только путь Это могло произойти, если вы добавили новый отпечаток пальца. Сброс хранилища хешей должен решить эту проблему. Сброс хранилища хешей Проверка подписи/MAC не выполнена. Хранилище ключей Android или сохранённый хеш был изменён. Сброс хранилища хешей должен решить эту проблему. Хранилище хешей успешно сброшено Шифрование и сохранение хеша пароля Расшифровка хеша пароля - Хеш сохранённого пароля Настройки DroidFS Проводник Порядок сортировки @@ -135,7 +110,7 @@ Экспорт/расшифровка Ошибка при копировании %s. Копирование выполнено! - Добавить + Добавить Камера Изображение сохранено в %s Невозможно сохранить изображение. @@ -150,12 +125,7 @@ Введите числовое значение. Невозможно получить выбранный путь. DroidFS не имеет доступа на запись по этому пути. Попробуйте найти другое место. - Открытие тома с доступом только для чтения. - DroidFS не имеет доступа на запись по этому пути. Том открывается только для чтения. - DroidFS не имеет доступа на запись по этому пути. Попробуйте другой том. DroidFS может записывать на SD-карты только в: - Используйте вложенную папку этого пути или внутреннее хранилище. - DroidFS не имеет доступа на запись по этому пути. Попробуйте переместить том в место, доступное для записи. Слайдшоу остановлено Слайдшоу началось Изображение было повёрнуто. Сохранить изменения и перезаписать исходное изображение? @@ -204,4 +174,6 @@ Показывать эскизы изображений и видео +%d сек. -%d сек. + Повтор пароля + Новый пароль diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 937382a..5b8e4cf 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -12,6 +12,7 @@ @color/white @color/black @color/white + #AAAAAA #66666666 #18FFFFFF #20EEEEEE diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 8cd7df4..0e7c033 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,17 +1,14 @@ - 60dp - 50dp - 100dp - 15sp - 90dp - 100dp - 60dp - 50dp - 50dp - 20dp - 20dp - 20dp - 18sp + 18sp + @dimen/title_text_size 40dp + 20dp + 20dp + 40dp + 90dp + 10sp + 30dp + 10dp + 16sp \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fe35ec9..a9b8bd2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,15 +1,10 @@ DroidFS - Open a volume - Create a volume - Change a volume\'s password + Create volume Open Create Change password - Password: - Password (confirmation): - Volume Path: - Volume Name: + Password Import/Encrypt files Import/Encrypt folder Discovering files… @@ -31,8 +26,6 @@ Deletion of %s failed. Passwords don\'t match The selected directory isn\'t empty - Volume successfully created ! - The volume has been successfully created. The volume creation has failed. Open failed Failed to open the volume. Please check your password. @@ -57,18 +50,10 @@ Do you want to wipe the original files ? Wiping failed: %s Files successfully wiped ! - 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. - 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. Rename New name: Failed to rename %s - Remember volume path Sort order: - Old password: - New password: - New Password (confirmation): - Password successfully changed ! - The volume\'s password has been successfully changed. Operation failed. Please check your old password. Encrypt with DroidFS Failed to handle the share request. @@ -76,15 +61,6 @@ Save password hash using fingerprint Please touch the fingerprint sensor Failed to open the volume. The password may have changed. - Authentication failed - Delete only the password hash or the password hash and the saved path ? (This won\'t delete the volume itself) - Password hash and path - Password hash - Are you sure you want to forget this volume path ? (This won\'t delete the volume itself) - Delete only the password hash, the password hash and the saved path, or the WHOLE volume (including its content) ? - Whole volume - Delete only the path or the whole volume ITSELF (including its content) ? - Path only IllegalBlockSizeException This can happen if you have added a new fingerprint. Resetting hash storage can solve this problem. Reset hash storage @@ -92,7 +68,6 @@ Hash storage successfully reset Encrypting and saving password hash. Decrypting password hash. - Saved password hash DroidFS Settings Explorer Default sort order @@ -140,7 +115,7 @@ Export/Decrypt Copy of %s failed. Copy successful ! - Add + Add Camera Picture saved to %s Failed to save this picture. @@ -155,12 +130,9 @@ Please enter a numeric value Failed to retrieve the selected path. DroidFS doesn\'t have write access to this path. Please try another location. - Opening volume with read-only access. - DroidFS doesn\'t have write access to this path. Opening volume with read-only access. - DroidFS doesn\'t have write access to this path. Please try another volume. + Adding volume with read-only access. + DroidFS doesn\'t have write access to this path. Adding volume with read-only access. DroidFS can only write to removable SD cards under: - Please use a subdirectory of this path or internal storage. - DroidFS doesn\'t have write access to this path. You can try to move the volume to a writable location. Slideshow stopped Slideshow started The image has been rotated. Do you want to save these changes and overwrite the original image ? @@ -175,9 +147,9 @@ You should read it carefully before enabling any of these options. Unsafe features documentation Unable to retrieve file name for URI: %s - Hidden Volume + Hidden volume Volume name cannot contain slashes - 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 ! + 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 ! Camera permission is needed to take photo. Choose a resolution File Operations @@ -210,4 +182,44 @@ Show images and videos thumbnails +%d seconds -%d seconds + Add volume + Pick directory + Volume already saved + Enter your password: + Remove + Settings + Select All + Forget password + %s. Can\'t load encryption key. + UnrecoverableKeyException + %s is hidden, do you just want to forget the path of the volume or also DELETE all its CONTENT? + Forget only + Delete volume + Store the volume in the DroidFS internal storage + Error: file exists + Select the path to the volume: + Enter the name of the volume: + Volume path + Volume name + Enter the volume password: + Repeat the password: + Password (confirmation) + password hash saved + read-only + No volume saved, add some by clicking on the + button + Fingerprint authentication can\'t be used: %s. + keyguard not secure + no suitable hardware found + hardware unavailable + no enrolled fingerprint + unknown error + Biometric error: %s + Apply this choice to all hidden volumes + Select volume + Enter the current volume password: + Current password + Enter the new volume password: + New password + Repeat the new password: + This feature is only available on Android 6.0 (Marshmallow) or above. diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 02f152b..66cf311 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,7 +1,8 @@ -