New home UI

cryfs
Hardcore Sushi 9 months ago
parent 842667cdee
commit 71a314b0a0
Signed by: hardcoresushi
GPG Key ID: AFE384344A45E13A
  1. 6
      app/build.gradle
  2. 2
      app/libgocryptfs
  3. 65
      app/src/main/AndroidManifest.xml
  4. 6
      app/src/main/java/sushi/hardcore/droidfs/BaseActivity.kt
  5. 1
      app/src/main/java/sushi/hardcore/droidfs/CameraActivity.kt
  6. 270
      app/src/main/java/sushi/hardcore/droidfs/ChangePasswordActivity.kt
  7. 1
      app/src/main/java/sushi/hardcore/droidfs/ConstValues.kt
  8. 180
      app/src/main/java/sushi/hardcore/droidfs/CreateActivity.kt
  9. 259
      app/src/main/java/sushi/hardcore/droidfs/FingerprintProtector.kt
  10. 2
      app/src/main/java/sushi/hardcore/droidfs/GocryptfsVolume.kt
  11. 6
      app/src/main/java/sushi/hardcore/droidfs/LoadingTask.kt
  12. 383
      app/src/main/java/sushi/hardcore/droidfs/MainActivity.kt
  13. 265
      app/src/main/java/sushi/hardcore/droidfs/OpenActivity.kt
  14. 36
      app/src/main/java/sushi/hardcore/droidfs/SettingsActivity.kt
  15. 53
      app/src/main/java/sushi/hardcore/droidfs/Volume.kt
  16. 397
      app/src/main/java/sushi/hardcore/droidfs/VolumeActionActivity.kt
  17. 20
      app/src/main/java/sushi/hardcore/droidfs/VolumeDatabase.kt
  18. 113
      app/src/main/java/sushi/hardcore/droidfs/adapters/SavedVolumesAdapter.kt
  19. 149
      app/src/main/java/sushi/hardcore/droidfs/adapters/VolumeAdapter.kt
  20. 6
      app/src/main/java/sushi/hardcore/droidfs/add_volume/Action.kt
  21. 77
      app/src/main/java/sushi/hardcore/droidfs/add_volume/AddVolumeActivity.kt
  22. 183
      app/src/main/java/sushi/hardcore/droidfs/add_volume/CreateVolumeFragment.kt
  23. 286
      app/src/main/java/sushi/hardcore/droidfs/add_volume/SelectPathFragment.kt
  24. 67
      app/src/main/java/sushi/hardcore/droidfs/explorers/BaseExplorerActivity.kt
  25. 6
      app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt
  26. 5
      app/src/main/java/sushi/hardcore/droidfs/file_viewers/ImageViewer.kt
  27. 1
      app/src/main/java/sushi/hardcore/droidfs/file_viewers/MediaPlayer.kt
  28. 26
      app/src/main/java/sushi/hardcore/droidfs/file_viewers/PdfViewer.kt
  29. 18
      app/src/main/java/sushi/hardcore/droidfs/file_viewers/TextEditor.kt
  30. 20
      app/src/main/java/sushi/hardcore/droidfs/util/WidgetUtil.kt
  31. 21
      app/src/main/java/sushi/hardcore/droidfs/util/Wiper.kt
  32. 46
      app/src/main/java/sushi/hardcore/droidfs/widgets/NonScrollableColoredBorderListView.kt
  33. 26
      app/src/main/native/gocryptfs_jni.c
  34. 5
      app/src/main/res/drawable/icon_arrow_back.xml
  35. 2
      app/src/main/res/drawable/icon_delete.xml
  36. 9
      app/src/main/res/drawable/icon_hidden.xml
  37. 9
      app/src/main/res/drawable/icon_volume.xml
  38. 10
      app/src/main/res/drawable/listview_border.xml
  39. BIN
      app/src/main/res/drawable/logo.png
  40. 8
      app/src/main/res/layout/action_bar.xml
  41. 12
      app/src/main/res/layout/activity_add_volume.xml
  42. 2
      app/src/main/res/layout/activity_audio_player.xml
  43. 173
      app/src/main/res/layout/activity_change_password.xml
  44. 100
      app/src/main/res/layout/activity_create.xml
  45. 2
      app/src/main/res/layout/activity_explorer.xml
  46. 2
      app/src/main/res/layout/activity_explorer_base.xml
  47. 2
      app/src/main/res/layout/activity_explorer_drop.xml
  48. 3
      app/src/main/res/layout/activity_image_viewer.xml
  49. 71
      app/src/main/res/layout/activity_main.xml
  50. 70
      app/src/main/res/layout/activity_open.xml
  51. 14
      app/src/main/res/layout/activity_pdf_viewer.xml
  52. 16
      app/src/main/res/layout/activity_settings.xml
  53. 36
      app/src/main/res/layout/activity_text_editor.xml
  54. 32
      app/src/main/res/layout/activity_text_editor_wrap.xml
  55. 4
      app/src/main/res/layout/adapter_explorer_element.xml
  56. 25
      app/src/main/res/layout/adapter_saved_volume.xml
  57. 54
      app/src/main/res/layout/adapter_volume.xml
  58. 20
      app/src/main/res/layout/checkboxes_section.xml
  59. 24
      app/src/main/res/layout/dialog_delete_volume.xml
  60. 4
      app/src/main/res/layout/dialog_edit_text.xml
  61. 2
      app/src/main/res/layout/dialog_loading.xml
  62. 26
      app/src/main/res/layout/dialog_open_volume.xml
  63. 11
      app/src/main/res/layout/dialog_sdcard_error.xml
  64. 2
      app/src/main/res/layout/explorer_content.xml
  65. 74
      app/src/main/res/layout/fragment_create_volume.xml
  66. 113
      app/src/main/res/layout/fragment_select_path.xml
  67. 19
      app/src/main/res/layout/toolbar.xml
  68. 68
      app/src/main/res/layout/volume_path_section.xml
  69. 33
      app/src/main/res/menu/main_activity.xml
  70. 36
      app/src/main/res/values-pt-rBR/strings.xml
  71. 36
      app/src/main/res/values-ru/strings.xml
  72. 1
      app/src/main/res/values/colors.xml
  73. 23
      app/src/main/res/values/dimens.xml
  74. 82
      app/src/main/res/values/strings.xml
  75. 3
      app/src/main/res/values/styles.xml
  76. 2
      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"

@ -1 +1 @@
Subproject commit 1da2407a614f17a3c64d14ee34fb41e081db9a71
Subproject commit 89966b1aaef93ac842f2240451ebfd50dd2bbce9

@ -31,61 +31,30 @@
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/BaseTheme">
<activity
android:name=".CameraActivity"
android:screenOrientation="nosensor" />
<activity
android:name=".SettingsActivity"
android:label="@string/title_activity_settings"
android:parentActivityName=".MainActivity" />
<activity android:name=".explorers.ExplorerActivity" />
<activity android:name=".explorers.ExplorerActivityPick" />
<activity android:name=".explorers.ExplorerActivityDrop" />
<activity
android:name=".OpenActivity"
android:parentActivityName=".MainActivity"
android:screenOrientation="nosensor">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:label="@string/share_menu_label">
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
</intent-filter>
</activity>
<activity
android:name=".CreateActivity"
android:parentActivityName=".MainActivity"
android:screenOrientation="nosensor" />
<activity
android:name=".ChangePasswordActivity"
android:parentActivityName=".MainActivity"
android:screenOrientation="nosensor" />
<activity
android:name=".MainActivity"
android:screenOrientation="nosensor">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".file_viewers.ImageViewer"
android:configChanges="screenSize|orientation" /> <!-- don't reload content on configuration change -->
<activity
android:name=".file_viewers.VideoPlayer"
android:configChanges="screenSize|orientation" />
<activity
android:name=".file_viewers.PdfViewer"
android:configChanges="screenSize|orientation" />
<activity
android:name=".file_viewers.AudioPlayer"
android:configChanges="screenSize|orientation" />
<activity
android:name=".file_viewers.TextEditor"
android:configChanges="screenSize|orientation" />
<activity android:name=".SettingsActivity" android:label="@string/title_activity_settings"/>
<activity android:name=".add_volume.AddVolumeActivity" android:windowSoftInputMode="adjustResize"/>
<activity android:name=".ChangePasswordActivity" android:windowSoftInputMode="adjustResize"/>
<activity android:name=".explorers.ExplorerActivity"/>
<activity android:name=".explorers.ExplorerActivityPick"/>
<activity android:name=".explorers.ExplorerActivityDrop"/>
<activity android:name=".file_viewers.ImageViewer" android:configChanges="screenSize|orientation" /> <!-- don't reload content on configuration change -->
<activity android:name=".file_viewers.VideoPlayer" android:configChanges="screenSize|orientation" />
<activity android:name=".file_viewers.PdfViewer" android:configChanges="screenSize|orientation" />
<activity android:name=".file_viewers.AudioPlayer" android:configChanges="screenSize|orientation" />
<activity android:name=".file_viewers.TextEditor" android:configChanges="screenSize|orientation" />
<activity android:name=".CameraActivity" android:screenOrientation="nosensor" />
<service android:name=".file_operations.FileOperationService" android:exported="false"/>

@ -8,13 +8,13 @@ import androidx.preference.PreferenceManager
open class BaseActivity: AppCompatActivity() {
protected lateinit var sharedPrefs: SharedPreferences
protected lateinit var themeValue: String
protected var shouldCheckTheme = true
lateinit var themeValue: String
private var shouldCheckTheme = true
override fun onCreate(savedInstanceState: Bundle?) {
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
if (shouldCheckTheme) {
themeValue = sharedPrefs.getString("theme", "dark_green")!!
themeValue = sharedPrefs.getString("theme", ConstValues.DEFAULT_THEME_VALUE)!!
when (themeValue) {
"black_green" -> setTheme(R.style.BlackGreen)
"dark_red" -> setTheme(R.style.DarkRed)

@ -91,6 +91,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
binding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.hide()
gocryptfsVolume = GocryptfsVolume(applicationContext, intent.getIntExtra("sessionID", -1))
outputDirectory = intent.getStringExtra("path")!!

@ -1,195 +1,161 @@
package sushi.hardcore.droidfs
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.widget.AdapterView.OnItemClickListener
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import sushi.hardcore.droidfs.adapters.SavedVolumesAdapter
import sushi.hardcore.droidfs.databinding.ActivityChangePasswordBinding
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.WidgetUtil
import sushi.hardcore.droidfs.util.Wiper
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import java.io.File
import java.util.*
class ChangePasswordActivity : VolumeActionActivity() {
private lateinit var savedVolumesAdapter: SavedVolumesAdapter
class ChangePasswordActivity: BaseActivity() {
private lateinit var binding: ActivityChangePasswordBinding
private lateinit var volume: Volume
private lateinit var volumeDatabase: VolumeDatabase
private var fingerprintProtector: FingerprintProtector? = null
private var usfFingerprint: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
volume = intent.getParcelableExtra("volume")!!
binding = ActivityChangePasswordBinding.inflate(layoutInflater)
setContentView(binding.root)
setupLayout()
setupFingerprintStuff()
savedVolumesAdapter = SavedVolumesAdapter(this, themeValue, volumeDatabase)
if (savedVolumesAdapter.count > 0){
binding.savedPathListview.adapter = savedVolumesAdapter
binding.savedPathListview.onItemClickListener = OnItemClickListener { _, _, position, _ ->
val volume = savedVolumesAdapter.getItem(position)
currentVolumeName = volume.name
if (volume.isHidden){
switchHiddenVolume.isChecked = true
editVolumeName.setText(currentVolumeName)
} else {
switchHiddenVolume.isChecked = false
editVolumePath.setText(currentVolumeName)
}
onClickSwitchHiddenVolume()
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
}
} 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
}
}
}
if (!usfFingerprint || fingerprintProtector == null) {
binding.checkboxSavePassword.visibility = View.GONE
}
editVolumePath.addTextChangedListener(textWatcher)
editVolumeName.addTextChangedListener(textWatcher)
binding.editNewPasswordConfirm.setOnEditorActionListener { _, _, _ ->
checkVolumePathThenChangePassword()
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 showCurrentPasswordInput() {
binding.textCurrentPasswordLabel.visibility = View.VISIBLE
binding.editCurrentPassword.visibility = View.VISIBLE
}
private fun changePassword(givenHash: ByteArray? = null){
val newPassword = binding.editNewPassword.text.toString().toCharArray()
val newPasswordConfirm = binding.editNewPasswordConfirm.text.toString().toCharArray()
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
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
}
}
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
}
}
override fun onPasswordHashDecrypted(hash: ByteArray) {
changeVolumePassword(newPassword, hash)
}
override fun onPasswordHashSaved() {}
override fun onFailed(pending: Boolean) {
Arrays.fill(newPassword, 0.toChar())
}
}
it.loadPasswordHash(volume.name, encryptedHash, iv)
}
if (changePasswordImmediately) {
if (GocryptfsVolume.changePassword(currentVolumePath, oldPassword, givenHash, newPassword, returnedHash)) {
val volume = Volume(currentVolumeName, switchHiddenVolume.isChecked)
if (volumeDatabase.isHashSaved(currentVolumeName)) {
volumeDatabase.removeHash(volume)
}
if (checkboxRememberPath.isChecked) {
volumeDatabase.saveVolume(volume)
}
if (checkboxSavePassword.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
stopTask {
savePasswordHash(returnedHash) {
onPasswordChanged()
}
}
if (changeWithCurrentPassword) {
changeVolumePassword(newPassword)
}
}
Arrays.fill(newPasswordConfirm, 0.toChar())
}
private fun changeVolumePassword(newPassword: CharArray, givenHash: ByteArray? = null) {
object : LoadingTask(this, themeValue, R.string.loading_msg_change_password) {
override fun doTask(activity: AppCompatActivity) {
var returnedHash: ByteArray? = null
if (binding.checkboxSavePassword.isChecked) {
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
}
var currentPassword: CharArray? = null
if (givenHash == null) {
currentPassword = CharArray(binding.editCurrentPassword.text.length)
binding.editCurrentPassword.text.getChars(0, currentPassword.size, currentPassword, 0)
}
if (GocryptfsVolume.changePassword(volume.getFullPath(filesDir.path), currentPassword, givenHash, newPassword, returnedHash)) {
if (volumeDatabase.isHashSaved(volume.name)) {
volumeDatabase.removeHash(volume)
}
stopTask {
@SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
if (binding.checkboxSavePassword.isChecked && returnedHash != null) {
fingerprintProtector!!.let {
it.listener = object : FingerprintProtector.Listener {
override fun onHashStorageReset() {
// retry
it.savePasswordHash(volume, returnedHash)
}
override fun onPasswordHashDecrypted(hash: ByteArray) {}
override fun onPasswordHashSaved() {
Arrays.fill(returnedHash, 0)
finish()
}
override fun onFailed(pending: Boolean) {
if (!pending) {
Arrays.fill(returnedHash, 0)
finish()
}
}
}
} else {
stopTask { onPasswordChanged() }
it.savePasswordHash(volume, returnedHash)
}
} else {
stopTask {
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.error)
.setMessage(R.string.change_password_failed)
.setPositiveButton(R.string.ok, null)
.show()
}
finish()
}
}
Arrays.fill(oldPassword, 0.toChar())
}
override fun doFinally(activity: AppCompatActivity) {
Arrays.fill(newPassword, 0.toChar())
Arrays.fill(newPasswordConfirm, 0.toChar())
} else {
stopTask {
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.error)
.setMessage(R.string.change_password_failed)
.setPositiveButton(R.string.ok, null)
.show()
}
}
if (currentPassword != null)
Arrays.fill(currentPassword, 0.toChar())
Arrays.fill(newPassword, 0.toChar())
if (givenHash != null)
Arrays.fill(givenHash, 0)
}
}
}
private fun onPasswordChanged(){
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.success_change_password)
.setMessage(R.string.success_change_password_msg)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> finish() }
.show()
}
override fun onDestroy() {
super.onDestroy()
Wiper.wipeEditText(binding.editOldPassword)
Wiper.wipeEditText(binding.editNewPassword)
Wiper.wipeEditText(binding.editNewPasswordConfirm)
}
}

@ -15,6 +15,7 @@ class ConstValues {
const val MAX_KERNEL_WRITE = 128*1024
const val wipe_passes = 2
const val slideshow_delay: Long = 4000
const val DEFAULT_THEME_VALUE = "dark_green"
private val fileExtensions = mapOf(
Pair("image", listOf("png", "jpg", "jpeg", "gif", "webp", "bmp")),
Pair("video", listOf("mp4", "webm", "mkv", "mov")),

@ -1,180 +0,0 @@
package sushi.hardcore.droidfs
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import sushi.hardcore.droidfs.databinding.ActivityCreateBinding
import sushi.hardcore.droidfs.explorers.ExplorerActivity
import sushi.hardcore.droidfs.util.Wiper
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import java.io.File
import java.util.*
class CreateActivity : VolumeActionActivity() {
private var sessionID = -1
private var isStartingExplorer = false
private lateinit var binding: ActivityCreateBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCreateBinding.inflate(layoutInflater)
setContentView(binding.root)
setupLayout()
setupFingerprintStuff(mayDecrypt = false)
binding.editPasswordConfirm.setOnEditorActionListener { _, _, _ ->
createVolume()
true
}
binding.spinnerXchacha.adapter = ArrayAdapter(
this@CreateActivity,
android.R.layout.simple_spinner_item,
resources.getStringArray(R.array.encryption_cipher)
).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
binding.spinnerXchacha.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
if (position == 1) {
CustomAlertDialogBuilder(this@CreateActivity, themeValue)
.setTitle(R.string.warning)
.setMessage(R.string.xchacha_warning)
.setPositiveButton(R.string.ok, null)
.show()
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
binding.buttonCreate.setOnClickListener {
createVolume()
}
}
override fun onClickSwitchHiddenVolume() {
super.onClickSwitchHiddenVolume()
if (switchHiddenVolume.isChecked){
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.warning)
.setMessage(R.string.hidden_volume_warning)
.setPositiveButton(R.string.ok, null)
.show()
}
}
fun createVolume() {
loadVolumePath {
val password = binding.editPassword.text.toString().toCharArray()
val passwordConfirm = binding.editPasswordConfirm.text.toString().toCharArray()
if (!password.contentEquals(passwordConfirm)) {
Toast.makeText(this, R.string.passwords_mismatch, Toast.LENGTH_SHORT).show()
} else {
val volumeFile = File(currentVolumePath)
var goodDirectory = false
if (!volumeFile.isDirectory) {
if (volumeFile.mkdirs()) {
goodDirectory = true
} else {
errorDirectoryNotWritable(R.string.create_cant_write_error_msg)
}
} else {
val dirContent = volumeFile.list()
if (dirContent != null) {
if (dirContent.isEmpty()) {
if (volumeFile.canWrite()) {
goodDirectory = true
} else {
errorDirectoryNotWritable(R.string.create_cant_write_error_msg)
}
} else {
Toast.makeText(this, R.string.dir_not_empty, Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(this, R.string.listdir_null_error_msg, Toast.LENGTH_SHORT).show()
}
}
if (goodDirectory) {
object: LoadingTask(this, themeValue, R.string.loading_msg_create) {
override fun doTask(activity: AppCompatActivity) {
val xchacha = when (binding.spinnerXchacha.selectedItemPosition) {
0 -> 0
1 -> 1
else -> -1
}
if (GocryptfsVolume.createVolume(currentVolumePath, password, false, xchacha, GocryptfsVolume.ScryptDefaultLogN, ConstValues.creator)) {
var returnedHash: ByteArray? = null
if (checkboxSavePassword.isChecked){
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
}
sessionID = GocryptfsVolume.init(currentVolumePath, password, null, returnedHash)
if (sessionID != -1) {
if (checkboxRememberPath.isChecked) {
if (volumeDatabase.isVolumeSaved(currentVolumeName)) { //cleaning old saved path
volumeDatabase.removeVolume(Volume(currentVolumeName))
}
volumeDatabase.saveVolume(Volume(currentVolumeName, switchHiddenVolume.isChecked))
}
if (checkboxSavePassword.isChecked && returnedHash != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){
stopTask {
savePasswordHash(returnedHash) {
startExplorer()
}
}
} else {
stopTask { startExplorer() }
}
} else {
stopTaskWithToast(R.string.open_volume_failed)
}
} else {
stopTask {
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.error)
.setMessage(R.string.create_volume_failed)
.setPositiveButton(R.string.ok, null)
.show()
}
}
}
override fun doFinally(activity: AppCompatActivity) {
Arrays.fill(password, 0.toChar())
Arrays.fill(passwordConfirm, 0.toChar())
}
}
}
}
}
}
private fun startExplorer(){
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.success_volume_create)
.setMessage(R.string.success_volume_create_msg)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ ->
val intent = Intent(this, ExplorerActivity::class.java)
intent.putExtra("sessionID", sessionID)
intent.putExtra("volume_name", File(currentVolumeName).name)
startActivity(intent)
isStartingExplorer = true
finish()
}
.show()
}
override fun onPause() {
super.onPause()
//Closing volume if leaving activity while showing dialog
if (sessionID != -1 && !isStartingExplorer) {
GocryptfsVolume(applicationContext, sessionID).close()
finish()
}
}
override fun onDestroy() {
super.onDestroy()
Wiper.wipeEditText(binding.editPassword)
Wiper.wipeEditText(binding.editPasswordConfirm)
}
}

@ -0,0 +1,259 @@
package sushi.hardcore.droidfs
import android.app.KeyguardManager
import android.content.Context
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import java.security.KeyStore
import java.security.KeyStoreException
import java.security.UnrecoverableKeyException
import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec
@RequiresApi(Build.VERSION_CODES.M)
class FingerprintProtector private constructor(
private val activity: FragmentActivity,
private val themeValue: String,
private val volumeDatabase: VolumeDatabase,
) {
interface Listener {
fun onHashStorageReset()
fun onPasswordHashDecrypted(hash: ByteArray)
fun onPasswordHashSaved()
fun onFailed(pending: Boolean)
}
companion object {
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
private const val KEY_ALIAS = "Hash Key"
private const val KEY_SIZE = 256
private const val GCM_TAG_LEN = 128
fun canAuthenticate(context: Context): Int {
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
return if (!keyguardManager.isKeyguardSecure)
1
else when (BiometricManager.from(context).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
BiometricManager.BIOMETRIC_SUCCESS -> 0
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> 2
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> 3
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> 4
else -> -1
}
}
fun new(
activity: FragmentActivity,
themeValue: String,
volumeDatabase: VolumeDatabase,
): FingerprintProtector? {
return if (canAuthenticate(activity) == 0)
FingerprintProtector(activity, themeValue, volumeDatabase)
else
null
}
}
lateinit var listener: Listener
private val biometricPrompt = BiometricPrompt(activity, ContextCompat.getMainExecutor(activity), object: BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
if (
errorCode != BiometricPrompt.ERROR_USER_CANCELED &&
errorCode != BiometricPrompt.ERROR_NEGATIVE_BUTTON &&
errorCode != BiometricPrompt.ERROR_TIMEOUT
) {
Toast.makeText(activity, activity.getString(R.string.biometric_error, errString), Toast.LENGTH_SHORT).show()
}
listener.onFailed(false)
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
val cipherObject = result.cryptoObject?.cipher
if (cipherObject != null) {
try {
when (cipherActionMode) {
Cipher.ENCRYPT_MODE -> {
val cipherText = cipherObject.doFinal(dataToProcess)
volume.encryptedHash = cipherText
volume.iv = cipherObject.iv
if (volumeDatabase.addHash(volume))
listener.onPasswordHashSaved()
else
listener.onFailed(false)
}
Cipher.DECRYPT_MODE -> {
try {
val plainText = cipherObject.doFinal(dataToProcess)
listener.onPasswordHashDecrypted(plainText)
} catch (e: AEADBadTagException) {
listener.onFailed(true)
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.error)
.setMessage(R.string.MAC_verification_failed)
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
resetHashStorage()
}
.setNegativeButton(R.string.cancel) { _, _ -> listener.onFailed(false) }
.setOnCancelListener { listener.onFailed(false) }
.show()
}
}
}
} catch (e: IllegalBlockSizeException) {
listener.onFailed(true)
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.illegal_block_size_exception)
.setMessage(R.string.illegal_block_size_exception_msg)
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
resetHashStorage()
}
.setNegativeButton(R.string.cancel) { _, _ -> listener.onFailed(false) }
.setOnCancelListener { listener.onFailed(false) }
.show()
}
} else {
Toast.makeText(activity, R.string.error_cipher_null, Toast.LENGTH_SHORT).show()
listener.onFailed(false)
}
}
})
private lateinit var keyStore: KeyStore
private lateinit var key: SecretKey
private lateinit var cipher: Cipher
private var isCipherReady = false
private var cipherActionMode: Int? = null
private lateinit var volume: Volume
private lateinit var dataToProcess: ByteArray
private fun resetHashStorage() {
try {
keyStore.deleteEntry(KEY_ALIAS)
} catch (e: KeyStoreException) {
e.printStackTrace()
}
volumeDatabase.getVolumes().forEach { volume ->
volumeDatabase.removeHash(volume)
}
isCipherReady = false
Toast.makeText(activity, R.string.hash_storage_reset, Toast.LENGTH_SHORT).show()
listener.onHashStorageReset()
}
private fun prepareCipher(): Boolean {
if (!isCipherReady) {
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
keyStore.load(null)
key = if (keyStore.containsAlias(KEY_ALIAS)) {
try {
keyStore.getKey(KEY_ALIAS, null) as SecretKey
} catch (e: UnrecoverableKeyException) {
listener.onFailed(true)
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(activity.getString(R.string.unrecoverable_key_exception))
.setMessage(activity.getString(R.string.unrecoverable_key_exception_msg, e.localizedMessage))
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
resetHashStorage()
}
.setNegativeButton(R.string.cancel) { _, _ -> listener.onFailed(false) }
.setOnCancelListener { listener.onFailed(false) }
.show()
return false
}
} else {
val builder = KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
builder.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
builder.setKeySize(KEY_SIZE)
builder.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
builder.setUserAuthenticationRequired(true)
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
ANDROID_KEY_STORE
)
keyGenerator.init(builder.build())
keyGenerator.generateKey()
}
cipher = Cipher.getInstance(
KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_GCM + "/" + KeyProperties.ENCRYPTION_PADDING_NONE
)
isCipherReady = true
}
return true
}
private fun alertKeyPermanentlyInvalidatedException() {
listener.onFailed(true)
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.key_permanently_invalidated_exception)
.setMessage(R.string.key_permanently_invalidated_exception_msg)
.setPositiveButton(R.string.reset_hash_storage) { _, _ ->
resetHashStorage()
}
.setNegativeButton(R.string.cancel) { _, _ -> listener.onFailed(false) }
.setOnCancelListener { listener.onFailed(false) }
.show()
}
fun savePasswordHash(volume: Volume, plainText: ByteArray) {
this.volume = volume
val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(activity.getString(R.string.encrypt_action_description))
.setSubtitle(volume.shortName)
.setDescription(activity.getString(R.string.fingerprint_instruction))
.setNegativeButtonText(activity.getString(R.string.cancel))
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.setConfirmationRequired(false)
.build()
cipherActionMode = Cipher.ENCRYPT_MODE
if (prepareCipher()) {
try {
cipher.init(Cipher.ENCRYPT_MODE, key)
dataToProcess = plainText
biometricPrompt.authenticate(
biometricPromptInfo,
BiometricPrompt.CryptoObject(cipher)
)
} catch (e: KeyPermanentlyInvalidatedException) {
alertKeyPermanentlyInvalidatedException()
}
}
}
fun loadPasswordHash(volumeName: String, cipherText: ByteArray, iv: ByteArray) {
val biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(activity.getString(R.string.decrypt_action_description))
.setSubtitle(volumeName)
.setDescription(activity.getString(R.string.fingerprint_instruction))
.setNegativeButtonText(activity.getString(R.string.cancel))
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.setConfirmationRequired(false)
.build()
cipherActionMode = Cipher.DECRYPT_MODE
if (prepareCipher()) {
dataToProcess = cipherText
val gcmSpec = GCMParameterSpec(GCM_TAG_LEN, iv)
try {
cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec)
biometricPrompt.authenticate(
biometricPromptInfo,
BiometricPrompt.CryptoObject(cipher)
)
} catch (e: KeyPermanentlyInvalidatedException) {
alertKeyPermanentlyInvalidatedException()
}
}
}
}

@ -30,7 +30,7 @@ class GocryptfsVolume(val applicationContext: Context, var sessionID: Int) {
const val KeyLen = 32
const val ScryptDefaultLogN = 16
const val DefaultBS = 4096
external fun createVolume(root_cipher_dir: String, password: CharArray, plainTextNames: Boolean, xchacha: Int, logN: Int, creator: String): Boolean
external fun createVolume(root_cipher_dir: String, password: CharArray, plainTextNames: Boolean, xchacha: Int, logN: Int, creator: String, returnedHash: ByteArray?): Boolean
external fun init(root_cipher_dir: String, password: CharArray?, givenHash: ByteArray?, returnedHash: ByteArray?): Int
external fun changePassword(root_cipher_dir: String, old_password: CharArray?, givenHash: ByteArray?, new_password: CharArray, returnedHash: ByteArray?): Boolean

@ -1,7 +1,6 @@
package sushi.hardcore.droidfs
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
@ -19,7 +18,6 @@ abstract class LoadingTask(val activity: AppCompatActivity, themeValue: String,
startTask()
}
abstract fun doTask(activity: AppCompatActivity)
open fun doFinally(activity: AppCompatActivity){}
private fun startTask() {
dialogLoading.show()
Thread {
@ -27,7 +25,6 @@ abstract class LoadingTask(val activity: AppCompatActivity, themeValue: String,
if (!isStopped){
dialogLoading.dismiss()
}
activity.runOnUiThread { doFinally(activity) }
}.start()
}
fun stopTask(onUiThread: (() -> Unit)?){
@ -39,7 +36,4 @@ abstract class LoadingTask(val activity: AppCompatActivity, themeValue: String,
}
}
}
protected fun stopTaskWithToast(stringId: Int){
stopTask { Toast.makeText(activity, stringId, Toast.LENGTH_SHORT).show() }
}
}

@ -1,19 +1,69 @@
package sushi.hardcore.droidfs
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import sushi.hardcore.droidfs.adapters.VolumeAdapter
import sushi.hardcore.droidfs.add_volume.AddVolumeActivity
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
import sushi.hardcore.droidfs.databinding.ActivityMainBinding
import sushi.hardcore.droidfs.databinding.DialogDeleteVolumeBinding
import sushi.hardcore.droidfs.databinding.DialogOpenVolumeBinding
import sushi.hardcore.droidfs.explorers.ExplorerActivity
import sushi.hardcore.droidfs.explorers.ExplorerActivityDrop
import sushi.hardcore.droidfs.explorers.ExplorerActivityPick
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import java.io.File
import java.util.*
class MainActivity : BaseActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var volumeDatabase: VolumeDatabase
private lateinit var volumeAdapter: VolumeAdapter
private var fingerprintProtector: FingerprintProtector? = null
private var usfFingerprint: Boolean = false
private var usfKeepOpen: Boolean = false
private var addVolume = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
when (result.resultCode) {
AddVolumeActivity.RESULT_VOLUME_ADDED -> {
volumeAdapter.apply {
volumes = volumeDatabase.getVolumes()
notifyItemInserted(volumes.size)
}
binding.textNoVolumes.visibility = View.GONE
}
AddVolumeActivity.RESULT_HASH_STORAGE_RESET -> {
volumeAdapter.refresh()
binding.textNoVolumes.visibility = View.GONE
}
}
}
private var changePasswordPosition: Int? = null
private var changePassword = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
changePasswordPosition?.let {
volumeAdapter.selectedItems.remove(it)
volumeAdapter.onVolumeChanged(it)
}
invalidateOptionsMenu()
}
private var pickMode = false
private var dropMode = false
private var shouldCloseVolume = true // used when launched to pick file from another volume
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar.toolbar)
if (sharedPrefs.getBoolean("applicationFirstOpening", true)) {
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.warning)
@ -28,20 +78,141 @@ class MainActivity : BaseActivity() {
.setOnDismissListener { sharedPrefs.edit().putBoolean("applicationFirstOpening", false).apply() }
.show()
}
binding.buttonOpen.setOnClickListener {
startActivity(OpenActivity::class.java)
pickMode = intent.action == "pick"
dropMode = (intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE) && intent.extras != null
volumeDatabase = VolumeDatabase(this)
volumeAdapter = VolumeAdapter(
this,
volumeDatabase,
!pickMode && !dropMode,
!dropMode,
::onVolumeItemClick,
::onVolumeItemLongClick,
)
binding.recyclerViewVolumes.adapter = volumeAdapter
binding.recyclerViewVolumes.layoutManager = LinearLayoutManager(this)
if (volumeAdapter.volumes.isEmpty()) {
binding.textNoVolumes.visibility = View.VISIBLE
}
binding.buttonCreate.setOnClickListener {
startActivity(CreateActivity::class.java)
if (pickMode) {
title = getString(R.string.select_volume)
binding.fab.visibility = View.GONE
} else {
binding.fab.setOnClickListener {
addVolume.launch(Intent(this, AddVolumeActivity::class.java))
}
}
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)
}
binding.buttonChangePassword.setOnClickListener {
startActivity(ChangePasswordActivity::class.java)
}
private fun onVolumeItemClick(volume: Volume, position: Int) {
if (volumeAdapter.selectedItems.isEmpty())
openVolume(volume, position)
else
invalidateOptionsMenu()
}
private fun onVolumeItemLongClick() {
invalidateOptionsMenu()
}
private fun unselectAll() {
volumeAdapter.unSelectAll()
invalidateOptionsMenu()
}
private fun removeVolumes(volumes: List<Volume>, i: Int = 0, doDeleteVolumeContent: Boolean? = null) {
if (i < volumes.size) {
if (volumes[i].isHidden) {
if (doDeleteVolumeContent == null) {
val dialogBinding = DialogDeleteVolumeBinding.inflate(layoutInflater)
dialogBinding.textContent.text = getString(R.string.delete_hidden_volume_question, volumes[i].name)
// show checkbox only if there is at least one other hidden volume
for (j in (i+1 until volumes.size)) {
if (volumes[j].isHidden) {
dialogBinding.checkboxApplyToAll.visibility = View.VISIBLE
break
}
}
CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.warning)
.setView(dialogBinding.root)
.setPositiveButton(R.string.forget_only) { _, _ ->
volumeDatabase.removeVolume(volumes[i].name)
removeVolumes(volumes, i + 1, if (dialogBinding.checkboxApplyToAll.isChecked) false else null)
}
.setNegativeButton(R.string.delete_volume) { _, _ ->
PathUtils.recursiveRemoveDirectory(File(volumes[i].getFullPath(filesDir.path)))
volumeDatabase.removeVolume(volumes[i].name)
removeVolumes(volumes, i + 1, if (dialogBinding.checkboxApplyToAll.isChecked) true else null)
}
.setOnCancelListener {
volumeAdapter.refresh()
invalidateOptionsMenu()
}
.show()
} else {
if (doDeleteVolumeContent) {
PathUtils.recursiveRemoveDirectory(File(volumes[i].getFullPath(filesDir.path)))
}
volumeDatabase.removeVolume(volumes[i].name)
removeVolumes(volumes, i + 1, doDeleteVolumeContent)
}
} else {
volumeDatabase.removeVolume(volumes[i].name)
removeVolumes(volumes, i + 1, doDeleteVolumeContent)
}
} else {
volumeAdapter.refresh()
invalidateOptionsMenu()
if (volumeAdapter.volumes.isEmpty()) {
binding.textNoVolumes.visibility = View.VISIBLE
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.menu_settings -> {
android.R.id.home -> {
if (pickMode || dropMode) {
if (pickMode)
shouldCloseVolume = false
finish()
} else {
unselectAll()
}
true
}
R.id.select_all -> {
volumeAdapter.selectAll()
invalidateOptionsMenu()
true
}
R.id.remove -> {
val selectedVolumes = volumeAdapter.selectedItems.map { i -> volumeAdapter.volumes[i] }
removeVolumes(selectedVolumes)
true
}
R.id.forget_password -> {
for (i in volumeAdapter.selectedItems) {
if (volumeDatabase.removeHash(volumeAdapter.volumes[i]))
volumeAdapter.onVolumeChanged(i)
}
unselectAll()
true
}
R.id.change_password -> {
changePasswordPosition = volumeAdapter.selectedItems.elementAt(0)
changePassword.launch(Intent(this, ChangePasswordActivity::class.java).apply {
putExtra("volume", volumeAdapter.volumes[changePasswordPosition!!])
})
true
}
R.id.settings -> {
val intent = Intent(this, SettingsActivity::class.java)
startActivity(intent)
true
@ -52,11 +223,199 @@ class MainActivity : BaseActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_activity, menu)
menu.findItem(R.id.settings).isVisible = !pickMode && !dropMode
val isSelecting = volumeAdapter.selectedItems.isNotEmpty()
menu.findItem(R.id.select_all).isVisible = isSelecting && !pickMode && !dropMode
menu.findItem(R.id.remove).isVisible = isSelecting && !pickMode && !dropMode
var showForgetPassword = isSelecting
if (isSelecting) {
for (volume in volumeAdapter.selectedItems.map { i -> volumeAdapter.volumes[i] }) {
if (volume.encryptedHash == null) {
showForgetPassword = false
break
}
}
}
menu.findItem(R.id.forget_password).isVisible = showForgetPassword && !pickMode
menu.findItem(R.id.change_password).isVisible =
!pickMode && !dropMode &&
volumeAdapter.selectedItems.size == 1 &&
volumeAdapter.volumes[volumeAdapter.selectedItems.elementAt(0)].canWrite(filesDir.path)
supportActionBar?.setDisplayHomeAsUpEnabled(isSelecting || pickMode || dropMode)
return true
}
fun <T> startActivity(clazz: Class<T>) {
val intent = Intent(this, clazz)
startActivity(intent)
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
private fun openVolume(volume: Volume, position: Int) {
var askForPassword = true
fingerprintProtector?.let { fingerprintProtector ->
volume.encryptedHash?.let { encryptedHash ->
volume.iv?.let { iv ->
askForPassword = false
fingerprintProtector.listener = object : FingerprintProtector.Listener {
override fun onHashStorageReset() {
volumeAdapter.refresh()
}
override fun onPasswordHashDecrypted(hash: ByteArray) {
object : LoadingTask(this@MainActivity, themeValue, R.string.loading_msg_open) {
override fun doTask(activity: AppCompatActivity) {
val sessionId = GocryptfsVolume.init(volume.getFullPath(filesDir.path), null, hash, null)
Arrays.fill(hash, 0)
if (sessionId != -1)
stopTask { startExplorer(sessionId, volume.shortName) }
else
stopTask {
CustomAlertDialogBuilder(activity, themeValue)
.setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_failed_hash_msg)
.setPositiveButton(R.string.ok, null)
.show()
}
}
}
}
override fun onPasswordHashSaved() {}
override fun onFailed(pending: Boolean) {}
}
fingerprintProtector.loadPasswordHash(volume.shortName, encryptedHash, iv)
}
}
}
if (askForPassword)
askForPassword(volume, position)
}
private fun onPasswordSubmitted(volume: Volume, position: Int, dialogBinding: DialogOpenVolumeBinding) {
val password = CharArray(dialogBinding.editPassword.text.length)
dialogBinding.editPassword.text.getChars(0, password.size, password, 0)
// openVolumeWithPassword is responsible for wiping the password
openVolumeWithPassword(
volume,
position,
password,
dialogBinding.checkboxSavePassword.isChecked,
)
}
private fun askForPassword(volume: Volume, position: Int) {
val dialogBinding = DialogOpenVolumeBinding.inflate(layoutInflater)
if (!usfFingerprint || fingerprintProtector == null) {
dialogBinding.checkboxSavePassword.visibility = View.GONE
}
val dialog = CustomAlertDialogBuilder(this, themeValue)
.setTitle(R.string.open_dialog_title)
.setView(dialogBinding.root)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.open) { _, _ ->
onPasswordSubmitted(volume, position, dialogBinding)
}
.create()
dialogBinding.editPassword.setOnEditorActionListener { _, _, _ ->
dialog